diff --git a/public/css/style.css b/public/css/style.css index 690b5a1..114ff33 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -5423,3 +5423,58 @@ body.sternenhimmel-page .star-loading-note { font-size: 0.68rem; letter-spacing: 0.04em; } + +/* Geocron */ + +body.geocron-page main > .container { + max-width: min(1600px, calc(100% - 32px)); +} + +.geocron-card { + padding: 0; + overflow: hidden; +} + +.geocron-stage { + padding: 14px; + background: + radial-gradient(circle at top, rgba(74, 158, 255, 0.08), transparent 46%), + linear-gradient(180deg, rgba(5, 10, 20, 0.94) 0%, rgba(8, 14, 28, 0.98) 100%); +} + +.geocron-earth-wrap { + position: relative; + width: 100%; + aspect-ratio: 2 / 1; + min-height: 320px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid rgba(201, 168, 76, 0.14); + border-radius: calc(var(--radius) + 4px); + background: #000; + overflow: hidden; +} + +.geocron-earth-viewport { + position: absolute; + top: 50%; + left: 50%; + max-width: 100%; + max-height: 100%; + transform: translate(-50%, -50%); +} + +.geocron-earth-viewport > canvas, +.geocron-earth-viewport > img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: block; +} + +.geocron-earth-viewport #earthDay { + z-index: 1; +} diff --git a/public/geocron/aurora.js b/public/geocron/aurora.js new file mode 100644 index 0000000..29f92b4 --- /dev/null +++ b/public/geocron/aurora.js @@ -0,0 +1,385 @@ +/** + * aurora.js – Polarlicht-Plugin für die Erd-Visualisierung + * =========================================================== + * Einbinden: + * (am Ende des , nach dem bestehenden + * (einfach am Ende des , nach dem bestehenden + + + + + + + + + + + + diff --git a/public/geocron/plugin-clouds.js b/public/geocron/plugin-clouds.js new file mode 100644 index 0000000..e53d75d --- /dev/null +++ b/public/geocron/plugin-clouds.js @@ -0,0 +1,97 @@ +/** + * plugin-clouds.js – Wolken-Layer + * ================================== + * Abhängigkeit: earth-core.js, plugin-terminator.js + * + * Erstellt: #cloudCanvas (z-index 3) + * + * Lauscht auf earth:terminator und zeichnet die Wolkentextur + * mit getrennter Deckkraft für Tag- und Nachtseite. + */ + +(function () { + + // ═══════════════════════════════════════════════════════════════ + // KONFIGURATION + // ═══════════════════════════════════════════════════════════════ + const CFG = { + + // Externe Wolkentextur (Alpha-PNG) + CLOUD_URL: "https://clouds.matteason.co.uk/images/2048x1024/clouds-alpha.png", + + // Deckkraft der Wolken (0.0 = unsichtbar | 1.0 = voll sichtbar) + CLOUD_ALPHA_DAY: 0.35, // Tagseite + CLOUD_ALPHA_NIGHT: 0.18, // Nachtseite + }; + + // ── Kurz-Referenz auf EarthCore-Helfer ──────────────────────── + const { toX, toY, createCanvas, syncCanvas } = EarthCore; + + // ── Canvas & Context ────────────────────────────────────────── + const cloudCanvas = createCanvas("cloudCanvas", 3); + const ctx = cloudCanvas.getContext("2d"); + + // ── Wolkenbild laden ────────────────────────────────────────── + const cloudImg = new Image(); + cloudImg.crossOrigin = "anonymous"; + cloudImg.src = CFG.CLOUD_URL; + + // Letzter bekannter Terminator-State (für Re-Renders bei Resize) + let _lastTerminator = null; + + // ═══════════════════════════════════════════════════════════════ + // ZEICHNEN + // ═══════════════════════════════════════════════════════════════ + + function clipToSide(pts, w, h, northIsNight, nightSide) { + ctx.beginPath(); + if (!nightSide) ctx.rect(0, 0, w, h); + ctx.moveTo(toX(pts[0].lon, w), toY(pts[0].lat, h)); + for (let i = 1; i < pts.length; i++) { + ctx.lineTo(toX(pts[i].lon, w), toY(pts[i].lat, h)); + } + if (northIsNight) { ctx.lineTo(w, 0); ctx.lineTo(0, 0); } + else { ctx.lineTo(w, h); ctx.lineTo(0, h); } + ctx.closePath(); + ctx.clip(nightSide ? "nonzero" : "evenodd"); + } + + function render({ pts, northIsNight, w, h }) { + if (!cloudImg.complete || !cloudImg.naturalWidth) return; + if (!w || !h) return; + + syncCanvas(cloudCanvas, w, h); + ctx.clearRect(0, 0, w, h); + + // Tagseite + ctx.save(); + clipToSide(pts, w, h, northIsNight, false); + ctx.globalAlpha = CFG.CLOUD_ALPHA_DAY; + ctx.drawImage(cloudImg, 0, 0, w, h); + ctx.restore(); + + // Nachtseite + ctx.save(); + clipToSide(pts, w, h, northIsNight, true); + ctx.globalAlpha = CFG.CLOUD_ALPHA_NIGHT; + ctx.drawImage(cloudImg, 0, 0, w, h); + ctx.restore(); + } + + // ── Events ──────────────────────────────────────────────────── + window.addEventListener("earth:terminator", e => { + _lastTerminator = e.detail; + render(e.detail); + }); + + window.addEventListener("earth:resize", e => { + if (_lastTerminator) { + render({ ..._lastTerminator, w: e.detail.w, h: e.detail.h }); + } + }); + + cloudImg.onload = () => { + if (_lastTerminator) render(_lastTerminator); + }; + +}()); diff --git a/public/geocron/plugin-markers.js b/public/geocron/plugin-markers.js new file mode 100644 index 0000000..45c3995 --- /dev/null +++ b/public/geocron/plugin-markers.js @@ -0,0 +1,567 @@ +/** + * plugin-markers.js – Ortsmarker auf der Weltkarte + * ==================================================== + * Abhängigkeit: earth-core.js + * + * Erstellt: #markersCanvas (z-index 8) + * + * Jeden Marker in der MARKERS-Liste eintragen. + * Unterstützt: Icon-PNG, Fallback-Punkt, Label, Klick-Popup. + */ + +(function () { + + // ═══════════════════════════════════════════════════════════════ + // MARKER-LISTE – hier eintragen + // ═══════════════════════════════════════════════════════════════ + const MARKERS = [ + + // ═══════════════════════════════════════════════════════════ + // EIGENE STANDORTE + // ═══════════════════════════════════════════════════════════ + + { + name: "Astronomiemuseum", + lat: 50.377222, + lon: 11.189722, + icon: "../images/logoAM.png", + iconW: 67, + iconH: 45, + showLabel: false, + popup: "Astronomiemuseum\nSternwarte Rodewisch", + }, + + { + name: "Antipode Astronomiemuseum", + lat: -50.377222, + lon: -168.810278, + color: "rgba(255, 100, 180, 0.95)", + radius: 5, + showLabel: true, + popup: "Antipode des Astronomiemuseums\nGenau gegenüber auf der Erde\n50.38°S / 168.81°W\nSüdpazifik – ca. 800 km südöstlich von Neuseeland", + }, + + // ═══════════════════════════════════════════════════════════ + // GEOGRAFISCHE BESONDERHEITEN + // ═══════════════════════════════════════════════════════════ + + { + name: "Null Island", + lat: 0, + lon: 0, + color: "rgba(255, 255, 100, 0.95)", + radius: 5, + showLabel: true, + popup: "Null Island (0°/0°)\nSchnittpunkt Äquator & Nullmeridian\nHier liegt eine echte GPS-Referenzboje", + }, + { + name: "Nullmeridian – Greenwich", + lat: 51.4769, + lon: -0.0005, + color: "rgba(100, 200, 255, 0.95)", + radius: 5, + showLabel: true, + popup: "Royal Observatory Greenwich\nDefiniert seit 1884 den Nullmeridian (0° Länge)\nLondon, England", + }, + { + name: "Pol der Unzugänglichkeit", + lat: -48.873, + lon: -123.393, + color: "rgba(200, 100, 255, 0.95)", + radius: 5, + showLabel: true, + popup: "Pol der Unzugänglichkeit (Pazifik)\nAm weitesten von jeder Küste entfernt\n2.688 km bis zur nächsten Landmasse", + }, + { + name: "Erdmittelpunkt der Landmasse", + lat: 39.0, + lon: 34.0, + color: "rgba(255, 160, 60, 0.95)", + radius: 5, + showLabel: true, + popup: "Erdmittelpunkt der Landmasse\nGeografisches Zentrum aller Kontinente\nTürkei", + }, + { + name: "Magnetischer Nordpol", + lat: 86.5, + lon: -150.0, + color: "rgba(100, 255, 200, 0.95)", + radius: 5, + showLabel: true, + popup: "Magnetischer Nordpol (ca. 2024)\nWandert jährlich ~50 km Richtung Sibirien\nPosition ändert sich kontinuierlich", + }, + { + name: "Magnetischer Südpol", + lat: -64.1, + lon: 136.0, + color: "rgba(100, 255, 200, 0.95)", + radius: 5, + showLabel: true, + popup: "Magnetischer Südpol (ca. 2024)\nDerzeit nahe der Küste der Antarktis\nPosition ändert sich kontinuierlich", + }, + + // ═══════════════════════════════════════════════════════════ + // HISTORISCHE OBSERVATORIEN + // ═══════════════════════════════════════════════════════════ + + { + name: "Sternwarte Paris", + lat: 48.8365, + lon: 2.3363, + color: "rgba(180, 220, 255, 0.95)", + radius: 5, + showLabel: true, + popup: "Observatoire de Paris\nDefinierte bis 1884 den Nullmeridian\nGegründet 1667 – älteste Staatssternwarte", + }, + { + name: "Uraniborg", + lat: 55.9, + lon: 12.7, + color: "rgba(180, 220, 255, 0.95)", + radius: 5, + showLabel: true, + popup: "Uraniborg – Tycho Brahes Observatorium\nVen, Dänemark (heute Schweden)\nErbaut 1576 – präziseste vorteleskopische Messungen", + }, + { + name: "Jantar Mantar Jaipur", + lat: 26.9245, + lon: 75.8236, + color: "rgba(255, 200, 100, 0.95)", + radius: 5, + showLabel: true, + popup: "Jantar Mantar – Jaipur, Indien\nGrößte steinerne Sonnenuhren der Welt\nErbaut 1724 – UNESCO Weltkulturerbe", + }, + { + name: "Göbekli Tepe", + lat: 37.2232, + lon: 38.9224, + color: "rgba(255, 200, 100, 0.95)", + radius: 5, + showLabel: true, + popup: "Göbekli Tepe – Türkei\nÄlteste bekannte astronomische Anlage\nCa. 10.000 v. Chr. – vor Stonehenge & den Pyramiden", + }, + + // ═══════════════════════════════════════════════════════════ + // MODERNE TELESKOPE & OBSERVATORIEN + // ═══════════════════════════════════════════════════════════ + + { + name: "VLT – Very Large Telescope", + lat: -24.6275, + lon: -70.4044, + color: "rgba(120, 200, 255, 0.95)", + radius: 6, + showLabel: true, + popup: "VLT – Very Large Telescope (ESO)\nCerro Paranal, Atacama-Wüste, Chile\n4 Teleskope à 8,2 m Spiegel\nEins der leistungsfähigsten optischen Observatorien der Welt", + }, + { + name: "Mauna Kea", + lat: 19.8228, + lon: -155.4683, + color: "rgba(120, 200, 255, 0.95)", + radius: 6, + showLabel: true, + popup: "Mauna Kea Observatory – Hawaii, USA\nGrößte Teleskopkonzentration der Welt\n4.205 m Höhe – ideale Beobachtungsbedingungen\nHeimat von Keck, Subaru, UKIRT u.v.m.", + }, + { + name: "Atacama (ALMA)", + lat: -23.0229, + lon: -67.7540, + color: "rgba(120, 200, 255, 0.95)", + radius: 5, + showLabel: true, + popup: "ALMA – Atacama Large Millimeter Array\nChile, 5.058 m Höhe\n66 Radioteleskop-Antennen\nBeobachtet die kühlsten Objekte des Universums", + }, + { + name: "Arecibo (Standort)", + lat: 18.3463, + lon: -66.7528, + color: "rgba(180, 180, 180, 0.85)", + radius: 5, + showLabel: true, + popup: "Arecibo Observatory – Puerto Rico\n305 m Radioteleskop (1963–2020)\nKollabiert im Dez. 2020\nWar 53 Jahre lang das größte Radioteleskop der Welt", + }, + { + name: "FAST Radioteleskop", + lat: 25.6531, + lon: 106.8566, + color: "rgba(120, 200, 255, 0.95)", + radius: 5, + showLabel: true, + popup: "FAST – Five-hundred-meter Aperture\nSpherical Telescope, Guizhou, China\n500 m Durchmesser – größtes Radioteleskop der Welt\nIn Betrieb seit 2016", + }, + + // ═══════════════════════════════════════════════════════════ + // METEORITEN & EINSCHLAGKRATER + // ═══════════════════════════════════════════════════════════ + + { + name: "Chicxulub-Krater", + lat: 21.4, + lon: -89.5, + color: "rgba(255, 80, 50, 0.95)", + radius: 8, + showLabel: true, + popup: "Chicxulub-Krater – Yucatán, Mexiko\n~180 km Durchmesser\nEinschlag vor 66 Mio. Jahren\nAuslöser des Massen­aussterbens der Dinosaurier\nGrößter bekannter Einschlag der Erdgeschichte", + }, + { + name: "Vredefort-Krater", + lat: -27.0, + lon: 27.5, + color: "rgba(255, 80, 50, 0.95)", + radius: 7, + showLabel: true, + popup: "Vredefort-Krater – Südafrika\n~300 km Durchmesser (ursprünglich)\nÄltester bekannter Einschlagkrater: vor 2,02 Mrd. Jahren\nUNESCO Weltkulturerbe", + }, + { + name: "Sudbury-Krater", + lat: 46.6, + lon: -81.2, + color: "rgba(255, 80, 50, 0.95)", + radius: 7, + showLabel: true, + popup: "Sudbury Basin – Ontario, Kanada\n~130 km Durchmesser\nEinschlag vor 1,85 Mrd. Jahren\nEiner der größten und ältesten Krater\nHeute bedeutende Nickel- und Kupfermine", + }, + { + name: "Popigai-Krater", + lat: 71.65, + lon: 111.0, + color: "rgba(255, 80, 50, 0.95)", + radius: 6, + showLabel: true, + popup: "Popigai-Krater – Sibirien, Russland\n~100 km Durchmesser\nEinschlag vor 35,7 Mio. Jahren\nEnthält Billionen Karat Impact-Diamanten\nViertgrößter bekannter Krater", + }, + { + name: "Manicouagan-Krater", + lat: 51.37, + lon: -68.7, + color: "rgba(255, 80, 50, 0.95)", + radius: 6, + showLabel: true, + popup: "Manicouagan-Krater – Québec, Kanada\n~100 km Durchmesser\nEinschlag vor 215 Mio. Jahren\nRing-See gut sichtbar auf Satellitenbildern\n'Auge Quebecs'", + }, + { + name: "Acraman-Krater", + lat: -32.0, + lon: 135.45, + color: "rgba(255, 80, 50, 0.95)", + radius: 6, + showLabel: true, + popup: "Acraman-Krater – Südaustralien\n~90 km Durchmesser\nEinschlag vor 580 Mio. Jahren\nHeute ein ausgetrockneter Salzsee", + }, + { + name: "Tunguska", + lat: 60.917, + lon: 101.95, + color: "rgba(255, 160, 50, 0.95)", + radius: 5, + showLabel: true, + popup: "Tunguska-Ereignis – Sibirien, Russland\n30. Juni 1908\nLuftexplosion eines ~50 m Asteroiden\nStärke ~1.000 Hiroshima-Bomben\n2.000 km² Wald niedergewalzt\nKein Krater – Objekt verglühte in der Atmosphäre", + }, + { + name: "Meteor Crater (Barringer)", + lat: 35.0272, + lon: -111.0225, + color: "rgba(255, 80, 50, 0.95)", + radius: 5, + showLabel: true, + popup: "Barringer Meteor Crater – Arizona, USA\n1,2 km Durchmesser, 170 m tief\nEinschlag vor ~50.000 Jahren\nBester erhaltener Einschlagkrater der Erde\nEisenmeteorit, ca. 50 m groß", + }, + { + name: "Hoba-Meteorit", + lat: -19.5936, + lon: 17.9347, + color: "rgba(200, 180, 140, 0.95)", + radius: 5, + showLabel: true, + popup: "Hoba-Meteorit – Namibia\nGrößter bekannter Meteorit der Erde\n60 Tonnen, 2,7 × 2,7 m\nEntdeckt 1920 – liegt noch am Fundort\nEisenmeteorit, vor ~80.000 Jahren gefallen", + }, + { + name: "Tscheljabinsk", + lat: 55.1644, + lon: 61.4168, + color: "rgba(255, 160, 50, 0.95)", + radius: 5, + showLabel: true, + popup: "Tscheljabinsk-Meteorit – Russland\n15. Februar 2013\n~20 m großer Asteroid\nLuftexplosion in 30 km Höhe\n~1.500 Verletzte durch Druckwelle\nHellster Meteor seit Tunguska", + }, + { + name: "Nördlingen (Ries-Krater)", + lat: 48.8566, + lon: 10.6167, + color: "rgba(255, 80, 50, 0.95)", + radius: 5, + showLabel: true, + popup: "Nördlinger Ries – Bayern, Deutschland\n~24 km Durchmesser\nEinschlag vor 14,6 Mio. Jahren\nStadtmauer Nördlingen besteht aus Suevit (Impaktgestein)\nKirche St. Georg enthält ~5.000 Karat Mikro-Diamanten", + }, + { + name: "Sikhote-Alin", + lat: 46.1, + lon: 134.7, + color: "rgba(200, 180, 140, 0.95)", + radius: 5, + showLabel: true, + popup: "Sikhote-Alin-Meteorit – Russland\n12. Februar 1947\nEisenmeteorit, ~23 Tonnen (gefundene Fragmente)\nZerbrach in der Atmosphäre in tausende Stücke\nHinterließ ~100 kleine Krater im Primorsky Krai\nGrößter beobachteter Eisenmeteorit-Einschlag", + }, + { + name: "Campo del Cielo", + lat: -27.5, + lon: -61.7, + color: "rgba(200, 180, 140, 0.95)", + radius: 5, + showLabel: true, + popup: "Campo del Cielo – Argentinien\nEinschlag vor ca. 4.000–5.000 Jahren\nGruppe von ~26 Kratern auf 60 km²\nBisher ~100 Tonnen Eisenmeteoriten gefunden\nGróncio: 30,8 Tonnen – zweitgrößter Meteorit der Erde", + }, + { + name: "Siljan-Krater", + lat: 61.0, + lon: 14.9, + color: "rgba(255, 80, 50, 0.95)", + radius: 5, + showLabel: true, + popup: "Siljan-Krater – Schweden\n~52 km Durchmesser\nEinschlag vor 376 Mio. Jahren\nGrößter Einschlagkrater Europas\nHeute malerische Seenlandschaft", + }, + // ═══════════════════════════════════════════════════════════ + + { + name: "Baikonur", + lat: 45.9200, + lon: 63.3420, + color: "rgba(255, 120, 120, 0.95)", + radius: 5, + showLabel: true, + popup: "Baikonur Kosmodrom – Kasachstan\nÄltester und größter Raumfahrtbahnhof\nErster Satellit (Sputnik 1957)\nErster Mensch im All (Gagarin 1961)", + }, + { + name: "Cape Canaveral", + lat: 28.3922, + lon: -80.6077, + color: "rgba(255, 120, 120, 0.95)", + radius: 5, + showLabel: true, + popup: "Kennedy Space Center – Florida, USA\nNASA-Hauptstartplatz seit 1958\nMond-Missionen Apollo, Space Shuttle\nHeute auch SpaceX Falcon & Starship", + }, + { + name: "Kourou (ESA)", + lat: 5.2390, + lon: -52.7684, + color: "rgba(255, 120, 120, 0.95)", + radius: 5, + showLabel: true, + popup: "Centre Spatial Guyanais – Kourou\nFranzösisch-Guyana, ESA-Startplatz\nIdeal: nur 5° vom Äquator → Erdrotation spart Treibstoff\nHeimstart von Ariane-Raketen", + }, + { + name: "Jiuquan (CNSA)", + lat: 40.9605, + lon: 100.2913, + color: "rgba(255, 120, 120, 0.95)", + radius: 5, + showLabel: true, + popup: "Jiuquan Satellite Launch Center\nGobi-Wüste, China\nErster chinesischer Taikonaut ins All (2003)\nStart der Shenzhou-Missionen", + }, + + // ═══════════════════════════════════════════════════════════ + // POLARLICHT-HOTSPOTS + // ═══════════════════════════════════════════════════════════ + + { + name: "Tromsø", + lat: 69.6496, + lon: 18.9560, + color: "rgba(80, 255, 160, 0.95)", + radius: 5, + showLabel: true, + popup: "Tromsø – Norwegen\n69°N – mitten im Aurora-Oval\nPolarlicht-Hauptstadt Europas\nBeste Sichtbarkeit: Oktober–März", + }, + + ]; + + // ═══════════════════════════════════════════════════════════════ + // KONFIGURATION – globale Darstellungsoptionen + // ═══════════════════════════════════════════════════════════════ + const CFG = { + + // ── Fallback-Punkt (wenn kein Icon angegeben) ────────────── + DOT_RADIUS_PX: 5, + DOT_COLOR: "rgba(255, 200, 80, 0.95)", + DOT_BORDER: "rgba(255, 255, 255, 0.6)", + DOT_BORDER_W: 1.2, + DOT_GLOW: "rgba(255, 200, 80, 0.6)", + DOT_GLOW_BLUR: 6, + + // ── Label ────────────────────────────────────────────────── + LABEL_FONT: "13px system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif", + LABEL_OFFSET_X: 10, + LABEL_OFFSET_Y: -10, + LABEL_TEXT: "rgba(255, 255, 255, 0.95)", + LABEL_BG: "rgba(0, 0, 0, 0.55)", + LABEL_PAD_X: 6, + LABEL_PAD_Y: 3, + LABEL_RADIUS: 5, + + // ── Popup ────────────────────────────────────────────────── + POPUP_BG: "rgba(10, 15, 30, 0.92)", + POPUP_BORDER: "rgba(180, 160, 255, 0.6)", + POPUP_TEXT: "#e8f0ff", + POPUP_FONT: "13px system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif", + }; + + // ═══════════════════════════════════════════════════════════════ + // INTERNER STATE + // ═══════════════════════════════════════════════════════════════ + const { toX, toY, createCanvas, syncCanvas, roundRectPath } = EarthCore; + + const markersCanvas = createCanvas("markersCanvas", 8, false); // pointerEvents: auto + const ctx = markersCanvas.getContext("2d"); + + let popup = null; // { x, y, text } oder null + + // ── Icons vorladen ──────────────────────────────────────────── + const iconCache = new Map(); // marker-index → HTMLImageElement | null + + MARKERS.forEach((m, i) => { + if (!m.icon) { iconCache.set(i, null); return; } + const img = new Image(); + img.onload = () => { iconCache.set(i, img); draw(EarthCore.size); }; + img.onerror = () => { iconCache.set(i, null); }; + img.src = m.icon; + }); + + // ═══════════════════════════════════════════════════════════════ + // ZEICHNEN + // ═══════════════════════════════════════════════════════════════ + + function drawMarker(m, i, w, h) { + const x = toX(m.lon, w); + const y = toY(m.lat, h); + const img = iconCache.get(i); + + if (img && img.complete && img.naturalWidth > 0) { + // ── Icon ───────────────────────────────────────────────── + const hw = (m.iconW || 32) / 2; + const hh = (m.iconH || 32) / 2; + ctx.drawImage(img, x - hw, y - hh, m.iconW || 32, m.iconH || 32); + // Wrap an Datumsgrenze + if (x - hw < 0) ctx.drawImage(img, x - hw + w, y - hh, m.iconW || 32, m.iconH || 32); + if (x + hw > w) ctx.drawImage(img, x - hw - w, y - hh, m.iconW || 32, m.iconH || 32); + } else { + // ── Fallback: farbiger Leuchtpunkt ──────────────────────── + const r = m.radius ?? CFG.DOT_RADIUS_PX; + const fc = m.color ?? CFG.DOT_COLOR; + ctx.save(); + ctx.beginPath(); + ctx.arc(x, y, r, 0, Math.PI * 2); + ctx.fillStyle = fc; + ctx.shadowColor = m.glow ?? CFG.DOT_GLOW; + ctx.shadowBlur = CFG.DOT_GLOW_BLUR; + ctx.fill(); + ctx.strokeStyle = CFG.DOT_BORDER; + ctx.lineWidth = CFG.DOT_BORDER_W; + ctx.stroke(); + ctx.restore(); + } + + // ── Label ───────────────────────────────────────────────── + if (m.showLabel && m.name) drawLabel(x, y, m.name); + } + + function drawLabel(x, y, text) { + ctx.save(); + ctx.font = CFG.LABEL_FONT; + ctx.textBaseline = "middle"; + ctx.textAlign = "left"; + const tx = x + CFG.LABEL_OFFSET_X; + const ty = y + CFG.LABEL_OFFSET_Y; + const boxW = ctx.measureText(text).width + CFG.LABEL_PAD_X * 2; + const boxH = 13 + CFG.LABEL_PAD_Y * 2; + ctx.fillStyle = CFG.LABEL_BG; + roundRectPath(ctx, tx, ty - boxH / 2, boxW, boxH, CFG.LABEL_RADIUS); + ctx.fill(); + ctx.fillStyle = CFG.LABEL_TEXT; + ctx.fillText(text, tx + CFG.LABEL_PAD_X, ty); + ctx.restore(); + } + + function drawPopup({ x, y, text }) { + const lines = text.split("\n"); + ctx.save(); + ctx.font = CFG.POPUP_FONT; + ctx.textBaseline = "top"; + const lh = 18, padX = 10, padY = 8; + const bw = Math.max(...lines.map(l => ctx.measureText(l).width)) + padX * 2; + const bh = lines.length * lh + padY * 2; + const w = markersCanvas.width, h = markersCanvas.height; + + let px = x + 12, py = y - bh / 2; + if (px + bw > w - 5) px = x - bw - 12; + if (py < 5) py = 5; + if (py + bh > h - 5) py = h - bh - 5; + + roundRectPath(ctx, px, py, bw, bh, 7); + ctx.fillStyle = CFG.POPUP_BG; + ctx.fill(); + ctx.strokeStyle = CFG.POPUP_BORDER; + ctx.lineWidth = 1; + ctx.stroke(); + + ctx.fillStyle = CFG.POPUP_TEXT; + lines.forEach((line, i) => { + if (i === 0) ctx.font = "bold " + CFG.POPUP_FONT; + else ctx.font = CFG.POPUP_FONT; + ctx.fillText(line, px + padX, py + padY + i * lh); + }); + ctx.restore(); + } + + function draw({ w, h }) { + if (!w || !h) return; + syncCanvas(markersCanvas, w, h); + ctx.clearRect(0, 0, w, h); + MARKERS.forEach((m, i) => drawMarker(m, i, w, h)); + if (popup) drawPopup(popup); + } + + // ═══════════════════════════════════════════════════════════════ + // INTERAKTION – Klick öffnet Popup + // ═══════════════════════════════════════════════════════════════ + + function hitTest(cx, cy) { + const w = markersCanvas.width, h = markersCanvas.height; + for (const m of MARKERS) { + if (!m.popup) continue; + const x = toX(m.lon, w); + const y = toY(m.lat, h); + const hw = (m.iconW ?? CFG.DOT_RADIUS_PX * 2) / 2 + 4; + const hh = (m.iconH ?? CFG.DOT_RADIUS_PX * 2) / 2 + 4; + if (Math.abs(cx - x) <= hw && Math.abs(cy - y) <= hh) return { m, x, y }; + } + return null; + } + + markersCanvas.addEventListener("click", e => { + const rect = markersCanvas.getBoundingClientRect(); + const scaleX = markersCanvas.width / rect.width; + const scaleY = markersCanvas.height / rect.height; + const mx = (e.clientX - rect.left) * scaleX; + const my = (e.clientY - rect.top) * scaleY; + const hit = hitTest(mx, my); + popup = hit ? { x: mx, y: my, text: hit.m.popup } : null; + draw(EarthCore.size); + }); + + markersCanvas.addEventListener("mousemove", e => { + const rect = markersCanvas.getBoundingClientRect(); + const scaleX = markersCanvas.width / rect.width; + const scaleY = markersCanvas.height / rect.height; + const mx = (e.clientX - rect.left) * scaleX; + const my = (e.clientY - rect.top) * scaleY; + markersCanvas.style.cursor = hitTest(mx, my) ? "pointer" : "default"; + }); + + // ═══════════════════════════════════════════════════════════════ + // EVENTS + // ═══════════════════════════════════════════════════════════════ + window.addEventListener("earth:slow", e => draw(e.detail)); + window.addEventListener("earth:resize", e => draw(e.detail)); + +}()); diff --git a/public/geocron/plugin-moon.js b/public/geocron/plugin-moon.js new file mode 100644 index 0000000..b8a65ba --- /dev/null +++ b/public/geocron/plugin-moon.js @@ -0,0 +1,230 @@ +/** + * plugin-moon.js – Mond-Marker mit Popup (Alter & Phase) + * ========================================================= + * Abhängigkeit: earth-core.js + * Erstellt: #moonCanvas (z-index 6) + */ + +(function () { + + // ═══════════════════════════════════════════════════════════════ + // KONFIGURATION + // ═══════════════════════════════════════════════════════════════ + const CFG = { + RADIUS_PX: 16, + ICON: "../images/moons/moon4geocron.png", + UPDATE_MS: 60_000, + OBS_LAT: 50.3667, // Sternwarte Sonneberg + OBS_LON: 11.1833, + }; + + const { toX, toY, createCanvas, syncCanvas, roundRectPath, toDeg } = EarthCore; + + const moonCanvas = createCanvas("moonCanvas", 6); + const ctx = moonCanvas.getContext("2d"); + + const moonImg = new Image(); + moonImg.src = CFG.ICON; + + let _moon = { lat: 0, lon: 0 }; + let _phase = 0; // 0–360° + let _popup = false; + let _rise = null; // Date | null + let _set = null; // Date | null + + // ═══════════════════════════════════════════════════════════════ + // MONDBERECHNUNG (Astronomy Engine) + // ═══════════════════════════════════════════════════════════════ + + function moonPosition(date) { + try { + const vec = Astronomy.GeoMoon(date); + const dist = Math.sqrt(vec.x * vec.x + vec.y * vec.y + vec.z * vec.z); + const dec = toDeg(Math.asin(vec.z / dist)); + const ra = toDeg(Math.atan2(vec.y, vec.x)); + const gmst = Astronomy.SiderealTime(date) * 15; // Stunden → Grad + const lon = ((ra - gmst + 180) % 360 + 360) % 360 - 180; + return { lat: dec, lon: lon }; + } catch (e) { + return { lat: 0, lon: 0 }; + } + } + + // ═══════════════════════════════════════════════════════════════ + // AUFGANG / UNTERGANG (Astronomy Engine) + // ═══════════════════════════════════════════════════════════════ + + let _obs = null; + + function findMoonRiseSet(date) { + try { + if (!_obs) _obs = new Astronomy.Observer(CFG.OBS_LAT, CFG.OBS_LON, 0); + const d0 = new Date(date); d0.setUTCHours(0, 0, 0, 0); + const r = Astronomy.SearchRiseSet(Astronomy.Body.Moon, _obs, +1, d0, 1); + const s = Astronomy.SearchRiseSet(Astronomy.Body.Moon, _obs, -1, d0, 1); + return { rise: r ? r.date : null, set: s ? s.date : null }; + } catch (e) { + return { rise: null, set: null }; + } + } + + function fmtTime(d) { + if (!d) return "–"; + return d.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }); + } + + /** Mondalter in Tagen (0–29.53) */ + function moonAge(phaseDeg) { + return phaseDeg / 360 * 29.530589; + } + + /** Deutsche Phasenbezeichnung */ + function phaseName(phaseDeg) { + if (phaseDeg < 2 || phaseDeg >= 358) return "Neumond"; + if (phaseDeg < 88) return "Zunehmende Sichel"; + if (phaseDeg < 92) return "Erstes Viertel"; + if (phaseDeg < 178) return "Zunehmender Mond"; + if (phaseDeg < 182) return "Vollmond"; + if (phaseDeg < 268) return "Abnehmender Mond"; + if (phaseDeg < 272) return "Letztes Viertel"; + return "Abnehmende Sichel"; + } + + // ═══════════════════════════════════════════════════════════════ + // ZEICHNEN + // ═══════════════════════════════════════════════════════════════ + + function drawMoon(cx, cy) { + const r = CFG.RADIUS_PX; + if (moonImg.complete && moonImg.naturalWidth > 0) { + ctx.drawImage(moonImg, cx - r, cy - r, r * 2, r * 2); + } else { + ctx.save(); + ctx.beginPath(); + ctx.arc(cx, cy, r, 0, Math.PI * 2); + ctx.fillStyle = "rgba(210, 210, 210, 0.95)"; + ctx.shadowColor = "rgba(200, 200, 200, 0.6)"; + ctx.shadowBlur = 8; + ctx.fill(); + ctx.restore(); + } + } + + function drawPopup(cx, cy, w, h) { + const age = moonAge(_phase); + const illum = (1 - Math.cos(_phase * Math.PI / 180)) / 2 * 100; + const lines = [ + phaseName(_phase), + "Mondalter: " + age.toFixed(1) + " Tage", + "Beleuchtung: " + illum.toFixed(0) + " %", + "Aufgang: " + fmtTime(_rise), + "Untergang: " + fmtTime(_set), + ]; + + ctx.save(); + ctx.font = "13px system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif"; + ctx.textBaseline = "top"; + const lh = 18, padX = 10, padY = 8; + const bw = Math.max(...lines.map(l => ctx.measureText(l).width)) + padX * 2; + const bh = lines.length * lh + padY * 2; + + let bx = cx + CFG.RADIUS_PX + 8; + let by = cy - bh / 2; + if (bx + bw > w - 5) bx = cx - bw - CFG.RADIUS_PX - 8; + if (by < 5) by = 5; + if (by + bh > h - 5) by = h - bh - 5; + + roundRectPath(ctx, bx, by, bw, bh, 7); + ctx.fillStyle = "rgba(10, 15, 30, 0.92)"; + ctx.fill(); + ctx.strokeStyle = "rgba(180, 200, 255, 0.6)"; + ctx.lineWidth = 1; + ctx.stroke(); + + lines.forEach((line, i) => { + ctx.fillStyle = i === 0 + ? "rgba(180, 210, 255, 0.95)" + : "rgba(220, 230, 255, 0.9)"; + ctx.font = i === 0 + ? "bold 13px system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif" + : "13px system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif"; + ctx.fillText(line, bx + padX, by + padY + i * lh); + }); + ctx.restore(); + } + + // ═══════════════════════════════════════════════════════════════ + // RENDER + // ═══════════════════════════════════════════════════════════════ + + function render({ w, h }) { + if (!w || !h) return; + syncCanvas(moonCanvas, w, h); + ctx.clearRect(0, 0, w, h); + const mx = toX(_moon.lon, w); + const my = toY(_moon.lat, h); + const edge = CFG.RADIUS_PX + 4; + + drawMoon(mx, my); + if (mx - edge < 0) drawMoon(mx + w, my); + if (mx + edge > w) drawMoon(mx - w, my); + + if (_popup) drawPopup(mx, my, w, h); + } + + // ═══════════════════════════════════════════════════════════════ + // KLICK-ERKENNUNG (via window, Canvas hat pointerEvents:none) + // ═══════════════════════════════════════════════════════════════ + + function isHit(mx, my) { + const { w, h } = EarthCore.size; + const cx = toX(_moon.lon, w); + const cy = toY(_moon.lat, h); + const r = CFG.RADIUS_PX + 6; + // Auch Wrap-Kopie prüfen + return Math.hypot(mx - cx, my - cy) <= r + || Math.hypot(mx - (cx + w), my - cy) <= r + || Math.hypot(mx - (cx - w), my - cy) <= r; + } + + window.addEventListener("click", e => { + const rect = moonCanvas.getBoundingClientRect(); + if (!rect.width) return; + const mx = (e.clientX - rect.left) * (moonCanvas.width / rect.width); + const my = (e.clientY - rect.top) * (moonCanvas.height / rect.height); + if (isHit(mx, my)) { + _popup = !_popup; + render(EarthCore.size); + } else if (_popup) { + _popup = false; + render(EarthCore.size); + } + }); + + window.addEventListener("mousemove", e => { + const rect = moonCanvas.getBoundingClientRect(); + if (!rect.width) return; + const mx = (e.clientX - rect.left) * (moonCanvas.width / rect.width); + const my = (e.clientY - rect.top) * (moonCanvas.height / rect.height); + if (isHit(mx, my)) document.body.style.cursor = "pointer"; + }); + + // ── Updates ─────────────────────────────────────────────────── + function update() { + const now = new Date(); + _moon = moonPosition(now); + _phase = Astronomy.MoonPhase(now); + const rs = findMoonRiseSet(now); + _rise = rs.rise; + _set = rs.set; + render(EarthCore.size); + } + + window.addEventListener("earth:slow", () => update()); + window.addEventListener("earth:resize", () => render(EarthCore.size)); + moonImg.onload = () => render(EarthCore.size); + + update(); + setInterval(update, CFG.UPDATE_MS); + +}()); diff --git a/public/geocron/plugin-satellites.js b/public/geocron/plugin-satellites.js new file mode 100644 index 0000000..922ea02 --- /dev/null +++ b/public/geocron/plugin-satellites.js @@ -0,0 +1,355 @@ +/** + * plugin-satellites.js – Satelliten & Orbits + * ============================================== + * Abhängigkeit: earth-core.js, satellite.js (unpkg CDN) + * + * Erstellt: #orbitCanvas (z-index 4) + * + * Ortsmarker (z.B. Museum) → plugin-markers.js + */ + +(function () { + + // ═══════════════════════════════════════════════════════════════ + // KONFIGURATION + // ═══════════════════════════════════════════════════════════════ + const CFG = { + + // ── TLE-Endpunkt ─────────────────────────────────────────── + TLE_ENDPOINT: "getTLE.php", // ?ids=25544,20580,… → JSON aus DB + + + // ── Satelliten-Icons ─────────────────────────────────────── + // Zuerst ../images/satpics/{id}.png versucht, dann Fallback + SAT_ICON_FALLBACK: "../images/satpics/satellit.png", + + // ── Marker-Größe ─────────────────────────────────────────── + SAT_MARKER_SIZE_PX: 36, + SAT_MARKER_MIN_PX: 18, + SAT_SCALE: 1.5, + ISS_SCALE: 3, // zusätzlicher Faktor nur für ISS (ID 25544) + + // ── Labels ───────────────────────────────────────────────── + SHOW_SAT_LABELS: true, + LABEL_FONT_PX: 14, + LABEL_FONT_FAMILY: "system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif", + LABEL_OFFSET_X_PX: 10, + LABEL_OFFSET_Y_PX: -10, + LABEL_TEXT_COLOR: "rgba(255, 255, 255, 0.95)", + LABEL_BG_COLOR: "rgba(0, 0, 0, 0.55)", + LABEL_PADDING_X_PX: 6, + LABEL_PADDING_Y_PX: 3, + LABEL_RADIUS_PX: 6, + + // ── Orbit-Vorschau ───────────────────────────────────────── + ORBIT_MINUTES: 90, // Vorschau-Zeitraum ab jetzt + ORBIT_STEP_SEC: 10, // Zeitschritt + + ORBIT_COLORS: [ + "rgba(120, 200, 255, 0.95)", + "rgba(255, 160, 120, 0.95)", + "rgba(160, 255, 160, 0.95)", + "rgba(220, 180, 255, 0.95)", + "rgba(255, 220, 120, 0.95)", + ], + + ORBIT_LINE_WIDTH_DIV: 1500, + ORBIT_SHADOW_BLUR: 0, + }; + + // ── Kurz-Referenz auf EarthCore-Helfer ──────────────────────── + const { toX, toY, createCanvas, syncCanvas, roundRectPath } = EarthCore; + + // ── Canvas & Context ────────────────────────────────────────── + const orbitCanvas = createCanvas("orbitCanvas", 10000); // pointerEvents: none → Klicks via window + const ctx = orbitCanvas.getContext("2d"); + + // ── Icons laden ─────────────────────────────────────────────── + const satIconMap = new Map(); + const satFallbackIcon = new Image(); + satFallbackIcon.src = CFG.SAT_ICON_FALLBACK; + + function loadSatIcon(id) { + return new Promise(resolve => { + const img = new Image(); + img.onload = () => { satIconMap.set(id, img); resolve(); }; + img.onerror = () => { satIconMap.set(id, satFallbackIcon); resolve(); }; + img.src = `../images/satpics/${id}.png`; + }); + } + + // ── TLE-State ───────────────────────────────────────────────── + let satrecs = []; + + // ── Popup-State ─────────────────────────────────────────────── + let popup = null; // { x, y, sat: { name, altKm, speedKms, lat, lon } } + let lastPositions = []; // [{ id, name, x, y, altKm, speedKms }] nach jedem Render + + // ═══════════════════════════════════════════════════════════════ + // TLE AUS DB LADEN & SATRECS AUFBAUEN + // ═══════════════════════════════════════════════════════════════ + + async function buildSatRecs() { + const url = CFG.TLE_ENDPOINT; + const res = await fetch(url, { cache: "no-store" }); + if (!res.ok) throw new Error("TLE-Endpunkt Fehler: " + res.status); + + const rows = await res.json(); + if (rows.error) throw new Error("TLE-DB-Fehler: " + rows.error); + + satrecs = rows.map(r => ({ + id: Number(r.norad_cat_id), + name: r.object_name ?? String(r.norad_cat_id), + isFavorite: Boolean(r.is_favorite), + satrec: satellite.twoline2satrec(r.tle_line1, r.tle_line2), + })); + + await Promise.all(satrecs.map(s => loadSatIcon(s.id))); + } + + // ═══════════════════════════════════════════════════════════════ + // SATELLITEN-POSITION + // ═══════════════════════════════════════════════════════════════ + + const wrapLon = lon => ((lon + 180) % 360 + 360) % 360 - 180; + + function currentLatLon(satrec, date) { + const pv = satellite.propagate(satrec, date); + if (!pv.position) return null; + const gmst = satellite.gstime(date); + const gd = satellite.eciToGeodetic(pv.position, gmst); + const v = pv.velocity; + const speedKms = v ? Math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z) : null; + return { + lat: satellite.degreesLat(gd.latitude), + lon: wrapLon(satellite.degreesLong(gd.longitude)), + altKm: gd.height, // km über Ellipsoid + speedKms, // km/s + }; + } + + function groundTrackFuture(satrec, startDate) { + const steps = Math.floor((CFG.ORBIT_MINUTES * 60) / CFG.ORBIT_STEP_SEC); + const pts = []; + for (let i = 0; i <= steps; i++) { + const d = new Date(startDate.getTime() + i * CFG.ORBIT_STEP_SEC * 1000); + const ll = currentLatLon(satrec, d); + if (ll) pts.push(ll); + } + return pts; + } + + // ═══════════════════════════════════════════════════════════════ + // ZEICHNEN + // ═══════════════════════════════════════════════════════════════ + + function drawOrbitLine(points, baseColor) { + if (points.length < 2) return; + const total = points.length - 1; + const match = baseColor.match(/[\d.]+/g); + const r = match[0], g = match[1], b = match[2]; + + for (let i = 0; i < total; i++) { + const p0 = points[i], p1 = points[i + 1]; + if (Math.abs(p1.lon - p0.lon) > 180) continue; + const t = i / total; + const alpha = (1 - t) * (1 - t); + ctx.beginPath(); + ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${alpha.toFixed(3)})`; + ctx.moveTo(toX(p0.lon, orbitCanvas.width), toY(p0.lat, orbitCanvas.height)); + ctx.lineTo(toX(p1.lon, orbitCanvas.width), toY(p1.lat, orbitCanvas.height)); + ctx.stroke(); + } + } + + function getMarkerSize(id) { + const base = Math.max(CFG.SAT_MARKER_MIN_PX, CFG.SAT_MARKER_SIZE_PX) * CFG.SAT_SCALE; + return id === 25544 ? base * CFG.ISS_SCALE : base; + } + + function drawMarker(id, lat, lon) { + const w = orbitCanvas.width, h = orbitCanvas.height; + const x = toX(lon, w), y = toY(lat, h); + const img = satIconMap.get(id) ?? satFallbackIcon; + const size = getMarkerSize(id); + const half = size / 2; + + if (img.complete && img.naturalWidth > 0) { + ctx.drawImage(img, x - half, y - half, size, size); + if (x - half < 0) ctx.drawImage(img, x - half + w, y - half, size, size); + if (x + half > w) ctx.drawImage(img, x - half - w, y - half, size, size); + } else { + ctx.save(); + ctx.beginPath(); + ctx.arc(x, y, Math.max(4, size / 6), 0, 2 * Math.PI); + ctx.fillStyle = "rgba(255, 255, 255, 0.9)"; + ctx.shadowColor = "rgba(255, 255, 255, 0.6)"; + ctx.shadowBlur = 6; + ctx.fill(); + ctx.restore(); + } + } + + function drawLabel(x, y, text) { + if (!CFG.SHOW_SAT_LABELS || !text) return; + ctx.save(); + ctx.font = `${CFG.LABEL_FONT_PX}px ${CFG.LABEL_FONT_FAMILY}`; + ctx.textBaseline = "middle"; + ctx.textAlign = "left"; + const tx = x + CFG.LABEL_OFFSET_X_PX; + const ty = y + CFG.LABEL_OFFSET_Y_PX; + const padX = CFG.LABEL_PADDING_X_PX; + const padY = CFG.LABEL_PADDING_Y_PX; + const boxW = ctx.measureText(text).width + padX * 2; + const boxH = CFG.LABEL_FONT_PX + padY * 2; + ctx.fillStyle = CFG.LABEL_BG_COLOR; + roundRectPath(ctx, tx, ty - boxH / 2, boxW, boxH, CFG.LABEL_RADIUS_PX); + ctx.fill(); + ctx.fillStyle = CFG.LABEL_TEXT_COLOR; + ctx.fillText(text, tx + padX, ty); + ctx.restore(); + } + + // ── Popup zeichnen ──────────────────────────────────────────── + function drawPopup(px, py, sat, w, h) { + const lines = [ + sat.name, + "Höhe: " + sat.altKm.toFixed(0) + " km", + "Geschw.: " + (sat.speedKms * 3600).toFixed(0) + " km/h", + ]; + ctx.save(); + ctx.font = "13px " + CFG.LABEL_FONT_FAMILY; + ctx.textBaseline = "top"; + const lh = 18, padX = 10, padY = 8; + const bw = Math.max(...lines.map(l => ctx.measureText(l).width)) + padX * 2; + const bh = lines.length * lh + padY * 2; + + let bx = px + 14, by = py - bh / 2; + if (bx + bw > w - 5) bx = px - bw - 14; + if (by < 5) by = 5; + if (by + bh > h - 5) by = h - bh - 5; + + roundRectPath(ctx, bx, by, bw, bh, 7); + ctx.fillStyle = "rgba(10, 15, 30, 0.92)"; + ctx.fill(); + ctx.strokeStyle = "rgba(120, 180, 255, 0.6)"; + ctx.lineWidth = 1; + ctx.stroke(); + + lines.forEach((line, i) => { + ctx.fillStyle = i === 0 ? "rgba(120, 200, 255, 0.95)" : "rgba(220, 230, 255, 0.9)"; + if (i === 0) ctx.font = "bold 13px " + CFG.LABEL_FONT_FAMILY; + else ctx.font = "13px " + CFG.LABEL_FONT_FAMILY; + ctx.fillText(line, bx + padX, by + padY + i * lh); + }); + ctx.restore(); + } + + function render({ w, h }) { + if (!w || !h) return; + syncCanvas(orbitCanvas, w, h); + ctx.clearRect(0, 0, w, h); + + const now = new Date(); + lastPositions = []; + + for (let i = 0; i < satrecs.length; i++) { + const s = satrecs[i]; + const color = CFG.ORBIT_COLORS[i % CFG.ORBIT_COLORS.length]; + const pts = groundTrackFuture(s.satrec, now); + + ctx.save(); + ctx.lineWidth = Math.max(1, w / CFG.ORBIT_LINE_WIDTH_DIV); + ctx.shadowBlur = CFG.ORBIT_SHADOW_BLUR; + drawOrbitLine(pts, color); + ctx.restore(); + + if (pts.length > 0) { + const p0 = pts[0]; + drawMarker(s.id, p0.lat, p0.lon); + const sx = toX(p0.lon, w), sy = toY(p0.lat, h); + drawLabel(sx, sy, s.name || String(s.id)); + lastPositions.push({ + id: s.id, name: s.name || String(s.id), + x: sx, y: sy, + altKm: p0.altKm ?? 0, + speedKms: p0.speedKms ?? 0, + }); + } + } + + // Popup über allem zeichnen + if (popup) { + drawPopup(popup.x, popup.y, popup.sat, w, h); + } + } + + // ── Klick & Hover via window (orbitCanvas hat pointerEvents:none) ── + function canvasCoords(e) { + const rect = orbitCanvas.getBoundingClientRect(); + if (!rect.width || !rect.height) return null; + const scaleX = orbitCanvas.width / rect.width; + const scaleY = orbitCanvas.height / rect.height; + return { + mx: (e.clientX - rect.left) * scaleX, + my: (e.clientY - rect.top) * scaleY, + }; + } + + function hitSatellite(mx, my) { + let best = null, bestDist = Infinity; + for (const p of lastPositions) { + const d = Math.hypot(mx - p.x, my - p.y); + const r = getMarkerSize(p.id) / 2 + 8; + if (d <= r && d < bestDist) { best = p; bestDist = d; } + } + return best; + } + + window.addEventListener("click", e => { + const c = canvasCoords(e); + if (!c) return; + const hit = hitSatellite(c.mx, c.my); + if (hit) { + popup = { x: c.mx, y: c.my, sat: hit }; + render(EarthCore.size); + } else if (popup) { + popup = null; + render(EarthCore.size); + } + }); + + window.addEventListener("mousemove", e => { + const c = canvasCoords(e); + if (!c) return; + const hit = hitSatellite(c.mx, c.my); + document.body.style.cursor = hit ? "pointer" : ""; + }); + + // ── Events ──────────────────────────────────────────────────── + window.addEventListener("earth:fast", e => render(e.detail)); + window.addEventListener("earth:resize", e => render(e.detail)); + + // ── Abhängigkeit laden & initialisieren ─────────────────────── + function loadScript(url) { + return new Promise((resolve, reject) => { + if (document.querySelector(`script[src="${url}"]`)) { resolve(); return; } + const s = document.createElement("script"); + s.src = url; + s.onload = resolve; + s.onerror = () => reject(new Error("Konnte nicht laden: " + url)); + document.head.appendChild(s); + }); + } + + (async function init() { + try { + await loadScript("https://unpkg.com/satellite.js@6.0.2/dist/satellite.min.js"); + await buildSatRecs(); + render(EarthCore.size); + } catch (e) { + console.warn("plugin-satellites:", e.message); + } + }()); + +}()); diff --git a/public/geocron/plugin-sun.js b/public/geocron/plugin-sun.js new file mode 100644 index 0000000..c084b12 --- /dev/null +++ b/public/geocron/plugin-sun.js @@ -0,0 +1,216 @@ +/** + * plugin-sun.js – Sonnen-Marker mit Popup (Aufgang & Untergang) + * ================================================================ + * Abhängigkeit: earth-core.js, astronomy.browser.min.js + * + * Erstellt: #sunCanvas (z-index 6) + */ + +(function () { + + // ═══════════════════════════════════════════════════════════════ + // KONFIGURATION + // ═══════════════════════════════════════════════════════════════ + const CFG = { + CORE_RADIUS_PX: 16, + RAY_INNER_PX: 19, + RAY_OUTER_PX: 29, + RAY_COUNT: 11, + OBS_LAT: 50.3667, // Sternwarte Sonneberg + OBS_LON: 11.1833, + UPDATE_MS: 60_000, + }; + + const { toX, toY, createCanvas, syncCanvas, roundRectPath, toDeg } = EarthCore; + + const sunCanvas = createCanvas("sunCanvas", 6); + const ctx = sunCanvas.getContext("2d"); + + let _sun = { lat: 0, lon: 0 }; + let _rise = null; + let _set = null; + let _popup = false; + let _obs = null; + + // ═══════════════════════════════════════════════════════════════ + // SONNENBERECHNUNG (Astronomy Engine) + // ═══════════════════════════════════════════════════════════════ + + function sunPosition(date) { + try { + const vec = Astronomy.GeoVector(Astronomy.Body.Sun, date, true); + const dist = Math.sqrt(vec.x * vec.x + vec.y * vec.y + vec.z * vec.z); + const dec = toDeg(Math.asin(vec.z / dist)); + const ra = toDeg(Math.atan2(vec.y, vec.x)); + const gmst = Astronomy.SiderealTime(date) * 15; // Stunden → Grad + const lon = ((ra - gmst + 180) % 360 + 360) % 360 - 180; + return { lat: dec, lon: lon }; + } catch (e) { + return { lat: 0, lon: 0 }; + } + } + + // ═══════════════════════════════════════════════════════════════ + // AUFGANG / UNTERGANG (Astronomy Engine) + // ═══════════════════════════════════════════════════════════════ + + function findSunRiseSet(date) { + try { + if (!_obs) _obs = new Astronomy.Observer(CFG.OBS_LAT, CFG.OBS_LON, 0); + const d0 = new Date(date); d0.setUTCHours(0, 0, 0, 0); + const r = Astronomy.SearchRiseSet(Astronomy.Body.Sun, _obs, +1, d0, 1); + const s = Astronomy.SearchRiseSet(Astronomy.Body.Sun, _obs, -1, d0, 1); + return { rise: r ? r.date : null, set: s ? s.date : null }; + } catch (e) { + return { rise: null, set: null }; + } + } + + function fmtTime(d) { + if (!d) return "–"; + return d.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }); + } + + // ═══════════════════════════════════════════════════════════════ + // ZEICHNEN + // ═══════════════════════════════════════════════════════════════ + + function drawSunAt(cx, cy) { + ctx.save(); + ctx.strokeStyle = "rgba(255, 200, 0, 0.9)"; + ctx.lineWidth = 1.5; + ctx.shadowColor = "rgba(255, 200, 0, 0.7)"; + ctx.shadowBlur = 6; + for (let i = 0; i < CFG.RAY_COUNT; i++) { + const a = (i / CFG.RAY_COUNT) * 2 * Math.PI; + ctx.beginPath(); + ctx.moveTo(cx + Math.cos(a) * CFG.RAY_INNER_PX, cy + Math.sin(a) * CFG.RAY_INNER_PX); + ctx.lineTo(cx + Math.cos(a) * CFG.RAY_OUTER_PX, cy + Math.sin(a) * CFG.RAY_OUTER_PX); + ctx.stroke(); + } + ctx.beginPath(); + ctx.arc(cx, cy, CFG.CORE_RADIUS_PX, 0, 2 * Math.PI); + ctx.fillStyle = "rgba(255, 230, 0, 1)"; + ctx.shadowColor = "rgba(255, 200, 0, 0.9)"; + ctx.shadowBlur = 12; + ctx.fill(); + ctx.strokeStyle = "rgba(255, 140, 0, 1)"; + ctx.lineWidth = 1.5; + ctx.stroke(); + ctx.restore(); + } + + function drawPopup(cx, cy, w, h) { + const lines = [ + "Aufgang: " + fmtTime(_rise), + "Untergang: " + fmtTime(_set), + ]; + + ctx.save(); + ctx.font = "13px system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif"; + ctx.textBaseline = "top"; + const lh = 18, padX = 10, padY = 8; + const bw = Math.max(...lines.map(l => ctx.measureText(l).width)) + padX * 2; + const bh = lines.length * lh + padY * 2; + + let bx = cx + CFG.RAY_OUTER_PX + 8; + let by = cy - bh / 2; + if (bx + bw > w - 5) bx = cx - bw - CFG.RAY_OUTER_PX - 8; + if (by < 5) by = 5; + if (by + bh > h - 5) by = h - bh - 5; + + roundRectPath(ctx, bx, by, bw, bh, 7); + ctx.fillStyle = "rgba(30, 20, 5, 0.92)"; + ctx.fill(); + ctx.strokeStyle = "rgba(255, 200, 80, 0.6)"; + ctx.lineWidth = 1; + ctx.stroke(); + + lines.forEach((line, i) => { + ctx.fillStyle = i === 0 + ? "rgba(255, 220, 100, 0.95)" + : "rgba(255, 240, 200, 0.9)"; + ctx.font = i === 0 + ? "bold 13px system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif" + : "13px system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif"; + ctx.fillText(line, bx + padX, by + padY + i * lh); + }); + ctx.restore(); + } + + // ═══════════════════════════════════════════════════════════════ + // RENDER + // ═══════════════════════════════════════════════════════════════ + + function render({ w, h }) { + if (!w || !h) return; + syncCanvas(sunCanvas, w, h); + ctx.clearRect(0, 0, w, h); + + const sx = toX(_sun.lon, w); + const sy = toY(_sun.lat, h); + const edge = CFG.RAY_OUTER_PX + 2; + + drawSunAt(sx, sy); + if (sx - edge < 0) drawSunAt(sx + w, sy); + if (sx + edge > w) drawSunAt(sx - w, sy); + + if (_popup) drawPopup(sx, sy, w, h); + } + + // ═══════════════════════════════════════════════════════════════ + // KLICK-ERKENNUNG + // ═══════════════════════════════════════════════════════════════ + + function isHit(mx, my) { + const { w, h } = EarthCore.size; + const cx = toX(_sun.lon, w); + const cy = toY(_sun.lat, h); + const r = CFG.RAY_OUTER_PX + 4; + return Math.hypot(mx - cx, my - cy) <= r + || Math.hypot(mx - (cx + w), my - cy) <= r + || Math.hypot(mx - (cx - w), my - cy) <= r; + } + + window.addEventListener("click", e => { + const rect = sunCanvas.getBoundingClientRect(); + if (!rect.width) return; + const mx = (e.clientX - rect.left) * (sunCanvas.width / rect.width); + const my = (e.clientY - rect.top) * (sunCanvas.height / rect.height); + if (isHit(mx, my)) { + _popup = !_popup; + render(EarthCore.size); + } else if (_popup) { + _popup = false; + render(EarthCore.size); + } + }); + + window.addEventListener("mousemove", e => { + const rect = sunCanvas.getBoundingClientRect(); + if (!rect.width) return; + const mx = (e.clientX - rect.left) * (sunCanvas.width / rect.width); + const my = (e.clientY - rect.top) * (sunCanvas.height / rect.height); + if (isHit(mx, my)) document.body.style.cursor = "pointer"; + }); + + // ═══════════════════════════════════════════════════════════════ + // UPDATE + // ═══════════════════════════════════════════════════════════════ + + function update() { + const now = new Date(); + _sun = sunPosition(now); + const rs = findSunRiseSet(now); + _rise = rs.rise; + _set = rs.set; + render(EarthCore.size); + } + + window.addEventListener("earth:slow", () => update()); + window.addEventListener("earth:resize", () => render(EarthCore.size)); + + update(); + setInterval(update, CFG.UPDATE_MS); + +}()); diff --git a/public/geocron/plugin-terminator.js b/public/geocron/plugin-terminator.js new file mode 100644 index 0000000..6d62ac6 --- /dev/null +++ b/public/geocron/plugin-terminator.js @@ -0,0 +1,226 @@ +/** + * plugin-terminator.js – Nachtmaske, Dämmerungszonen & Terminator-Linien + * ========================================================================= + * Abhängigkeit: earth-core.js + * + * Erstellt 2 Canvas-Layer: + * #nightCanvas (z-index 2) – per-Pixel Dämmerungsmaske über Nachtkarte + * #terminatorCanvas (z-index 5) – Terminator-Linien + * + * Feuert zusätzlich das Event earth:terminator mit + * { pts, northIsNight } + * damit plugin-clouds.js den Terminator zum Clipping nutzen kann. + */ + +(function () { + + // ═══════════════════════════════════════════════════════════════ + // KONFIGURATION + // ═══════════════════════════════════════════════════════════════ + const CFG = { + + // ── Nachtbild ───────────────────────────────────────────── + IMG_NIGHT_DIR: "../images/planets/earth/night/", + IMG_EXT: ".jpg", + + // ── Dämmerungsmaske ─────────────────────────────────────── + // Alpha-Werte pro Sonnenhöhenwinkel-Zone + NIGHT_ALPHA: { + 0: 80, // Bürgerliche Dämmerung (Sonne < 0°) + "-6": 150, // Nautische Dämmerung (Sonne < 6°) + "-12": 200, // Astronomische Dämmerung (Sonne < 12°) + "-18": 255, // Vollständige Nacht (Sonne < 18°) + }, + + // Auflösung der Dämmerungsmaske (höher = schärfer, mehr CPU) + MASK_W: 2440, + MASK_H: 1720, + + // ── Terminator-Linien ───────────────────────────────────── + TERMINATOR_LINES: [ + { elev: 0, color: "rgba(255, 210, 80, 0.3)", shadow: "rgba(255, 180, 30, 0.2)" }, + { elev: -6, color: "rgba(255, 140, 50, 0.1)", shadow: "rgba(255, 100, 10, 0.2)" }, + { elev: -12, color: "rgba(200, 80, 200, 0.1)", shadow: "rgba(160, 40, 160, 0.2)" }, + { elev: -18, color: "rgba( 80, 120, 255, 0.1)", shadow: "rgba( 40, 70, 200, 0.2)" }, + ], + + // Linienstärke: Canvas-Breite / Wert (Minimum 1 px) + LINE_WIDTH_DIV: 1000, + + // Leuchtschein-Stärke um Terminator-Linien + SHADOW_BLUR: 4, + + // Berechnungspunkte pro Kurve (höher = glatter) + STEPS: 5000, + }; + + // ── Kurz-Referenz auf EarthCore-Helfer ──────────────────────── + const { toRad, toDeg, toX, toY, createCanvas, syncCanvas } = EarthCore; + + // ── Canvas & Context ────────────────────────────────────────── + const nightCanvas = createCanvas("nightCanvas", 2); + const termCanvas = createCanvas("terminatorCanvas", 5); + const ctxNight = nightCanvas.getContext("2d"); + const ctxTerm = termCanvas.getContext("2d"); + + // ── Nachtbild laden ─────────────────────────────────────────── + const nightImg = new Image(); + nightImg.src = `${CFG.IMG_NIGHT_DIR}${new Date().getMonth() + 1}${CFG.IMG_EXT}`; + + // ═══════════════════════════════════════════════════════════════ + // ASTRONOMISCHE HILFSFUNKTIONEN + // ═══════════════════════════════════════════════════════════════ + + function isNight(lat, lon, sLat, sLon) { + return ( + Math.sin(toRad(lat)) * Math.sin(toRad(sLat)) + + Math.cos(toRad(lat)) * Math.cos(toRad(sLat)) * + Math.cos(toRad(lon - sLon)) + ) < 0; + } + + /** Terminator-Kurve als Parameterkurve (Kleinstkreis) */ + function terminatorCurve(sunLat, sunLon, elev = 0) { + const steps = CFG.STEPS; + const sinE = Math.sin(toRad(elev)); + const cosE = Math.sqrt(Math.max(0, 1 - sinE * sinE)); + + const sLat = toRad(sunLat), sLon = toRad(sunLon); + const Sx = Math.cos(sLat) * Math.cos(sLon); + const Sy = Math.cos(sLat) * Math.sin(sLon); + const Sz = Math.sin(sLat); + + let ux, uy, uz; + if (Math.abs(Sz) < 0.9) { + const len = Math.sqrt(Sx * Sx + Sy * Sy); + ux = -Sy / len; uy = Sx / len; uz = 0; + } else { + const len = Math.sqrt(Sy * Sy + Sz * Sz); + ux = 0; uy = -Sz / len; uz = Sy / len; + } + const vx = Sy * uz - Sz * uy; + const vy = Sz * ux - Sx * uz; + const vz = Sx * uy - Sy * ux; + + const pts = []; + for (let i = 0; i <= steps; i++) { + const t = (2 * Math.PI * i) / steps; + const ct = Math.cos(t), st = Math.sin(t); + const px = sinE * Sx + cosE * (ct * ux + st * vx); + const py = sinE * Sy + cosE * (ct * uy + st * vy); + const pz = sinE * Sz + cosE * (ct * uz + st * vz); + pts.push({ + lat: toDeg(Math.asin(Math.max(-1, Math.min(1, pz)))), + lon: toDeg(Math.atan2(py, px)), + }); + } + return pts; + } + + /** Einfacher lon-sortierter Terminator (für Clipping in Clouds-Plugin) */ + function terminatorPtsSimple(sunLat, sunLon, steps = 720) { + const sR = toRad(sunLat); + const pts = []; + for (let i = 0; i <= steps; i++) { + const lon = -180 + (360 / steps) * i; + const dLon = toRad(lon - sunLon); + const lat = Math.abs(sunLat) < 0.001 + ? (Math.cos(dLon) >= 0 ? 90 : -90) + : toDeg(Math.atan(-Math.cos(dLon) / Math.tan(sR))); + pts.push({ lon, lat }); + } + return pts; + } + + // ═══════════════════════════════════════════════════════════════ + // ZEICHNEN + // ═══════════════════════════════════════════════════════════════ + + /** Nachtmaske mit per-Pixel Dämmerungsübergängen */ + function drawNightMask(sunLat, sunLon, w, h) { + if (!nightImg.complete || !nightImg.naturalWidth) return; + + const MW = CFG.MASK_W, MH = CFG.MASK_H; + const offNight = document.createElement("canvas"); + offNight.width = MW; offNight.height = MH; + const octx = offNight.getContext("2d"); + octx.drawImage(nightImg, 0, 0, MW, MH); + + const imgData = octx.getImageData(0, 0, MW, MH); + const d = imgData.data; + const sinSLat = Math.sin(toRad(sunLat)); + const cosSLat = Math.cos(toRad(sunLat)); + + for (let py = 0; py < MH; py++) { + const lat = 90 - (py / MH) * 180; + const sinLat = Math.sin(toRad(lat)); + const cosLat = Math.cos(toRad(lat)); + for (let px = 0; px < MW; px++) { + const lon = (px / MW) * 360 - 180; + const sinAlt = sinLat * sinSLat + cosLat * cosSLat * Math.cos(toRad(lon - sunLon)); + const alt = toDeg(Math.asin(Math.max(-1, Math.min(1, sinAlt)))); + + let a = 0; + if (alt < 0) a = CFG.NIGHT_ALPHA[0]; + if (alt < -6) a = CFG.NIGHT_ALPHA["-6"]; + if (alt < -12) a = CFG.NIGHT_ALPHA["-12"]; + if (alt < -18) a = CFG.NIGHT_ALPHA["-18"]; + + d[(py * MW + px) * 4 + 3] = a; + } + } + + octx.putImageData(imgData, 0, 0); + ctxNight.clearRect(0, 0, w, h); + ctxNight.drawImage(offNight, 0, 0, w, h); + } + + /** Terminator-Kurve zeichnen (Stift bei Meridian-Wrap heben) */ + function strokeCurve(ctx, pts, w, h) { + ctx.beginPath(); + let first = true, prevLon = null; + for (const p of pts) { + if (prevLon !== null && Math.abs(p.lon - prevLon) > 180) first = true; + const x = toX(p.lon, w), y = toY(p.lat, h); + first ? ctx.moveTo(x, y) : ctx.lineTo(x, y); + first = false; prevLon = p.lon; + } + ctx.stroke(); + } + + // ── Haupt-Render ────────────────────────────────────────────── + function render({ sun, w, h }) { + if (!w || !h) return; + + // Größe synchronisieren + syncCanvas(nightCanvas, w, h); + syncCanvas(termCanvas, w, h); + + // Nachtmaske + drawNightMask(sun.lat, sun.lon, w, h); + + // Terminator-Linien + ctxTerm.clearRect(0, 0, w, h); + for (const line of CFG.TERMINATOR_LINES) { + const curve = terminatorCurve(sun.lat, sun.lon, line.elev); + ctxTerm.strokeStyle = line.color; + ctxTerm.lineWidth = Math.max(1, w / CFG.LINE_WIDTH_DIV); + ctxTerm.shadowColor = line.shadow; + ctxTerm.shadowBlur = CFG.SHADOW_BLUR; + strokeCurve(ctxTerm, curve, w, h); + } + + // Event für Clouds-Plugin feuern + const pts = terminatorPtsSimple(sun.lat, sun.lon); + const northIsNight = isNight(89, 0, sun.lat, sun.lon); + window.dispatchEvent(new CustomEvent("earth:terminator", { + detail: { pts, northIsNight, w, h } + })); + } + + // ── Events ──────────────────────────────────────────────────── + window.addEventListener("earth:slow", e => render(e.detail)); + window.addEventListener("earth:resize", e => render({ sun: EarthCore.sun, ...e.detail })); + nightImg.onload = () => render({ sun: EarthCore.sun, ...EarthCore.size }); + +}()); diff --git a/public/geocron/plugin-ui.js b/public/geocron/plugin-ui.js new file mode 100644 index 0000000..1525b60 --- /dev/null +++ b/public/geocron/plugin-ui.js @@ -0,0 +1,190 @@ +/** + * plugin-ui.js – Layer-Steuerung (ein-/ausblenden) + * ==================================================== + * Abhängigkeit: earth-core.js + */ + +(function () { + + const LAYERS = [ + { name: "Nacht & Dämmerung", canvasIds: ["nightCanvas", "terminatorCanvas"], defaultOn: true }, + { name: "Wolken", canvasIds: ["cloudCanvas"], defaultOn: false }, + { name: "Aurora", canvasIds: ["auroraCanvas"], defaultOn: false }, + { name: "Satelliten", canvasIds: ["orbitCanvas"], defaultOn: true }, + { name: "Sonne", canvasIds: ["sunCanvas"], defaultOn: true }, + { name: "Mond", canvasIds: ["moonCanvas"], defaultOn: true }, + { name: "Erdbeben", canvasIds: ["earthquakeCanvas"], defaultOn: false }, + { name: "Ortsmarker", canvasIds: ["markersCanvas"], defaultOn: false }, + ]; + + const CFG = { + TITLE: "Layer", + ACCENT: "#4a9eff", + ACCENT_OFF: "rgba(50, 60, 90, 0.9)", + }; + + // ── pendingVisibility und setVisibility VOR dem UI-Aufbau ────── + const pendingVisibility = new Map(); + + function setVisibility(layer, on) { + layer.canvasIds.forEach(id => { + const el = document.getElementById(id); + if (el) { + el.style.display = on ? "block" : "none"; + } else { + pendingVisibility.set(id, on); + } + }); + } + + const observer = new MutationObserver(mutations => { + for (const mut of mutations) { + for (const node of mut.addedNodes) { + if (node.id && pendingVisibility.has(node.id)) { + node.style.display = pendingVisibility.get(node.id) ? "block" : "none"; + pendingVisibility.delete(node.id); + } + } + } + if (pendingVisibility.size === 0) observer.disconnect(); + }); + observer.observe(document.getElementById("earth-wrapper") || document.body, { + childList: true, subtree: true, + }); + + // ── CSS ──────────────────────────────────────────────────────── + const style = document.createElement("style"); + style.textContent = ` + #earth-ui { + position: absolute; bottom: 14px; left: 14px; z-index: 9999; + background: rgba(6, 10, 22, 0.78); + border: 1px solid rgba(80, 110, 190, 0.28); + border-radius: 8px; padding: 7px 10px; + font: 11px/1.4 system-ui, -apple-system, sans-serif; + color: rgba(190, 210, 255, 0.88); + user-select: none; min-width: 140px; + backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); + box-shadow: 0 2px 16px rgba(0,0,0,0.5); + opacity: 0.75; transition: opacity 0.2s; + } + #earth-ui:hover { opacity: 1; } + + #earth-ui-header { + display: flex; align-items: center; justify-content: space-between; + margin-bottom: 6px; cursor: pointer; gap: 8px; + } + #earth-ui-title { + font-weight: 600; font-size: 10px; + letter-spacing: 0.1em; text-transform: uppercase; + color: rgba(140, 170, 255, 0.75); + } + #earth-ui-chevron { + font-size: 8px; color: rgba(100, 120, 180, 0.6); + transition: transform 0.2s; + } + #earth-ui-chevron.collapsed { transform: rotate(-90deg); } + + #earth-ui-rows { + display: flex; flex-direction: column; gap: 4px; + overflow: hidden; max-height: 300px; opacity: 1; + transition: max-height 0.2s ease, opacity 0.15s; + } + #earth-ui-rows.collapsed { max-height: 0; opacity: 0; } + + .earth-ui-row { + display: flex; align-items: center; + justify-content: space-between; gap: 8px; + } + .earth-ui-label { + font-size: 11px; color: rgba(190, 210, 255, 0.85); + transition: color 0.15s; flex: 1; cursor: pointer; + } + .earth-ui-label:hover { color: #fff; } + .earth-ui-label.off { color: rgba(100, 115, 155, 0.65); } + + /* Schmaler Toggle: 28×15px */ + .earth-ui-toggle { position: relative; width: 28px; height: 15px; flex-shrink: 0; } + .earth-ui-toggle input { opacity: 0; width: 0; height: 0; position: absolute; } + .earth-ui-slider { + position: absolute; inset: 0; border-radius: 15px; + background: ${CFG.ACCENT_OFF}; transition: background 0.18s; cursor: pointer; + } + .earth-ui-slider::before { + content: ""; position: absolute; + width: 10px; height: 10px; left: 2.5px; top: 50%; + transform: translateY(-50%); border-radius: 50%; + background: rgba(160, 175, 210, 0.8); + transition: left 0.18s, background 0.18s; + } + .earth-ui-toggle input:checked + .earth-ui-slider { background: ${CFG.ACCENT}; } + .earth-ui-toggle input:checked + .earth-ui-slider::before { + left: 15.5px; background: #fff; + } + `; + document.head.appendChild(style); + + // ── Panel ────────────────────────────────────────────────────── + const panel = document.createElement("div"); + panel.id = "earth-ui"; + + const header = document.createElement("div"); + header.id = "earth-ui-header"; + header.innerHTML = ` + ${CFG.TITLE} + ▼ + `; + panel.appendChild(header); + + const rows = document.createElement("div"); + rows.id = "earth-ui-rows"; + panel.appendChild(rows); + + let collapsed = false; + header.addEventListener("click", () => { + collapsed = !collapsed; + rows.classList.toggle("collapsed", collapsed); + document.getElementById("earth-ui-chevron").classList.toggle("collapsed", collapsed); + }); + + // ── Rows ─────────────────────────────────────────────────────── + LAYERS.forEach(layer => { + const row = document.createElement("div"); + row.className = "earth-ui-row"; + + const label = document.createElement("span"); + label.className = "earth-ui-label"; + label.textContent = layer.name; + + const toggleWrap = document.createElement("label"); + toggleWrap.className = "earth-ui-toggle"; + + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.checked = layer.defaultOn; + + const slider = document.createElement("span"); + slider.className = "earth-ui-slider"; + + toggleWrap.appendChild(checkbox); + toggleWrap.appendChild(slider); + row.appendChild(label); + row.appendChild(toggleWrap); + rows.appendChild(row); + + setVisibility(layer, layer.defaultOn); + if (!layer.defaultOn) label.classList.add("off"); + + checkbox.addEventListener("change", () => { + setVisibility(layer, checkbox.checked); + label.classList.toggle("off", !checkbox.checked); + }); + + label.addEventListener("click", () => { + checkbox.checked = !checkbox.checked; + checkbox.dispatchEvent(new Event("change")); + }); + }); + + (document.getElementById("earth-wrapper") || document.body).appendChild(panel); + +}()); diff --git a/public/header.php b/public/header.php index 5dc217e..4dc2f3a 100644 --- a/public/header.php +++ b/public/header.php @@ -59,6 +59,7 @@ $menuGroups = [ ['href' => 'jupitersystem.php', 'label' => 'Jupitersystem'], ['href' => 'tagbogen.php', 'label' => 'Tagbogen'], ['href' => 'sternenhimmel.php', 'label' => 'Sternenhimmel'], + ['href' => 'geocron/index.php', 'label' => 'Geocron'], ], ], [ diff --git a/public/images/moons/moon4geocron.png b/public/images/moons/moon4geocron.png new file mode 100644 index 0000000..b66bbe4 Binary files /dev/null and b/public/images/moons/moon4geocron.png differ diff --git a/public/images/planets/earth/day/1.jpg b/public/images/planets/earth/day/1.jpg new file mode 100644 index 0000000..5d3ba70 Binary files /dev/null and b/public/images/planets/earth/day/1.jpg differ diff --git a/public/images/planets/earth/day/10.jpg b/public/images/planets/earth/day/10.jpg new file mode 100644 index 0000000..33034eb Binary files /dev/null and b/public/images/planets/earth/day/10.jpg differ diff --git a/public/images/planets/earth/day/11.jpg b/public/images/planets/earth/day/11.jpg new file mode 100644 index 0000000..41f44aa Binary files /dev/null and b/public/images/planets/earth/day/11.jpg differ diff --git a/public/images/planets/earth/day/12.jpg b/public/images/planets/earth/day/12.jpg new file mode 100644 index 0000000..4ee37ea Binary files /dev/null and b/public/images/planets/earth/day/12.jpg differ diff --git a/public/images/planets/earth/day/2.jpg b/public/images/planets/earth/day/2.jpg new file mode 100644 index 0000000..8687637 Binary files /dev/null and b/public/images/planets/earth/day/2.jpg differ diff --git a/public/images/planets/earth/day/3.jpg b/public/images/planets/earth/day/3.jpg new file mode 100644 index 0000000..d229d92 Binary files /dev/null and b/public/images/planets/earth/day/3.jpg differ diff --git a/public/images/planets/earth/day/4.jpg b/public/images/planets/earth/day/4.jpg new file mode 100644 index 0000000..2c7ee7f Binary files /dev/null and b/public/images/planets/earth/day/4.jpg differ diff --git a/public/images/planets/earth/day/5.jpg b/public/images/planets/earth/day/5.jpg new file mode 100644 index 0000000..7ab2fd0 Binary files /dev/null and b/public/images/planets/earth/day/5.jpg differ diff --git a/public/images/planets/earth/day/6.jpg b/public/images/planets/earth/day/6.jpg new file mode 100644 index 0000000..ffe81e7 Binary files /dev/null and b/public/images/planets/earth/day/6.jpg differ diff --git a/public/images/planets/earth/day/7.jpg b/public/images/planets/earth/day/7.jpg new file mode 100644 index 0000000..4ac8836 Binary files /dev/null and b/public/images/planets/earth/day/7.jpg differ diff --git a/public/images/planets/earth/day/8.jpg b/public/images/planets/earth/day/8.jpg new file mode 100644 index 0000000..38bbd87 Binary files /dev/null and b/public/images/planets/earth/day/8.jpg differ diff --git a/public/images/planets/earth/day/9.jpg b/public/images/planets/earth/day/9.jpg new file mode 100644 index 0000000..847383c Binary files /dev/null and b/public/images/planets/earth/day/9.jpg differ diff --git a/public/images/planets/earth/night/1.jpg b/public/images/planets/earth/night/1.jpg new file mode 100644 index 0000000..c44b96c Binary files /dev/null and b/public/images/planets/earth/night/1.jpg differ diff --git a/public/images/planets/earth/night/10.jpg b/public/images/planets/earth/night/10.jpg new file mode 100644 index 0000000..d1ad377 Binary files /dev/null and b/public/images/planets/earth/night/10.jpg differ diff --git a/public/images/planets/earth/night/11.jpg b/public/images/planets/earth/night/11.jpg new file mode 100644 index 0000000..3f5f9df Binary files /dev/null and b/public/images/planets/earth/night/11.jpg differ diff --git a/public/images/planets/earth/night/12.jpg b/public/images/planets/earth/night/12.jpg new file mode 100644 index 0000000..0f13c0d Binary files /dev/null and b/public/images/planets/earth/night/12.jpg differ diff --git a/public/images/planets/earth/night/2.jpg b/public/images/planets/earth/night/2.jpg new file mode 100644 index 0000000..aec604c Binary files /dev/null and b/public/images/planets/earth/night/2.jpg differ diff --git a/public/images/planets/earth/night/3.jpg b/public/images/planets/earth/night/3.jpg new file mode 100644 index 0000000..e03f796 Binary files /dev/null and b/public/images/planets/earth/night/3.jpg differ diff --git a/public/images/planets/earth/night/4.jpg b/public/images/planets/earth/night/4.jpg new file mode 100644 index 0000000..175726f Binary files /dev/null and b/public/images/planets/earth/night/4.jpg differ diff --git a/public/images/planets/earth/night/5.jpg b/public/images/planets/earth/night/5.jpg new file mode 100644 index 0000000..fbcc5cf Binary files /dev/null and b/public/images/planets/earth/night/5.jpg differ diff --git a/public/images/planets/earth/night/6.jpg b/public/images/planets/earth/night/6.jpg new file mode 100644 index 0000000..4343586 Binary files /dev/null and b/public/images/planets/earth/night/6.jpg differ diff --git a/public/images/planets/earth/night/7.jpg b/public/images/planets/earth/night/7.jpg new file mode 100644 index 0000000..bd854bf Binary files /dev/null and b/public/images/planets/earth/night/7.jpg differ diff --git a/public/images/planets/earth/night/8.jpg b/public/images/planets/earth/night/8.jpg new file mode 100644 index 0000000..8df3b63 Binary files /dev/null and b/public/images/planets/earth/night/8.jpg differ diff --git a/public/images/planets/earth/night/9.jpg b/public/images/planets/earth/night/9.jpg new file mode 100644 index 0000000..dd92198 Binary files /dev/null and b/public/images/planets/earth/night/9.jpg differ