Files
skyview.astronomiemuseum.de/public/mondfinsternis.php
T
2026-04-02 11:17:37 +02:00

991 lines
42 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
session_start();
$loggedIn = isset($_SESSION['user_id']);
$pageTitle = 'Mondfinsternis – AstroTools';
$bodyClass = 'solarsystem-page';
?>
<?php require __DIR__ . '/header.php'; ?>
<?php
// Standort kommt aus header.php ($currentLocation)
$userLat = isset($currentLocation['latitude']) ? (float)$currentLocation['latitude'] : null;
$userLon = isset($currentLocation['longitude']) ? (float)$currentLocation['longitude'] : null;
?>
<script src="js/astronomy.browser.min.js"></script>
<div class="solarsystem-tool">
<div class="solarsystem-layout">
<!-- ── Sidebar ─────────────────────────────────── -->
<aside class="card solarsystem-sidebar" id="eclipseSidebar">
<div class="solarsystem-sidebar-block">
<h2>Mondfinsternis</h2>
</div>
<div class="solarsystem-sidebar-block">
<div class="solarsystem-label">Zeitraum</div>
<div class="solarsystem-input-row">
<select id="eclipseYearFrom" class="eclipse-year-select"></select>
<span class="eclipse-range-separator">–</span>
<select id="eclipseYearTo" class="eclipse-year-select"></select>
</div>
<button class="btn btn-secondary btn-full" id="eclipseSearchBtn">Finsternisse suchen</button>
</div>
<div class="solarsystem-sidebar-block">
<div class="solarsystem-label">Finsternisliste</div>
<div id="eclipseList" class="eclipse-list">
<p class="hint">Suche läuft…</p>
</div>
</div>
<div class="solarsystem-sidebar-block is-hidden" id="eclipseDataBlock">
<div class="solarsystem-label">Kontaktzeiten (UTC)</div>
<table class="data-table" id="eclipseContactTable"></table>
</div>
</aside>
<!-- ── Stage ──────────────────────────────────── -->
<div class="solarsystem-stage-wrap" id="eclipseStageWrap">
<!-- View toggle -->
<div class="eclipse-view-toggle">
<button class="btn btn-sm btn-secondary is-active" id="btnGeoView">Geometrie</button>
<button class="btn btn-sm btn-secondary" id="btnMoonView">Mondansicht</button>
<button class="btn btn-sm btn-secondary is-active" id="btnToggleShadows" title="Schattenkegel ein-/ausblenden">Schatten</button>
</div>
<!-- Three.js canvas (geometry) -->
<canvas id="eclipseCanvas" class="eclipse-canvas"></canvas>
<!-- 2D Moon canvas -->
<canvas id="eclipseMoonCanvas" class="eclipse-canvas is-hidden"></canvas>
<!-- Scale notice -->
<div id="eclipseScaleNote" class="eclipse-scale-note">
Abstände nicht maßstabsgetreu<br>(Mond real ~60× weiter entfernt)
</div>
<!-- Info overlay -->
<div class="solarsystem-info-panel" id="eclipseInfoPanel">
<div class="eclipse-info-empty">Wähle eine Finsternis aus der Liste</div>
</div>
<!-- Timeline -->
<div class="eclipse-timeline" id="eclipseTimeline">
<div class="eclipse-timeline-bar" id="eclipseTimelineBar">
<div class="eclipse-tl-cursor" id="eclipseTimelineCursor"></div>
</div>
<div class="eclipse-timeline-controls">
<button class="btn btn-sm btn-secondary" id="eclipseBtnRewind" title="Zum Anfang">⏮</button>
<button class="btn btn-sm btn-primary" id="eclipseBtnPlay" title="Play/Pause">▶ Play</button>
<button class="btn btn-sm btn-secondary" id="eclipseBtnEnd" title="Zum Ende">⏭</button>
<span class="eclipse-time-display" id="eclipseTimeDisplay">– – : – – UTC</span>
<label class="eclipse-speed-label">
<span class="solarsystem-label">Geschw.</span>
<select id="eclipseSpeedSel" class="eclipse-speed-sel">
<option value="1">×1</option>
<option value="3">×3</option>
<option value="10" selected>×10</option>
<option value="30">×30</option>
<option value="60">×60</option>
</select>
</label>
</div>
</div>
</div><!-- /stage-wrap -->
</div><!-- /solarsystem-layout -->
</div><!-- /solarsystem-tool -->
<script type="module">
const USER_LAT = <?= $userLat !== null ? $userLat : 'null' ?>;
const USER_LON = <?= $userLon !== null ? $userLon : 'null' ?>;
// ════════════════════════════════════════════════════════════
// Constants
// ════════════════════════════════════════════════════════════
const AE = window.Astronomy;
// Visual scale: 1 vis-unit = 1 Earth radius
const R_E = 1.0; // Earth visual radius
const R_M = 0.2725; // Moon visual radius (0.2725 R_E)
const D_M = 7.0; // Compressed Moon orbit (real: 60.27 R_E)
// Shadow cone geometry derived from real Solar-system geometry
// Umbra apex: D_APEX = R_E * D_ES / (R_SUN - R_E) → compressed
// R_SUN/R_E ≈ 109.08, D_ES/R_E ≈ 23481
// D_APEX_real = 23481 / (109.08 - 1) = 217.3 R_E
const D_APEX_VIS = 217.3 * (D_M / 60.27); // ≈ 25.24 vis-units
// Penumbra apex on Sun-side:
// D_APEX_PEN_real = 23481 / (109.08 + 1) = 213.3 R_E
const D_APEX_PEN = 213.3 * (D_M / 60.27); // ≈ 24.77 vis-units
// Shadow radii at Moon's mean orbit (D_M from Earth)
const R_UMB = R_E * (1 - D_M / D_APEX_VIS); // ≈ 0.7227
const R_PEN = R_E * (D_APEX_PEN + D_M) / D_APEX_PEN; // ≈ 1.283
// Mean Moon speed in vis-units/min (real: ~1.023 km/s → 61.4 km/min → 0.009637 R_E/min)
const V_MOON_MEAN = 0.009637;
// ════════════════════════════════════════════════════════════
// DOM refs
// ════════════════════════════════════════════════════════════
const canvas3d = document.getElementById('eclipseCanvas');
const canvas2d = document.getElementById('eclipseMoonCanvas');
const stageWrap = document.getElementById('eclipseStageWrap');
const infoPanel = document.getElementById('eclipseInfoPanel');
const timelineBar = document.getElementById('eclipseTimelineBar');
const tlCursor = document.getElementById('eclipseTimelineCursor');
const tlDisplay = document.getElementById('eclipseTimeDisplay');
const eclipseListEl = document.getElementById('eclipseList');
const dataBlock = document.getElementById('eclipseDataBlock');
const contactTable = document.getElementById('eclipseContactTable');
const btnPlay = document.getElementById('eclipseBtnPlay');
const btnGeoView = document.getElementById('btnGeoView');
const btnMoonView = document.getElementById('btnMoonView');
const speedSel = document.getElementById('eclipseSpeedSel');
// ════════════════════════════════════════════════════════════
// State
// ════════════════════════════════════════════════════════════
let currentEclipse = null;
let eclipseListData = [];
let simTime = 0; // minutes from peak
let playing = false;
let playSpeed = 10; // minutes / real second
let lastRAF = 0;
let viewMode = 'geo';
// Eclipse geometry (computed per eclipse)
let eclB = 0; // impact parameter (vis-units)
let eclV = V_MOON_MEAN; // actual Moon speed (vis-units/min)
// ════════════════════════════════════════════════════════════
// Three.js state
// ════════════════════════════════════════════════════════════
let THREE, renderer, scene, camera, cameraMoon;
let earthMesh, earthTiltGroup, moonMesh, moonMatRef;
let umbraMesh, penumbraMesh, moonTrailLine;
let sunLight, ambientLight, bloodAmbient;
let camTheta = 0.25, camPhi = 1.15, camRadius = 18;
const camTarget = { x: D_M * 0.5, y: 0, z: 0 };
let isDragging = false, dragX = 0, dragY = 0;
// 2D Moon texture image
let moonImgEl = null;
// ════════════════════════════════════════════════════════════
// Init
// ════════════════════════════════════════════════════════════
async function init() {
// Pre-load Moon texture for 2D canvas
moonImgEl = new Image();
moonImgEl.src = 'images/moons/moon_2048.jpg';
THREE = await import('./js/three.module.js');
// Renderer
renderer = new THREE.WebGLRenderer({ canvas: canvas3d, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.1;
// Scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x000309);
// Camera
camera = new THREE.PerspectiveCamera(52, 1, 0.05, 500);
updateCamera();
// Close-up camera for Moon view (looking from Earth side toward the Moon)
cameraMoon = new THREE.PerspectiveCamera(22, 1, 0.01, 50);
// ── Lights ──────────────────────────────────────
ambientLight = new THREE.AmbientLight(0x0d1630, 1.0);
scene.add(ambientLight);
sunLight = new THREE.DirectionalLight(0xfff8e8, 2.0);
sunLight.position.set(-80, 1, 0.5);
scene.add(sunLight);
bloodAmbient = new THREE.AmbientLight(0xff1500, 0.0);
scene.add(bloodAmbient);
// Sun glow sphere (very far left)
const sunGeo = new THREE.SphereGeometry(2.0, 32, 32);
const sunMat = new THREE.MeshBasicMaterial({ color: 0xfff5aa });
const sunMesh = new THREE.Mesh(sunGeo, sunMat);
sunMesh.position.set(-84, 0, 0);
scene.add(sunMesh);
const haloGeo = new THREE.SphereGeometry(4.0, 32, 32);
const haloMat = new THREE.MeshBasicMaterial({ color: 0xffdd44, transparent: true, opacity: 0.07 });
const halo = new THREE.Mesh(haloGeo, haloMat);
halo.position.set(-84, 0, 0);
scene.add(halo);
// Starfield
buildStarfield();
// ── Earth ───────────────────────────────────────
const tl = new THREE.TextureLoader();
const earthTex = tl.load('images/planets/earth_day_1024.jpg');
earthMesh = new THREE.Mesh(
new THREE.SphereGeometry(R_E, 64, 64),
new THREE.MeshStandardMaterial({ map: earthTex, roughness: 0.88, metalness: 0.0 })
);
earthTiltGroup = new THREE.Group();
earthTiltGroup.add(earthMesh);
scene.add(earthTiltGroup);
// Roter Standort-Punkt des Users auf der Erde
if (USER_LAT !== null && USER_LON !== null) {
const phi = (90 - USER_LAT) * Math.PI / 180;
const theta = Math.PI + USER_LON * Math.PI / 180; // u=0 (Antimeridian) bei theta=0, Nullmeridian bei theta=π
const r = R_E + 0.012;
const px = -r * Math.sin(phi) * Math.cos(theta);
const py = r * Math.cos(phi);
const pz = r * Math.sin(phi) * Math.sin(theta);
const dot = new THREE.Mesh(
new THREE.SphereGeometry(0.03, 12, 12),
new THREE.MeshBasicMaterial({ color: 0xff2222 })
);
dot.position.set(px, py, pz);
earthMesh.add(dot); // Kind von earthMesh → dreht sich mit
}
// ── Shadow cones ────────────────────────────────
buildShadowCones();
// ── Moon ────────────────────────────────────────
const moonTex = tl.load('images/moons/moon_2048.jpg');
const moonBump = tl.load('images/moons/moon_displacement_16bit.jpg');
moonMatRef = new THREE.MeshStandardMaterial({
map: moonTex, bumpMap: moonBump, bumpScale: 0.06,
roughness: 0.92, metalness: 0.0,
emissive: new THREE.Color(0, 0, 0), emissiveIntensity: 0.0
});
// Per-fragment shadow: the shadow sweeps across the Moon disk as it enters the umbra/penumbra.
// We compute each fragment's distance from the Earth shadow axis (the +X axis) in world space.
moonMatRef.onBeforeCompile = (shader) => {
// Pass constants as uniforms so the shader can be updated if needed
shader.uniforms.uDApexVis = { value: D_APEX_VIS };
shader.uniforms.uDApexPen = { value: D_APEX_PEN };
moonMatRef.userData.shader = shader;
// Inject varying into vertex shader
shader.vertexShader = 'varying vec3 vMoonWorldPos;\n' + shader.vertexShader;
shader.vertexShader = shader.vertexShader.replace(
'#include <fog_vertex>',
`#include <fog_vertex>
vMoonWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;`
);
// Inject shadow darkening into fragment shader
shader.fragmentShader = 'varying vec3 vMoonWorldPos;\nuniform float uDApexVis;\nuniform float uDApexPen;\n'
+ shader.fragmentShader;
shader.fragmentShader = shader.fragmentShader.replace(
'#include <color_fragment>',
`#include <color_fragment>
if (vMoonWorldPos.x > 0.0) {
// Perpendicular distance from shadow axis (X axis)
float d = length(vMoonWorldPos.yz);
// Shadow cone radii at this x-position
float rUmb = max(0.0, ${R_E.toFixed(4)} * (1.0 - vMoonWorldPos.x / uDApexVis));
float rPen = ${R_E.toFixed(4)} * (uDApexPen + vMoonWorldPos.x) / uDApexPen;
float edge = ${(R_M * 0.8).toFixed(4)};
// 0 = fully inside, 1 = fully outside
float outUmb = smoothstep(rUmb - edge, rUmb + edge, d);
float outPen = smoothstep(rPen - edge, rPen + edge, d);
// Penumbra: partial shadow
float penFactor = (1.0 - outPen) * outUmb; // in penumbra but not umbra
// Umbra: deep shadow
float umbFactor = 1.0 - outUmb;
float darken = 1.0 - umbFactor * 0.93 - penFactor * 0.75;
diffuseColor.rgb *= darken;
// Blood-moon glow: deep red only in umbra
diffuseColor.rgb += vec3(0.10, 0.006, 0.0) * umbFactor * umbFactor;
}`
);
};
moonMesh = new THREE.Mesh(new THREE.SphereGeometry(R_M, 64, 64), moonMatRef);
moonMesh.position.set(D_M, 0, 0);
moonMesh.rotation.y = Math.PI; // near-side facing Earth, correct east/west orientation
scene.add(moonMesh);
// Moon trail line (placeholder, updated on eclipse select)
moonTrailLine = buildMoonTrail(0);
scene.add(moonTrailLine);
// ── Resize + events ─────────────────────────────
resizeAll();
window.addEventListener('resize', resizeAll);
setupInteraction();
buildYearSelectors();
// Start render loop
requestAnimationFrame(renderLoop);
// Auto-search
searchEclipses();
}
// ════════════════════════════════════════════════════════════
// Three.js helpers
// ════════════════════════════════════════════════════════════
function buildStarfield() {
const pos = [];
for (let i = 0; i < 3000; i++) {
const t = Math.random() * Math.PI * 2;
const p = Math.acos(2 * Math.random() - 1);
const r = 180 + Math.random() * 60;
pos.push(r * Math.sin(p) * Math.cos(t), r * Math.sin(p) * Math.sin(t), r * Math.cos(p));
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
scene.add(new THREE.Points(geo, new THREE.PointsMaterial({ color: 0xffffff, size: 0.35, sizeAttenuation: true })));
}
function buildShadowCones() {
if (umbraMesh) { scene.remove(umbraMesh); umbraMesh.geometry.dispose(); }
if (penumbraMesh){ scene.remove(penumbraMesh); penumbraMesh.geometry.dispose(); }
// Replace filled cones with thin boundary rings in the YZ plane at Moon's orbital distance.
// RingGeometry lies in the XY plane by default → rotate 90° around Y to face +X.
const t = 0.015; // ring thickness
const umbraRingGeo = new THREE.RingGeometry(R_UMB - t, R_UMB + t, 96);
umbraMesh = new THREE.Mesh(umbraRingGeo, new THREE.MeshBasicMaterial({
color: 0x1a4a8a, transparent: true, opacity: 0.55,
side: THREE.DoubleSide, depthWrite: false
}));
umbraMesh.rotation.y = Math.PI / 2;
umbraMesh.position.x = D_M;
scene.add(umbraMesh);
const penRingGeo = new THREE.RingGeometry(R_PEN - t, R_PEN + t, 96);
penumbraMesh = new THREE.Mesh(penRingGeo, new THREE.MeshBasicMaterial({
color: 0x0d2244, transparent: true, opacity: 0.35,
side: THREE.DoubleSide, depthWrite: false
}));
penumbraMesh.rotation.y = Math.PI / 2;
penumbraMesh.position.x = D_M;
scene.add(penumbraMesh);
}
function buildMoonTrail(bVal) {
if (moonTrailLine) { scene.remove(moonTrailLine); moonTrailLine.geometry.dispose(); }
const pts = [];
const omega = eclV / D_M;
for (let t = -180; t <= 180; t += 3) {
const th = omega * t;
pts.push(new THREE.Vector3(D_M * Math.cos(th), bVal, -D_M * Math.sin(th)));
}
const geo = new THREE.BufferGeometry().setFromPoints(pts);
const mat = new THREE.LineBasicMaterial({ color: 0x223355, transparent: true, opacity: 0.5 });
return new THREE.Line(geo, mat);
}
function updateCamera() {
const sp = Math.sin(camPhi), cp = Math.cos(camPhi);
const st = Math.sin(camTheta), ct = Math.cos(camTheta);
camera.position.set(
camTarget.x + camRadius * sp * st,
camTarget.y + camRadius * cp,
camTarget.z + camRadius * sp * ct
);
camera.lookAt(camTarget.x, camTarget.y, camTarget.z);
}
function resizeAll() {
const w = stageWrap.clientWidth;
const h = Math.max(400, Math.round(w * 0.58));
stageWrap.style.minHeight = h + 'px';
renderer.setSize(w, h);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
// ════════════════════════════════════════════════════════════
// Eclipse search (AstronomyEngine)
// ════════════════════════════════════════════════════════════
function buildYearSelectors() {
const yf = document.getElementById('eclipseYearFrom');
const yt = document.getElementById('eclipseYearTo');
const now = new Date().getFullYear();
for (let y = now - 5; y <= now + 20; y++) {
yf.add(new Option(y, y, y === now, y === now));
yt.add(new Option(y, y, y === now + 1, y === now + 1));
}
}
function searchEclipses() {
const yf = parseInt(document.getElementById('eclipseYearFrom').value);
const yt = parseInt(document.getElementById('eclipseYearTo').value);
eclipseListData = [];
let date = new Date(yf, 0, 1);
const limit = new Date(yt + 1, 0, 1);
while (date < limit) {
let ecl;
try { ecl = AE.SearchLunarEclipse(date); } catch(e) { break; }
if (ecl.peak.date >= limit) break;
eclipseListData.push(ecl);
date = new Date(ecl.peak.date.getTime() + 29 * 86400000);
}
renderEclipseList();
// Auto-select first upcoming (or first in list)
const now = new Date();
const idx = eclipseListData.findIndex(e => e.peak.date >= now);
selectEclipse(idx >= 0 ? idx : 0);
}
function renderEclipseList() {
if (!eclipseListData.length) {
eclipseListEl.innerHTML = '<p class="hint">Keine Finsternisse gefunden.</p>';
return;
}
eclipseListEl.innerHTML = eclipseListData.map((ecl, i) => {
const d = ecl.peak.date;
const ds = d.toLocaleDateString('de-DE', { year: 'numeric', month: 'short', day: 'numeric' });
const tc = { penumbral: 'eclipse-type-pen', partial: 'eclipse-type-par', total: 'eclipse-type-tot' }[ecl.kind] || '';
const tl = { penumbral: 'Halbschatten', partial: 'Partial', total: 'Total' }[ecl.kind] || ecl.kind;
return `<div class="eclipse-list-item" data-i="${i}">
<span class="eclipse-list-date">${ds}</span>
<span class="eclipse-list-type ${tc}">${tl}</span>
</div>`;
}).join('');
eclipseListEl.querySelectorAll('.eclipse-list-item').forEach(el =>
el.addEventListener('click', () => selectEclipse(+el.dataset.i))
);
}
// ════════════════════════════════════════════════════════════
// Eclipse selection & geometry
// ════════════════════════════════════════════════════════════
function selectEclipse(idx) {
if (!eclipseListData[idx]) return;
currentEclipse = eclipseListData[idx];
// Highlight list item
eclipseListEl.querySelectorAll('.eclipse-list-item').forEach((el, i) =>
el.classList.toggle('is-selected', i === idx)
);
// Scroll into view
const sel = eclipseListEl.querySelector('.is-selected');
if (sel) sel.scrollIntoView({ block: 'nearest' });
computeEclipseGeometry();
orientEarthAxis(currentEclipse.peak.date);
simTime = timeRange().min;
updateSim();
renderInfo();
renderContactTable();
renderTimeline();
// Rebuild Moon trail
if (moonTrailLine) scene.remove(moonTrailLine);
moonTrailLine = buildMoonTrail(eclB);
scene.add(moonTrailLine);
dataBlock.classList.remove('is-hidden');
playing = false;
btnPlay.textContent = '▶ Play';
lastRAF = performance.now();
}
// Ekliptikale Länge der Sonne (Näherung, Grad) für ein gegebenes Datum
function solarLongitude(date) {
const J2000 = 2451545.0;
const jd = date.getTime() / 86400000 + 2440587.5;
const n = jd - J2000;
const L = (280.46 + 0.9856474 * n) % 360;
const g = ((357.528 + 0.9856003 * n) % 360) * Math.PI / 180;
return ((L + 1.915 * Math.sin(g) + 0.020 * Math.sin(2 * g)) % 360 + 360) % 360;
}
// Erdachse korrekt ausrichten: Neigung 23.44° zeigt zum Himmelspol
// abhängig von der Stellung der Erde in ihrer Sonnenbahn
function orientEarthAxis(date) {
if (!earthTiltGroup) return;
const lambda = solarLongitude(date) * Math.PI / 180; // ekliptikale Länge der Sonne
const eps = 23.44 * Math.PI / 180; // Schiefe der Ekliptik
// Nordpol-Richtung in Szenenkoordinaten (X = Erde→Mond, Y = ekliptischer Nordpol)
const px = -Math.sin(eps) * Math.sin(lambda);
const py = Math.cos(eps);
const pz = Math.sin(eps) * Math.cos(lambda);
const pole = new THREE.Vector3(px, py, pz).normalize();
earthTiltGroup.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), pole);
// Spin-Startoffset: UTC-Stunde des Finsternismaximums → Längengrad auf Tagseite
const utcHours = date.getUTCHours() + date.getUTCMinutes() / 60;
earthMesh.rotation.y = (utcHours / 24) * Math.PI * 2;
}
function computeEclipseGeometry() {
const ec = currentEclipse;
if (ec.sd_partial > 0) {
// Use two contact semi-durations to find actual Moon speed and impact param
const rP = R_PEN + R_M;
const rU = R_UMB + R_M;
const sdP = ec.sd_penum, sdU = ec.sd_partial;
const v2 = (rP * rP - rU * rU) / (sdP * sdP - sdU * sdU);
eclV = Math.sqrt(Math.max(v2, V_MOON_MEAN * V_MOON_MEAN * 0.7));
eclB = Math.sqrt(Math.max(0, rP * rP - eclV * eclV * sdP * sdP));
} else {
eclV = V_MOON_MEAN;
const rP = R_PEN + R_M;
eclB = Math.sqrt(Math.max(0, rP * rP - eclV * eclV * ec.sd_penum * ec.sd_penum));
}
}
function moonPos(t) {
// Moon orbits Earth in the XZ plane (equatorial plane; Three.js Y = pole axis).
// eclB is the perpendicular offset in Y (ecliptic latitude).
// At t=0 (peak) the Moon is at z=0, x=D_M — closest alignment with shadow axis.
const theta = (eclV / D_M) * t; // angle swept in XZ plane
const xMoon = D_M * Math.cos(theta);
const zMoon = -D_M * Math.sin(theta);
const yMoon = eclB; // constant ecliptic-latitude offset
// Perpendicular distance from the shadow axis (X axis) = sqrt(y² + z²)
const dist = Math.sqrt(yMoon * yMoon + zMoon * zMoon);
return { x: xMoon, y: yMoon, z: zMoon, dist };
}
function shadowDepth(mp) {
// Returns { inUmbra: 0-1, inPenumbra: 0-1, totalDepth: 0-1 }
const dist = mp.dist; // distance from shadow-cone centre at (D_M, 0)
const inPen = Math.max(0, Math.min(1, (R_PEN + R_M - dist) / (2 * R_M)));
const inUmb = Math.max(0, Math.min(1, (R_UMB + R_M - dist) / (2 * R_M)));
// Total: Moon fully inside umbra when dist < R_UMB - R_M
const totD = Math.max(0, Math.min(1, (R_UMB - R_M - dist) / (2 * R_M) * 4));
return { inPenumbra: inPen, inUmbra: inUmb, totalDepth: Math.min(1, totD) };
}
function timeRange() {
if (!currentEclipse) return { min: -10, max: 10 };
const sd = currentEclipse.sd_penum;
return { min: -sd - 8, max: sd + 8 };
}
// ════════════════════════════════════════════════════════════
// Simulation update
// ════════════════════════════════════════════════════════════
function updateSim() {
if (!currentEclipse) return;
const mp = moonPos(simTime);
const sdw = shadowDepth(mp);
// ── Three.js scene ──────────────────────────────
if (moonMesh) {
moonMesh.position.set(mp.x, mp.y, mp.z);
// Shadow is handled per-fragment in the Moon's onBeforeCompile shader.
// sunLight stays constant so Earth is unaffected.
}
// ── Timeline cursor ─────────────────────────────
const { min, max } = timeRange();
const frac = (simTime - min) / (max - min);
tlCursor.style.left = `${Math.max(0, Math.min(100, frac * 100)).toFixed(2)}%`;
// ── Time display ────────────────────────────────
const peakMs = currentEclipse.peak.date.getTime();
const cur = new Date(peakMs + simTime * 60000);
tlDisplay.textContent = cur.toISOString().slice(11, 16) + ' UTC';
// ── 2D moon view ────────────────────────────────
if (viewMode === 'moon') drawMoonView(mp, sdw);
// ── Info overlay phase ──────────────────────────
updateInfoPhase(sdw);
}
// ════════════════════════════════════════════════════════════
// Render loop
// ════════════════════════════════════════════════════════════
function renderLoop(now) {
requestAnimationFrame(renderLoop);
if (playing && currentEclipse) {
const dt = (now - lastRAF) / 1000;
lastRAF = now;
simTime += dt * playSpeed;
const { max } = timeRange();
if (simTime >= max) {
simTime = max;
playing = false;
btnPlay.textContent = '▶ Play';
}
updateSim();
}
if (earthMesh && currentEclipse) {
const utcH = currentEclipse.peak.date.getUTCHours() + currentEclipse.peak.date.getUTCMinutes() / 60;
earthMesh.rotation.y = ((utcH / 24) + (simTime / 1440)) * Math.PI * 2;
}
// In geo view the Moon spins slowly for aesthetics.
// In moon view we freeze rotation.y = Math.PI: correct texture orientation.
if (moonMesh && viewMode === 'geo') moonMesh.rotation.y += 0.0001;
if (viewMode === 'geo') {
renderer.render(scene, camera);
} else if (viewMode === 'moon' && moonMesh) {
const mp = moonMesh.position;
cameraMoon.up.set(0, 1, 0); // north (Y+) always at top
cameraMoon.position.set(mp.x - 2.8, mp.y, mp.z);
cameraMoon.lookAt(mp.x, mp.y, mp.z);
cameraMoon.aspect = camera.aspect;
cameraMoon.updateProjectionMatrix();
renderer.render(scene, cameraMoon);
}
}
// ════════════════════════════════════════════════════════════
// 2D Moon view
// ════════════════════════════════════════════════════════════
function drawMoonView(mp, sdw) {
if (!currentEclipse) return;
if (!mp) mp = moonPos(simTime);
if (!sdw) sdw = shadowDepth(mp);
const ctx = canvas2d.getContext('2d');
const W = canvas2d.width;
const H = canvas2d.height;
ctx.clearRect(0, 0, W, H);
// Background
ctx.fillStyle = '#000309';
ctx.fillRect(0, 0, W, H);
// Static stars
ctx.save();
for (let i = 0; i < 300; i++) {
const sx = ((i * 137.508 + 60) % (W - 4)) + 2;
const sy = ((i * 79.317 + 40) % (H - 4)) + 2;
const sz = 0.4 + (i % 5) * 0.18;
const sa = 0.2 + (i % 7) * 0.06;
ctx.beginPath(); ctx.arc(sx, sy, sz, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255,255,255,${sa.toFixed(2)})`;
ctx.fill();
}
ctx.restore();
const R = Math.min(W, H) * 0.37;
const cx = W / 2, cy = H / 2;
const sc = R / R_M; // pixels per vis-unit
// ── Moon disc ───────────────────────────────────
ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, R, 0, Math.PI * 2);
ctx.clip();
if (moonImgEl && moonImgEl.complete && moonImgEl.naturalWidth > 0) {
// Draw real texture – equirectangular, take centre strip
const iw = moonImgEl.naturalWidth, ih = moonImgEl.naturalHeight;
const sx = iw * 0.25, sy = ih * 0.25, sw = iw * 0.5, sh = ih * 0.5;
ctx.drawImage(moonImgEl, sx, sy, sw, sh, cx - R, cy - R, R * 2, R * 2);
} else {
// Fallback gradient
const g = ctx.createRadialGradient(cx - R * 0.2, cy - R * 0.2, R * 0.05, cx, cy, R);
g.addColorStop(0, '#d8d0c8'); g.addColorStop(0.6, '#b0a898'); g.addColorStop(1, '#7a7268');
ctx.fillStyle = g; ctx.fillRect(cx - R, cy - R, R * 2, R * 2);
}
// Limb darkening
const ld = ctx.createRadialGradient(cx, cy, R * 0.55, cx, cy, R);
ld.addColorStop(0, 'rgba(0,0,0,0)');
ld.addColorStop(1, 'rgba(0,0,0,0.48)');
ctx.fillStyle = ld; ctx.fillRect(cx - R, cy - R, R * 2, R * 2);
ctx.restore();
// ── Shadow overlay ──────────────────────────────
// Shadow center in canvas coordinates
// moonPos gives Moon center; shadow axis is at y=0, x=D_M always
// Shadow center relative to Moon center (in vis-units):
// shadow_dx = D_M - mp.x = -eclV * simTime (shadow doesn't move, Moon does)
// shadow_dy = 0 - mp.y = -eclB
const sdxVis = D_M - mp.x; // = -eclV * simTime
const sdyVis = 0 - mp.y; // = -eclB
const scx = cx + sdxVis * sc;
const scy = cy + sdyVis * sc; // canvas +Y is down
const Rpen = R_PEN * sc;
const Rumb = R_UMB * sc;
ctx.save();
ctx.beginPath(); ctx.arc(cx, cy, R, 0, Math.PI * 2); ctx.clip();
// Penumbra gradient
const pg = ctx.createRadialGradient(scx, scy, Math.max(0, Rumb * 0.95), scx, scy, Rpen * 1.05);
pg.addColorStop(0, 'rgba(0,0,0,0)');
pg.addColorStop(1, 'rgba(0, 0, 0, 0.95)');
ctx.fillStyle = pg; ctx.fillRect(0, 0, W, H);
// Umbra gradient – reddish deep shadow
if (Rumb > 0) {
const td = sdw.totalDepth;
const r0 = Math.round(15 + td * 50), r1 = Math.round(10 + td * 30);
const ug = ctx.createRadialGradient(scx, scy, 0, scx, scy, Rumb * 1.02);
ug.addColorStop(0, `rgba(${r0},${Math.round(td*6)},0,0.93)`);
ug.addColorStop(0.65,`rgba(${r1},2,0,0.88)`);
ug.addColorStop(1, 'rgba(0,0,0,0.62)');
ctx.fillStyle = ug; ctx.fillRect(0, 0, W, H);
}
ctx.restore();
// Moon rim
ctx.save();
ctx.beginPath(); ctx.arc(cx, cy, R, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(200,180,140,0.25)'; ctx.lineWidth = 1.5; ctx.stroke();
ctx.restore();
// Phase label
const pl = phaseName(simTime);
if (pl) {
ctx.fillStyle = 'rgba(240,217,144,0.75)';
ctx.font = `bold ${Math.round(R * 0.07)}px monospace`;
ctx.textAlign = 'center';
ctx.fillText(pl, cx, H - 24);
}
}
// ════════════════════════════════════════════════════════════
// UI helpers
// ════════════════════════════════════════════════════════════
function phaseName(t) {
const ec = currentEclipse;
if (!ec) return null;
if (ec.sd_total > 0 && Math.abs(t) <= ec.sd_total) return 'TOTALITÄT · BLUTMOND';
if (ec.sd_partial > 0 && Math.abs(t) <= ec.sd_partial) return 'PARTIELLE PHASE';
if (Math.abs(t) <= ec.sd_penum) return 'HALBSCHATTEN-PHASE';
return t < 0 ? 'VOR DER FINSTERNIS' : 'NACH DER FINSTERNIS';
}
function fmtUTC(peakDate, offsetMin) {
return new Date(peakDate.getTime() + offsetMin * 60000).toISOString().slice(11, 19) + ' UTC';
}
function renderInfo() {
const ec = currentEclipse;
const d = ec.peak.date;
const ds = d.toLocaleDateString('de-DE', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
const ts = d.toISOString().slice(11, 19) + ' UTC';
const tc = { penumbral: 'eclipse-type-pen', partial: 'eclipse-type-par', total: 'eclipse-type-tot' }[ec.kind] || '';
const tl = { penumbral: 'Halbschatten-Finsternis', partial: 'Partielle Mondfinsternis', total: 'Totale Mondfinsternis' }[ec.kind] || ec.kind;
const mag = ec.umbral_magnitude !== undefined
? ec.umbral_magnitude.toFixed(3)
: (ec.penumbral_magnitude !== undefined ? ec.penumbral_magnitude.toFixed(3) + ' (Hb)' : '—');
infoPanel.innerHTML = `
<div class="eclipse-info-type ${tc}">${tl}</div>
<div class="eclipse-info-date">${ds}</div>
<div class="eclipse-info-time">${ts}</div>
<table class="data-table eclipse-info-table">
<tr><td>Halbschatten</td><td>${Math.round(ec.sd_penum * 2)} Min.</td></tr>
${ec.sd_partial > 0 ? `<tr><td>Partielle Phase</td><td>${Math.round(ec.sd_partial * 2)} Min.</td></tr>` : ''}
${ec.sd_total > 0 ? `<tr><td>Totalität</td><td>${Math.round(ec.sd_total * 2)} Min.</td></tr>` : ''}
<tr><td>Magnitude</td><td>${mag}</td></tr>
</table>
<div class="eclipse-info-phase" id="eclipsePhaseLabel">—</div>
`;
}
function updateInfoPhase(sdw) {
const el = document.getElementById('eclipsePhaseLabel');
if (!el) return;
const name = phaseName(simTime) || '—';
const depth = sdw.inUmbra > 0.01
? ` · ${Math.round(sdw.inUmbra * 100)} % im Kernschatten`
: sdw.inPenumbra > 0.01
? ` · ${Math.round(sdw.inPenumbra * 100)} % im Halbschatten`
: '';
el.textContent = name + depth;
}
function renderContactTable() {
const ec = currentEclipse;
const pk = ec.peak.date;
const rows = [
['P1', -ec.sd_penum, 'Eintritt Halbschatten'],
...(ec.sd_partial > 0 ? [['U1', -ec.sd_partial, 'Eintritt Kernschatten']] : []),
...(ec.sd_total > 0 ? [['U2', -ec.sd_total, 'Beginn Totalität' ]] : []),
['MAX', 0, 'Größte Phase'],
...(ec.sd_total > 0 ? [['U3', ec.sd_total, 'Ende Totalität' ]] : []),
...(ec.sd_partial > 0 ? [['U4', ec.sd_partial, 'Austritt Kernschatten']] : []),
['P4', ec.sd_penum, 'Austritt Halbschatten'],
];
contactTable.innerHTML = rows.map(([lbl, off, desc]) =>
`<tr>
<td class="eclipse-contact-label">${lbl}</td>
<td class="eclipse-contact-time">${fmtUTC(pk, off)}</td>
<td class="eclipse-contact-desc">${desc}</td>
</tr>`
).join('');
}
function renderTimeline() {
const ec = currentEclipse;
const { min: tMin, max: tMax } = timeRange();
const span = tMax - tMin;
// Remove old markers / fills
timelineBar.querySelectorAll('.eclipse-tl-fill,.eclipse-tl-marker').forEach(el => el.remove());
function pct(t) { return ((t - tMin) / span * 100).toFixed(2) + '%'; }
// Penumbra fill
fillBar(pct(-ec.sd_penum), pct(ec.sd_penum), 'rgba(120,140,200,0.15)');
// Umbra fill
if (ec.sd_partial > 0) fillBar(pct(-ec.sd_partial), pct(ec.sd_partial), 'rgba(200,90,15,0.22)');
// Totality fill
if (ec.sd_total > 0) fillBar(pct(-ec.sd_total), pct(ec.sd_total), 'rgba(210,30,0,0.32)');
// Markers
const markers = [
{ t: -ec.sd_penum, lbl: 'P1', cls: 'pen' },
...(ec.sd_partial > 0 ? [{ t: -ec.sd_partial, lbl: 'U1', cls: 'umb' }] : []),
...(ec.sd_total > 0 ? [{ t: -ec.sd_total, lbl: 'U2', cls: 'tot' }] : []),
{ t: 0, lbl: 'MAX', cls: 'max' },
...(ec.sd_total > 0 ? [{ t: ec.sd_total, lbl: 'U3', cls: 'tot' }] : []),
...(ec.sd_partial > 0 ? [{ t: ec.sd_partial, lbl: 'U4', cls: 'umb' }] : []),
{ t: ec.sd_penum, lbl: 'P4', cls: 'pen' },
];
markers.forEach(m => {
const el = document.createElement('div');
el.className = `eclipse-tl-marker eclipse-tl-${m.cls}`;
el.style.left = pct(m.t);
el.innerHTML = `<span class="eclipse-tl-label">${m.lbl}</span>`;
el.addEventListener('click', () => { simTime = m.t; updateSim(); });
timelineBar.appendChild(el);
});
function fillBar(l, r, color) {
const f = document.createElement('div');
f.className = 'eclipse-tl-fill';
f.style.left = l; f.style.width = `calc(${r} - ${l})`;
f.style.background = color;
timelineBar.appendChild(f);
}
}
// ════════════════════════════════════════════════════════════
// Interaction
// ════════════════════════════════════════════════════════════
function setupInteraction() {
// Camera orbit
canvas3d.addEventListener('mousedown', e => { isDragging = true; dragX = e.clientX; dragY = e.clientY; });
window.addEventListener('mousemove', e => {
if (!isDragging) return;
camTheta -= (e.clientX - dragX) * 0.005;
camPhi = Math.max(0.12, Math.min(Math.PI - 0.12, camPhi + (e.clientY - dragY) * 0.005));
dragX = e.clientX; dragY = e.clientY;
updateCamera();
});
window.addEventListener('mouseup', () => isDragging = false);
canvas3d.addEventListener('wheel', e => {
e.preventDefault();
camRadius = Math.max(4, Math.min(55, camRadius + e.deltaY * 0.025));
updateCamera();
}, { passive: false });
// Touch orbit
let tx = 0, ty = 0;
canvas3d.addEventListener('touchstart', e => { tx = e.touches[0].clientX; ty = e.touches[0].clientY; }, { passive: true });
canvas3d.addEventListener('touchmove', e => {
e.preventDefault();
camTheta -= (e.touches[0].clientX - tx) * 0.005;
camPhi = Math.max(0.12, Math.min(Math.PI - 0.12, camPhi + (e.touches[0].clientY - ty) * 0.005));
tx = e.touches[0].clientX; ty = e.touches[0].clientY;
updateCamera();
}, { passive: false });
// Timeline click / drag
function seekTimeline(e) {
if (!currentEclipse) return;
const rect = timelineBar.getBoundingClientRect();
const frac = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
const { min, max } = timeRange();
simTime = min + (max - min) * frac;
updateSim();
}
let tlDragging = false;
timelineBar.addEventListener('mousedown', e => { tlDragging = true; seekTimeline(e); });
window.addEventListener('mousemove', e => { if (tlDragging) seekTimeline(e); });
window.addEventListener('mouseup', () => tlDragging = false);
// Play
btnPlay.addEventListener('click', () => {
playing = !playing;
btnPlay.textContent = playing ? '⏸ Pause' : '▶ Play';
if (playing) lastRAF = performance.now();
});
// Rewind / End
document.getElementById('eclipseBtnRewind').addEventListener('click', () => {
simTime = timeRange().min; updateSim();
});
document.getElementById('eclipseBtnEnd').addEventListener('click', () => {
simTime = timeRange().max; updateSim();
});
// Speed
speedSel.addEventListener('change', () => { playSpeed = parseInt(speedSel.value); });
// View toggle
btnGeoView.addEventListener('click', () => setView('geo'));
btnMoonView.addEventListener('click', () => setView('moon'));
document.getElementById('btnToggleShadows').addEventListener('click', function() {
const visible = umbraMesh && umbraMesh.visible;
if (umbraMesh) umbraMesh.visible = !visible;
if (penumbraMesh) penumbraMesh.visible = !visible;
if (moonTrailLine) moonTrailLine.visible = !visible;
this.classList.toggle('is-active', !visible);
});
// Search
document.getElementById('eclipseSearchBtn').addEventListener('click', searchEclipses);
}
function setView(mode) {
viewMode = mode;
btnGeoView.classList.toggle('is-active', mode === 'geo');
btnMoonView.classList.toggle('is-active', mode === 'moon');
// Both views use the 3D canvas – Moon view just switches to the close-up camera
canvas3d.classList.remove('is-hidden');
canvas2d.classList.add('is-hidden');
if (mode === 'moon' && moonMesh) moonMesh.rotation.y = Math.PI;
}
// ════════════════════════════════════════════════════════════
// Boot
// ════════════════════════════════════════════════════════════
init().catch(err => {
stageWrap.innerHTML = `<div class="eclipse-error">Fehler: ${err.message}</div>`;
console.error(err);
});
</script>
<?php require __DIR__ . '/footer.php'; ?>