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 => {