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