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

231 lines
9.2 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-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);
}());