diff --git a/sternwarte-homeassistant-card/dist/moon_small.png b/sternwarte-homeassistant-card/dist/moon_small.png new file mode 100644 index 0000000..b66bbe4 Binary files /dev/null and b/sternwarte-homeassistant-card/dist/moon_small.png differ diff --git a/sternwarte-homeassistant-card/dist/sternwarte-card.js b/sternwarte-homeassistant-card/dist/sternwarte-card.js index 9d32ea4..4ae6d86 100644 --- a/sternwarte-homeassistant-card/dist/sternwarte-card.js +++ b/sternwarte-homeassistant-card/dist/sternwarte-card.js @@ -22,6 +22,14 @@ const ASTRONOMY_LIB_URL = (() => { } })(); +const MOON_IMAGE_URL = (() => { + try { + return new URL("./moon_small.png", import.meta.url).toString(); + } catch (error) { + return null; + } +})(); + class SternwarteCard extends HTMLElement { constructor() { super(); @@ -31,10 +39,13 @@ class SternwarteCard extends HTMLElement { }; this._pendingRender = false; this._astronomyLoadPromise = null; + this._moonTexture = null; + this._moonTexturePromise = null; } connectedCallback() { this._ensureAstronomy(); + this._ensureMoonTexture(); this._ensureClockTimer(); } @@ -121,6 +132,29 @@ class SternwarteCard extends HTMLElement { return this._astronomyLoadPromise; } + _ensureMoonTexture() { + if (this._moonTexture) return Promise.resolve(this._moonTexture); + if (this._moonTexturePromise) return this._moonTexturePromise; + if (!MOON_IMAGE_URL) return Promise.resolve(null); + + this._moonTexturePromise = new Promise((resolve) => { + const image = new Image(); + image.addEventListener("load", () => { + this._moonTexture = image; + this._moonTexturePromise = null; + if (this._hass) this._requestRender(); + resolve(image); + }, { once: true }); + image.addEventListener("error", () => { + this._moonTexturePromise = null; + resolve(null); + }, { once: true }); + image.src = MOON_IMAGE_URL; + }); + + return this._moonTexturePromise; + } + _requestRender() { if (this._isScheduleEditing()) { this._pendingRender = true; @@ -500,7 +534,7 @@ class SternwarteCard extends HTMLElement { const astronomy = this._astronomy(); const observer = this._astroObserver(); if (!astronomy || !observer) { - return { altitude: Number.NEGATIVE_INFINITY, illumination: 0 }; + return { altitude: Number.NEGATIVE_INFINITY, illumination: 0, phaseAngle: 0 }; } try { @@ -508,16 +542,108 @@ class SternwarteCard extends HTMLElement { const equator = astronomy.Equator(bodyMoon, date, observer, true, true); const horizon = astronomy.Horizon(date, observer, equator.ra, equator.dec, "normal"); const illumination = astronomy.Illumination(bodyMoon, date); + const phaseAngle = Number(astronomy.MoonPhase(date)) || 0; return { altitude: (Number(horizon?.altitude) || 0) * Math.PI / 180, illumination: Math.max(0, Math.min(1, Number(illumination?.phase_fraction) || 0)), + phaseAngle: ((phaseAngle % 360) + 360) % 360, }; } catch (error) { - return { altitude: Number.NEGATIVE_INFINITY, illumination: 0 }; + return { altitude: Number.NEGATIVE_INFINITY, illumination: 0, phaseAngle: 0 }; } } + _moonVisualState() { + const moon = this._moonStateAt(new Date()); + const phaseAngle = moon.phaseAngle ?? 0; + const waxing = phaseAngle <= 180; + const radians = phaseAngle * Math.PI / 180; + + return { + illumination: Math.max(0, Math.min(1, moon.illumination || 0)), + illuminationPercent: Math.round((moon.illumination || 0) * 100), + waxing, + phaseAngle, + cosPhase: Math.cos(radians), + }; + } + + _renderMoonPhaseDataUrl(moonVisual, size = 64) { + const texture = this._moonTexture; + if (!texture) return ""; + + const canvas = document.createElement("canvas"); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext("2d"); + if (!ctx) return ""; + + const radius = size / 2 - 1; + const x = size / 2; + const y = size / 2; + const cosPhase = Number(moonVisual.cosPhase) || 0; + + ctx.save(); + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.clip(); + + ctx.fillStyle = "rgba(8,10,16,0.96)"; + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.fill(); + + ctx.globalAlpha = 0.98; + ctx.drawImage(texture, x - radius, y - radius, radius * 2, radius * 2); + ctx.globalAlpha = 1; + + ctx.fillStyle = "rgba(8,10,16,0.96)"; + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.fill(); + + ctx.save(); + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.clip(); + + if (moonVisual.illumination >= 0.999) { + ctx.drawImage(texture, x - radius, y - radius, radius * 2, radius * 2); + } else if (moonVisual.illumination > 0.001) { + ctx.beginPath(); + for (let i = 0; i <= 72; i += 1) { + const t = -radius + (2 * radius * i) / 72; + const xr = Math.sqrt(Math.max(0, radius * radius - t * t)); + const xt = (moonVisual.waxing ? cosPhase : -cosPhase) * xr; + if (i === 0) { + ctx.moveTo(x + xt, y + t); + } else { + ctx.lineTo(x + xt, y + t); + } + } + for (let i = 72; i >= 0; i -= 1) { + const t = -radius + (2 * radius * i) / 72; + const xr = Math.sqrt(Math.max(0, radius * radius - t * t)); + ctx.lineTo(x + (moonVisual.waxing ? xr : -xr), y + t); + } + ctx.closePath(); + ctx.clip(); + ctx.drawImage(texture, x - radius, y - radius, radius * 2, radius * 2); + } + + ctx.restore(); + ctx.restore(); + + ctx.strokeStyle = "rgba(255,255,255,0.18)"; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.stroke(); + + return canvas.toDataURL("image/png"); + } + _moonTrackState() { const now = new Date(); const cycleStart = new Date(now); @@ -709,6 +835,8 @@ class SternwarteCard extends HTMLElement { } const astro = this._astroState(); const moonTrack = this._moonTrackState(); + const moonVisual = this._moonVisualState(); + const moonImageDataUrl = this._renderMoonPhaseDataUrl(moonVisual, 56); this.shadowRoot.innerHTML = `