Files
skyview.astronomiemuseum.de/public/geocron/plugin-terminator.js
T
2026-04-02 15:40:06 +02:00

227 lines
9.3 KiB
JavaScript
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.
/**
* 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 });
}());