216 lines
8.3 KiB
JavaScript
216 lines
8.3 KiB
JavaScript
/**
|
||
* earth-core.js – Kern-Framework für die Erd-Visualisierung
|
||
* =============================================================
|
||
* Stellt Hilfsfunktionen, astronomische Berechnungen und ein
|
||
* Event-System bereit, auf das alle Plugins lauschen.
|
||
*
|
||
* Gefeuerte Events (auf window):
|
||
* earth:resize → { detail: { w, h } }
|
||
* earth:slow → { detail: { sun, w, h, now } } (~1×/min)
|
||
* earth:fast → { detail: { sun, w, h, now } } (~30 fps)
|
||
*
|
||
* Globales Objekt: window.EarthCore
|
||
*/
|
||
|
||
window.EarthCore = (function () {
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// KONFIGURATION
|
||
// ═══════════════════════════════════════════════════════════════
|
||
const CFG = {
|
||
|
||
// ── Bildpfade ──────────────────────────────────────────────
|
||
IMG_DAY_DIR: "../images/planets/earth/day/",
|
||
IMG_EXT: ".jpg",
|
||
|
||
// ── Aktualisierungsintervalle ──────────────────────────────
|
||
UPDATE_FAST_MS: 1000, // ~30 fps
|
||
UPDATE_SLOW_MS: 60_000, // 1× pro Minute
|
||
};
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// HILFSFUNKTIONEN (öffentlich über EarthCore)
|
||
// ═══════════════════════════════════════════════════════════════
|
||
const toRad = d => d * Math.PI / 180;
|
||
const toDeg = r => r * 180 / Math.PI;
|
||
const toX = (lon, w) => ((lon + 180) / 360) * w;
|
||
const toY = (lat, h) => ((90 - lat) / 180) * h;
|
||
|
||
/** Maximales 2:1-Rechteck im Earth-Container */
|
||
function calcSize() {
|
||
const wrapper = document.getElementById("earth-wrapper");
|
||
const W = wrapper?.clientWidth || window.innerWidth;
|
||
const H = wrapper?.clientHeight || window.innerHeight;
|
||
return W / H >= 2
|
||
? { w: H * 2, h: H }
|
||
: { w: W, h: W / 2 };
|
||
}
|
||
|
||
/**
|
||
* Erstellt einen Plugin-Canvas und hängt ihn in #earth-wrapper.
|
||
* @param {string} id – canvas.id
|
||
* @param {number} zIndex – CSS z-index
|
||
* @param {boolean} [noPointer] – pointerEvents: none (Standard: true)
|
||
*/
|
||
function createCanvas(id, zIndex, noPointer = true) {
|
||
const viewport = document.getElementById("earth-viewport");
|
||
if (!viewport) throw new Error("earth-core: #earth-viewport nicht gefunden");
|
||
const c = document.createElement("canvas");
|
||
c.id = id;
|
||
Object.assign(c.style, {
|
||
position: "absolute",
|
||
top: "0",
|
||
left: "0",
|
||
display: "block",
|
||
zIndex: String(zIndex),
|
||
pointerEvents: noPointer ? "none" : "auto",
|
||
});
|
||
viewport.appendChild(c);
|
||
return c;
|
||
}
|
||
|
||
/**
|
||
* Synchronisiert Größe eines Canvas auf aktuelle Erdgröße.
|
||
* Gibt true zurück wenn Größe gesetzt werden konnte.
|
||
*/
|
||
function syncCanvas(canvas, w, h) {
|
||
if (!w || !h) return false;
|
||
const rw = Math.round(w), rh = Math.round(h);
|
||
canvas.style.width = rw + "px";
|
||
canvas.style.height = rh + "px";
|
||
canvas.width = rw;
|
||
canvas.height = rh;
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Zeichnet eine abgerundete Rechteck-Path (kein fill/stroke).
|
||
* Universell für alle Plugins nutzbar.
|
||
*/
|
||
function roundRectPath(ctx, x, y, w, h, r) {
|
||
const rr = Math.min(r, w / 2, h / 2);
|
||
ctx.beginPath();
|
||
ctx.moveTo(x + rr, y);
|
||
ctx.arcTo(x + w, y, x + w, y + h, rr);
|
||
ctx.arcTo(x + w, y + h, x, y + h, rr);
|
||
ctx.arcTo(x, y + h, x, y, rr);
|
||
ctx.arcTo(x, y, x + w, y, rr);
|
||
ctx.closePath();
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// ASTRONOMISCHE BERECHNUNGEN
|
||
// ═══════════════════════════════════════════════════════════════
|
||
|
||
/** Subsolar-Punkt: wo steht die Sonne senkrecht? (Astronomy Engine) */
|
||
function subsolarPoint(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 };
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// ZUSTAND & EVENT-LOOP
|
||
// ═══════════════════════════════════════════════════════════════
|
||
let _sun = { lat: 0, lon: 0 };
|
||
let _size = { w: 0, h: 0 };
|
||
let _resizeRaf = 0;
|
||
let _resizeObserver = null;
|
||
|
||
function _buildDetail(now) {
|
||
return { sun: _sun, w: _size.w, h: _size.h, now };
|
||
}
|
||
|
||
function _fireSlow() {
|
||
const now = new Date();
|
||
_sun = subsolarPoint(now);
|
||
window.dispatchEvent(new CustomEvent("earth:slow", { detail: _buildDetail(now) }));
|
||
}
|
||
|
||
function _fireFast() {
|
||
window.dispatchEvent(new CustomEvent("earth:fast", { detail: _buildDetail(new Date()) }));
|
||
}
|
||
|
||
function _onResize() {
|
||
_size = calcSize();
|
||
const viewport = document.getElementById("earth-viewport");
|
||
if (viewport) {
|
||
viewport.style.width = Math.round(_size.w) + "px";
|
||
viewport.style.height = Math.round(_size.h) + "px";
|
||
}
|
||
window.dispatchEvent(new CustomEvent("earth:resize", { detail: { w: _size.w, h: _size.h } }));
|
||
_fireSlow();
|
||
_fireFast();
|
||
}
|
||
|
||
function _queueResize() {
|
||
if (_resizeRaf) cancelAnimationFrame(_resizeRaf);
|
||
_resizeRaf = requestAnimationFrame(() => {
|
||
_resizeRaf = 0;
|
||
_onResize();
|
||
});
|
||
}
|
||
|
||
window.addEventListener("resize", _queueResize);
|
||
|
||
// ── Start ──────────────────────────────────────────────────────
|
||
function _start() {
|
||
// Tagbild laden
|
||
const img = document.getElementById("earthDay");
|
||
if (img) {
|
||
const month = new Date().getMonth() + 1;
|
||
img.src = `${CFG.IMG_DAY_DIR}${month}${CFG.IMG_EXT}`;
|
||
}
|
||
|
||
_size = calcSize();
|
||
const viewport = document.getElementById("earth-viewport");
|
||
if (viewport) {
|
||
viewport.style.width = Math.round(_size.w) + "px";
|
||
viewport.style.height = Math.round(_size.h) + "px";
|
||
}
|
||
|
||
const wrapper = document.getElementById("earth-wrapper");
|
||
if (wrapper && typeof ResizeObserver !== "undefined") {
|
||
_resizeObserver = new ResizeObserver(() => _queueResize());
|
||
_resizeObserver.observe(wrapper);
|
||
}
|
||
|
||
_fireSlow();
|
||
_fireFast();
|
||
setInterval(_fireFast, CFG.UPDATE_FAST_MS);
|
||
setInterval(_fireSlow, CFG.UPDATE_SLOW_MS);
|
||
}
|
||
|
||
if (document.readyState === "loading") {
|
||
document.addEventListener("DOMContentLoaded", _start);
|
||
} else {
|
||
_start();
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// ÖFFENTLICHE API
|
||
// ═══════════════════════════════════════════════════════════════
|
||
return {
|
||
// Koordinaten-Helfer
|
||
toRad, toDeg, toX, toY,
|
||
// Canvas-Helfer
|
||
createCanvas, syncCanvas, roundRectPath,
|
||
// Astronomie
|
||
subsolarPoint,
|
||
// Größe
|
||
calcSize,
|
||
// Live-Getter
|
||
get sun() { return _sun; },
|
||
get size() { return _size; },
|
||
};
|
||
|
||
}());
|