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