/** * plugin-sun.js – Sonnen-Marker mit Popup (Aufgang & Untergang) * ================================================================ * Abhängigkeit: earth-core.js, astronomy.browser.min.js * * Erstellt: #sunCanvas (z-index 6) */ (function () { // ═══════════════════════════════════════════════════════════════ // KONFIGURATION // ═══════════════════════════════════════════════════════════════ const CFG = { CORE_RADIUS_PX: 16, RAY_INNER_PX: 19, RAY_OUTER_PX: 29, RAY_COUNT: 11, OBS_LAT: 50.3667, // Sternwarte Sonneberg OBS_LON: 11.1833, UPDATE_MS: 60_000, }; const { toX, toY, createCanvas, syncCanvas, roundRectPath, toDeg } = EarthCore; const sunCanvas = createCanvas("sunCanvas", 6); const ctx = sunCanvas.getContext("2d"); let _sun = { lat: 0, lon: 0 }; let _rise = null; let _set = null; let _popup = false; let _obs = null; // ═══════════════════════════════════════════════════════════════ // SONNENBERECHNUNG (Astronomy Engine) // ═══════════════════════════════════════════════════════════════ function sunPosition(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 }; } } // ═══════════════════════════════════════════════════════════════ // AUFGANG / UNTERGANG (Astronomy Engine) // ═══════════════════════════════════════════════════════════════ function findSunRiseSet(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.Sun, _obs, +1, d0, 1); const s = Astronomy.SearchRiseSet(Astronomy.Body.Sun, _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" }); } // ═══════════════════════════════════════════════════════════════ // ZEICHNEN // ═══════════════════════════════════════════════════════════════ function drawSunAt(cx, cy) { ctx.save(); ctx.strokeStyle = "rgba(255, 200, 0, 0.9)"; ctx.lineWidth = 1.5; ctx.shadowColor = "rgba(255, 200, 0, 0.7)"; ctx.shadowBlur = 6; for (let i = 0; i < CFG.RAY_COUNT; i++) { const a = (i / CFG.RAY_COUNT) * 2 * Math.PI; ctx.beginPath(); ctx.moveTo(cx + Math.cos(a) * CFG.RAY_INNER_PX, cy + Math.sin(a) * CFG.RAY_INNER_PX); ctx.lineTo(cx + Math.cos(a) * CFG.RAY_OUTER_PX, cy + Math.sin(a) * CFG.RAY_OUTER_PX); ctx.stroke(); } ctx.beginPath(); ctx.arc(cx, cy, CFG.CORE_RADIUS_PX, 0, 2 * Math.PI); ctx.fillStyle = "rgba(255, 230, 0, 1)"; ctx.shadowColor = "rgba(255, 200, 0, 0.9)"; ctx.shadowBlur = 12; ctx.fill(); ctx.strokeStyle = "rgba(255, 140, 0, 1)"; ctx.lineWidth = 1.5; ctx.stroke(); ctx.restore(); } function drawPopup(cx, cy, w, h) { const lines = [ "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.RAY_OUTER_PX + 8; let by = cy - bh / 2; if (bx + bw > w - 5) bx = cx - bw - CFG.RAY_OUTER_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(30, 20, 5, 0.92)"; ctx.fill(); ctx.strokeStyle = "rgba(255, 200, 80, 0.6)"; ctx.lineWidth = 1; ctx.stroke(); lines.forEach((line, i) => { ctx.fillStyle = i === 0 ? "rgba(255, 220, 100, 0.95)" : "rgba(255, 240, 200, 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(sunCanvas, w, h); ctx.clearRect(0, 0, w, h); const sx = toX(_sun.lon, w); const sy = toY(_sun.lat, h); const edge = CFG.RAY_OUTER_PX + 2; drawSunAt(sx, sy); if (sx - edge < 0) drawSunAt(sx + w, sy); if (sx + edge > w) drawSunAt(sx - w, sy); if (_popup) drawPopup(sx, sy, w, h); } // ═══════════════════════════════════════════════════════════════ // KLICK-ERKENNUNG // ═══════════════════════════════════════════════════════════════ function isHit(mx, my) { const { w, h } = EarthCore.size; const cx = toX(_sun.lon, w); const cy = toY(_sun.lat, h); const r = CFG.RAY_OUTER_PX + 4; 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 = sunCanvas.getBoundingClientRect(); if (!rect.width) return; const mx = (e.clientX - rect.left) * (sunCanvas.width / rect.width); const my = (e.clientY - rect.top) * (sunCanvas.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 = sunCanvas.getBoundingClientRect(); if (!rect.width) return; const mx = (e.clientX - rect.left) * (sunCanvas.width / rect.width); const my = (e.clientY - rect.top) * (sunCanvas.height / rect.height); if (isHit(mx, my)) document.body.style.cursor = "pointer"; }); // ═══════════════════════════════════════════════════════════════ // UPDATE // ═══════════════════════════════════════════════════════════════ function update() { const now = new Date(); _sun = sunPosition(now); const rs = findSunRiseSet(now); _rise = rs.rise; _set = rs.set; render(EarthCore.size); } window.addEventListener("earth:slow", () => update()); window.addEventListener("earth:resize", () => render(EarthCore.size)); update(); setInterval(update, CFG.UPDATE_MS); }());