diff --git a/public/css/style.css b/public/css/style.css index e4b63d1..ce79b0a 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -4899,6 +4899,29 @@ body.solarsystem-page:not(.jupitersystem-page) .solarsystem-stage-wrap { gap: 0.4rem; } +.eclipse-view-toggle .btn.is-active { + background: rgba(201, 168, 76, 0.18); + color: var(--gold-pale); + border-color: rgba(201, 168, 76, 0.38); +} + +.eclipse-local-hint { + position: absolute; + inset: 0; + display: none; + align-items: center; + justify-content: center; + text-align: center; + padding: 2rem; + color: var(--text-dim); + font-size: 0.9rem; + line-height: 1.5; +} + +.eclipse-local-hint.is-visible { + display: flex; +} + /* Timeline */ .eclipse-timeline { position: absolute; diff --git a/public/sonnenfinsternis.php b/public/sonnenfinsternis.php index 264e145..4af6571 100644 --- a/public/sonnenfinsternis.php +++ b/public/sonnenfinsternis.php @@ -71,7 +71,15 @@ $userAlt = isset($currentLocation['elevation']) ? (float)$currentLocation['eleva
+
+ + +
+ + + +
Distanzen stark komprimiert
Größenverhältnisse für die Anschauung angepasst @@ -124,6 +132,8 @@ const R_EARTH_VIS = 1.0; const D_SM_VIS = 7.5; const D_ME_VIS = 8.0; const EARTH_TILT = 23.44 * Math.PI / 180; +const DEG2RAD = Math.PI / 180; +const RAD2DEG = 180 / Math.PI; const LOCAL_MATCH_HOURS = 18; const D_EM_VIS = 3.1; const SUN_RADIUS_KM = 695700; @@ -135,6 +145,10 @@ let penumbraProxyScale = 1.00; const MEAN_MOON_DIST_AU = 384400 / AE.KM_PER_AU; const canvas3d = document.getElementById('eclipseCanvas'); +const canvasLocal = document.getElementById('eclipseLocalCanvas'); +const localHint = document.getElementById('eclipseLocalHint'); +const btnSpaceView = document.getElementById('btnSpaceView'); +const btnLocalView = document.getElementById('btnLocalView'); const stageWrap = document.getElementById('eclipseStageWrap'); const infoPanel = document.getElementById('eclipseInfoPanel'); const infoBody = document.getElementById('eclipseInfoBody'); @@ -157,6 +171,7 @@ let eclipseListData = []; let currentLocal = null; let localObserver = null; let geometryContacts = null; +let viewMode = 'space'; let simTime = 0; let playing = false; let playSpeed = 10; @@ -390,6 +405,11 @@ function resizeAll() { renderer.setSize(w, h); camera.aspect = w / h; camera.updateProjectionMatrix(); + + const dpr = Math.min(window.devicePixelRatio || 1, 2); + canvasLocal.width = Math.round(w * dpr); + canvasLocal.height = Math.round(h * dpr); + if (viewMode === 'local') drawLocalView(); } function buildYearSelectors() { @@ -454,6 +474,7 @@ function selectEclipse(idx) { if (!eclipseListData[idx]) return; currentEclipse = eclipseListData[idx]; currentLocal = findLocalCircumstances(currentEclipse); + updateLocalViewAvailability(); eclipseListEl.querySelectorAll('.eclipse-list-item').forEach((el, i) => el.classList.toggle('is-selected', i === idx) @@ -492,6 +513,170 @@ function findLocalCircumstances(globalEclipse) { } } +function updateLocalViewAvailability() { + if (!localObserver) { + btnLocalView.disabled = true; + btnLocalView.title = 'Kein Standort hinterlegt (siehe Einstellungen)'; + } else if (!currentLocal) { + btnLocalView.disabled = true; + btnLocalView.title = 'Von diesem Standort aus nicht sichtbar'; + } else { + btnLocalView.disabled = false; + btnLocalView.title = ''; + } + if (btnLocalView.disabled && viewMode === 'local') setView('space'); +} + +// Topozentrische Sonne/Mond-Geometrie am Favoritenstandort, Zenit oben (parallaktischer +// Winkel angewendet), siehe Plan "Standort-Ansicht" fuer Herleitung der Formeln. +function computeLocalSkyGeometry(date) { + if (!localObserver) return null; + const astroTime = AE.MakeTime(date); + + const sunEq = AE.Equator('Sun', astroTime, localObserver, true, true); + const moonEq = AE.Equator('Moon', astroTime, localObserver, true, true); + + const sunDistKm = sunEq.dist * AE.KM_PER_AU; + const moonDistKm = moonEq.dist * AE.KM_PER_AU; + const sunAngR = Math.asin(Math.min(1, SUN_RADIUS_KM / sunDistKm)); + const moonAngR = Math.asin(Math.min(1, MOON_RADIUS_KM / moonDistKm)); + + const sunRaR = sunEq.ra * 15 * DEG2RAD; + const sunDecR = sunEq.dec * DEG2RAD; + const moonRaR = moonEq.ra * 15 * DEG2RAD; + const moonDecR = moonEq.dec * DEG2RAD; + const dRa = moonRaR - sunRaR; + + const cosSep = Math.sin(sunDecR) * Math.sin(moonDecR) + + Math.cos(sunDecR) * Math.cos(moonDecR) * Math.cos(dRa); + const sep = Math.acos(Math.max(-1, Math.min(1, cosSep))); + const pa = Math.atan2( + Math.sin(dRa), + Math.cos(sunDecR) * Math.tan(moonDecR) - Math.sin(sunDecR) * Math.cos(dRa) + ); + + const gastHours = AE.SiderealTime(astroTime); + const lstHours = ((gastHours + localObserver.longitude / 15) % 24 + 24) % 24; + const hourAngleR = (lstHours - sunEq.ra) * 15 * DEG2RAD; + const latR = localObserver.latitude * DEG2RAD; + const parallacticAngle = Math.atan2( + Math.sin(hourAngleR), + Math.tan(latR) * Math.cos(sunDecR) - Math.sin(sunDecR) * Math.cos(hourAngleR) + ); + + const paZenith = pa - parallacticAngle; + const dx = sep * Math.sin(paZenith); + const dy = -sep * Math.cos(paZenith); + + const sunHor = AE.Horizon(astroTime, localObserver, sunEq.ra, sunEq.dec, 'normal'); + + return { + visible: sunHor.altitude > -sunAngR * RAD2DEG, + sunAngR, + moonAngR, + sep, + dx, + dy, + sunAltitudeDeg: sunHor.altitude + }; +} + +function drawLocalView() { + const ctx = canvasLocal.getContext('2d'); + const W = canvasLocal.width; + const H = canvasLocal.height; + ctx.clearRect(0, 0, W, H); + ctx.fillStyle = '#010309'; + ctx.fillRect(0, 0, W, H); + + if (!localObserver) { + localHint.textContent = 'Kein Standort hinterlegt.'; + localHint.classList.add('is-visible'); + return; + } + if (!currentEclipse) { + localHint.textContent = 'Wähle zunächst eine Finsternis aus der Liste.'; + localHint.classList.add('is-visible'); + return; + } + + const geo = computeLocalSkyGeometry(simDateAt()); + if (!geo || !geo.visible) { + localHint.textContent = geo + ? `Sonne unter dem Horizont am Favoritenstandort (Höhe ${geo.sunAltitudeDeg.toFixed(1)}°).` + : 'Standortgeometrie nicht bestimmbar.'; + localHint.classList.add('is-visible'); + return; + } + localHint.classList.remove('is-visible'); + + // Static stars + for (let i = 0; i < 200; i++) { + const sx = (i * 137.508 + 60) % W; + const sy = (i * 79.317 + 40) % H; + ctx.fillStyle = `rgba(255,255,255,${(0.15 + (i % 7) * 0.05).toFixed(2)})`; + ctx.beginPath(); + ctx.arc(sx, sy, 0.4 + (i % 5) * 0.15, 0, Math.PI * 2); + ctx.fill(); + } + + const cx = W / 2, cy = H / 2; + const R = Math.min(W, H) * 0.32; + const sc = R / geo.sunAngR; + + const sunR = R; + const moonR = geo.moonAngR * sc; + const mx = cx + geo.dx * sc; + const my = cy + geo.dy * sc; + + const glow = ctx.createRadialGradient(cx, cy, sunR * 0.9, cx, cy, sunR * 2.2); + glow.addColorStop(0, 'rgba(255,241,180,0.35)'); + glow.addColorStop(1, 'rgba(255,241,180,0)'); + ctx.fillStyle = glow; + ctx.fillRect(0, 0, W, H); + + const sunGrad = ctx.createRadialGradient(cx - sunR * 0.2, cy - sunR * 0.2, sunR * 0.05, cx, cy, sunR); + sunGrad.addColorStop(0, '#fff8dd'); + sunGrad.addColorStop(0.75, '#ffe9a8'); + sunGrad.addColorStop(1, '#f2b64c'); + ctx.fillStyle = sunGrad; + ctx.beginPath(); + ctx.arc(cx, cy, sunR, 0, Math.PI * 2); + ctx.fill(); + + ctx.fillStyle = '#03040a'; + ctx.beginPath(); + ctx.arc(mx, my, moonR, 0, Math.PI * 2); + ctx.fill(); + + ctx.strokeStyle = 'rgba(10,8,4,0.35)'; + ctx.lineWidth = Math.max(1, sunR * 0.01); + ctx.beginPath(); + ctx.arc(mx, my, moonR, 0, Math.PI * 2); + ctx.stroke(); + + // Dezenter Korona-Schimmer bei Totalitaet/Ringphase + if (geo.sep < Math.max(geo.sunAngR, geo.moonAngR) * 0.15) { + const corona = ctx.createRadialGradient(cx, cy, sunR * 0.98, cx, cy, sunR * 1.5); + corona.addColorStop(0, 'rgba(255,255,240,0.55)'); + corona.addColorStop(1, 'rgba(255,255,240,0)'); + ctx.fillStyle = corona; + ctx.beginPath(); + ctx.arc(cx, cy, sunR * 1.5, 0, Math.PI * 2); + ctx.fill(); + } +} + +function setView(mode) { + viewMode = mode; + btnSpaceView.classList.toggle('is-active', mode === 'space'); + btnLocalView.classList.toggle('is-active', mode === 'local'); + canvas3d.classList.toggle('is-hidden', mode !== 'space'); + canvasLocal.classList.toggle('is-hidden', mode !== 'local'); + localHint.classList.remove('is-visible'); + if (mode === 'local') drawLocalView(); +} + function geometryEventKind() { return currentEclipse?.kind || null; } @@ -980,6 +1165,8 @@ function updateSim() { tlDisplay.textContent = cur.toISOString().slice(11, 16) + ' UTC'; updateInfoPhase(cov); + + if (viewMode === 'local') drawLocalView(); } function eventPeakDate() { @@ -1002,7 +1189,7 @@ function renderLoop(now) { updateSim(); } - renderer.render(scene, camera); + if (viewMode === 'space') renderer.render(scene, camera); } function fmtUTC(date) { @@ -1275,6 +1462,9 @@ function setupInteraction() { const collapsed = infoPanel.classList.toggle('is-collapsed'); infoToggleBtn.setAttribute('aria-expanded', String(!collapsed)); }); + + btnSpaceView.addEventListener('click', () => setView('space')); + btnLocalView.addEventListener('click', () => setView('local')); } init().catch(err => {