/** * 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 }); }());