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