Standort-Ansicht für Sonnenfinsternis-Simulation hinzufügen

Zusätzlich zur 3D-Weltraum-Ansicht zeigt ein neuer Umschalter jetzt die
Finsternis als Sonnenscheibe mit Mond davor, wie sie am gespeicherten
Favoritenstandort tatsächlich am Himmel erscheint. Dazu werden Sonne
und Mond topozentrisch (inkl. lunarer Parallaxe) berechnet und die
Ausrichtung per parallaktischem Winkel auf Zenit-oben gedreht, statt
nur Norden-oben wie in Sternkarten.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 09:03:19 +02:00
co-authored by Claude Sonnet 5
parent fb75b180e2
commit d9a8b07a8d
2 changed files with 214 additions and 1 deletions
+23
View File
@@ -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;
+191 -1
View File
@@ -71,7 +71,15 @@ $userAlt = isset($currentLocation['elevation']) ? (float)$currentLocation['eleva
</aside>
<div class="solarsystem-stage-wrap" id="eclipseStageWrap">
<div class="eclipse-view-toggle">
<button class="btn btn-sm btn-secondary is-active" id="btnSpaceView">Weltraum</button>
<button class="btn btn-sm btn-secondary" id="btnLocalView">Standort</button>
</div>
<canvas id="eclipseCanvas" class="eclipse-canvas"></canvas>
<canvas id="eclipseLocalCanvas" class="eclipse-canvas is-hidden"></canvas>
<div id="eclipseLocalHint" class="eclipse-local-hint"></div>
<div id="eclipseScaleNote" class="eclipse-scale-note">
Distanzen stark komprimiert<br>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 => {