feat(view): the moon in its phase over the "Follow the Sun" background (#661)

At dawn, dusk and night the environment shows the moon in the top-left
corner of the scene, behind the plan: computed in the card from the home
coordinates and the browser clock (short Meeus series + topocentric
parallax, within 1.4° / 1.8 pp of JPL Horizons), one designer image under
a continuous phase mask with the lit side always on the left (owner
2026-09-29), a feathered terminator, 3°/3 % thresholds and the 2 s fade of
the window rays. Everything but a small gate lives in the lazy
moon-runtime chunk, with its own 30 s ticker. General settings: "Sun"
becomes "Sun and Moon" with one switch, on for new installations
(DEFAULT_CONFIG), off for existing ones.

The initial View graph sat 728 B under its budget: the gate is paid for by
moving fifteen dialog-only strings of General settings into the lazy
settings dictionary (#459) and by one build fingerprint literal instead of
three, so the graph ends 4 B above dev. Golden: two new moon scenes, and
the two General settings help frames show «Sun and Moon»; the WSL artifact
test fixture now models scenes whose first capture awaits acceptance.

Issue: #661
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
Sergey Matyunin
2026-09-30 12:25:46 +03:00
co-authored by Claude Opus 5.5
parent e515bdaab8
commit c8c470ed57
58 changed files with 1682 additions and 126 deletions
+7
View File
@@ -2,6 +2,13 @@
## Unreleased
- On the "Follow the Sun" background, the moon in its current phase now appears
in the top-left corner of the scene at dawn, dusk and night, behind the plan.
It follows the real moon over your home (Home Assistant home location), fades
in and out as it crosses 3° above the horizon, and is hidden by day and around
new moon. General settings: the "Sun" section is now "Sun and Moon", with a
new switch that is on for new installations
([#661](https://github.com/Matysh/houseplan-card/issues/661)).
- In 2.5D View, device icons no longer shift when a device changes state (for
example when a light turns on and shows its brightness); a Home Assistant
update in a dense plan also no longer re-lays out every icon
+7
View File
@@ -8,6 +8,13 @@
## Не выпущено
- На фоне «Следует за Солнцем» в сумерках и ночью в левом верхнем углу сцены,
за планом, теперь видна луна в текущей фазе. Она следует настоящей луне над
вашим домом (по координатам дома из Home Assistant), плавно появляется и
исчезает при пересечении 3° над горизонтом, днём и в новолуние её нет. В общих
настройках раздел «Солнце» стал «Солнце и Луна», в нём новый переключатель,
включённый у новых установок
([#661](https://github.com/Matysh/houseplan-card/issues/661)).
- В объёмном 2.5D-виде значки устройств больше не сдвигаются, когда меняется
состояние устройства (например, лампа включилась и показала яркость);
обновление Home Assistant в плотном плане больше не переразмещает все значки
+18
View File
@@ -134,6 +134,24 @@ An older frontend ignores the field and shows Flat. An older backend preserves
it through the unknown-settings policy. Full backup/import carries `settings`
whole, so the value survives.
## Moon (#661)
`settings.moon` is an optional global boolean: the moon on the "Follow the Sun"
background for every space, user, device and kiosk. Only exact `true` switches
it on; absence, `false` or anything else read as off. Saving `false` removes
the key. There is no per-space moon: a space shows it only when its effective
`bg_mode` is `daynight`. The backend accepts only a boolean
(`vol.Optional("moon"): bool`), and the privacy-safe support projection copies
only a validated boolean. New installations get `moon: true` in
`DEFAULT_CONFIG`; an existing config has no key and keeps its look after the
update (docs/SUN.md). "Reset to defaults" in General settings sets `true`. The
store and model versions do not change; there is no migration.
An older frontend ignores the field and shows no moon. An older backend
preserves it through the unknown-settings policy. Full backup/import carries
`settings` whole, so the value survives; a one-space export does not carry
global settings.
## Stairs (#663)
`spaces[].stairs[]` is an optional bounded (250 records per space)
+92
View File
@@ -376,6 +376,92 @@ The wash lives where the Flat `.sunlayer` lives (floor group, above room fills
and Glow) as `.sunlayer.iso-sunwash`, one `.iso-sunbeam[data-opening]` per lit
window. Witness: `demo/smoke_iso_sun.mjs`.
## Moon — `settings.moon` (#661)
At dawn, dusk and night on the "Follow the Sun" background, the moon in its
current phase stands in the top-left corner of the scene: a thin crescent, a
half, a full disc. General settings › Sun and Moon › «Moon over the plan at
dusk and night» switches it for the whole installation.
**When it is shown** — all at once, otherwise there is no moon:
- `settings.moon === true` (global; absent, `false` or anything else is off);
- the effective `bg_mode` of the space is `daynight` — the moon lives in the
four-phase environment, so a space with its own `static` has no moon, and
there is no per-space moon switch;
- the environment phase is `dawn`, `dusk` or `night` (the same
`resolveDayCycle`, browser-clock fallback included);
- the topocentric altitude is ≥ 3° (`MOON_ELEVATION_MIN = RAY_ELEVATION_MIN`);
- the illuminated fraction is ≥ 3 % (`MOON_MIN_ILLUMINATION`, about ±1.5 days
around new moon);
- a View surface: View, kiosk or `houseplan-space-card` (editors have no
environment);
- `hass.config.latitude/longitude` are finite numbers.
**Where the numbers come from.** Home Assistant publishes no moon altitude
(`sun.sun` is the sun; the optional Moon integration gives eight phase names
without altitude, illumination or hemisphere). The card computes both from the
home coordinates and the browser clock (`src/moon.ts`): the short Meeus series
as in SunCalc, the topocentric parallax `h − π·cos h` with
`π = asin(6378.14 / distance)`, no refraction; the illuminated fraction
`k = (1 + cos i) / 2` from the Sun–Moon elongation. Against JPL Horizons
(airless) on twelve points in Moscow and Sydney, October 2026: altitude within
1.39°, illumination within 1.77 percentage points (`test/moon.test.mjs`, the
table and the query parameters are in the test).
**Phase.** One designer image of the full moon (`assets/moon/houseplan-1.0.0`,
art by JB) under an SVG mask. The lit side is **always the left one**, in both
hemispheres; waning runs the waxing states backwards (owner 2026-09-29). The
lit region is the left half of the box plus or minus the terminator
half-ellipse `R × R·|2k − 1|` (bulging into the dark side when `k > 0.5`); the
half's outer arc runs along the box, never along the limb, so the disc keeps
the art's own anti-aliased edge. The mask is a `<mask>`, not a `clipPath`
(Chrome with GPU rasterisation draws clip edges jagged). The terminator is
feathered by a Gaussian blur inside the mask (5 units of 512, about 2 px at
200 px), except for a full disc (`k ≥ 0.995`). The phase is continuous in `k`,
quantised to 0.01 only so the element changes when the fingerprint does. The
dark side is the same art at 8 % opacity.
**Place, size, layer.** Fixed top-left corner, box `min(200px, 25cqmin)` of the
scene (the environment is the size container), inset 5 % of the box; it does
not move with pan or zoom and never takes the pointer. It is the last child of
`.hp-day-cycle-env`: above the phase gradients and the sun glow, below the plan
paper, rooms, devices, labels and UI. A plan that fills the scene covers the
moon partly or entirely — that is the environment's norm, like the sun glow
(owner decision 7).
**Movement.** Opacity only, 2 s on the background curve (`RAY_FADE_MS`),
none under `prefers-reduced-motion`: rising through 3°, setting through it,
the new-moon threshold and the day phase all fade; the first appearance after
a page or chunk load does not. The element is recomputed on every render of the
environment and by its own 30 s ticker (the moon rises at most 0.25°/min);
the ticker keeps the last rendered inputs, and an equal fingerprint
(`visible | k`) costs no render. A card that renders without a moon, leaves the
page or is hidden drops or pauses its ticker.
**Weight.** Astronomy, art (≈ 9 KB gzip) and template are one lazy chunk,
`moon-runtime-*`, loaded when the moon is switched on, the environment exists
and it is not daytime; until it arrives there is no moon, a failed load is
retried at most every 30 s through the exact-build loader of the isometric
runtime. The initial View graph carries only the gate (`src/moon-gate.ts`).
One element, no CSS filter and no `will-change`: the composited layer count
does not change (`demo/smoke_daycycle_layer_budget.mjs`).
**Limits (documented, not bugs).**
- Position accuracy ≈ 1°: the moment of crossing 3° may differ from ephemerides
by a few minutes.
- The terminator is not tilted by the parallactic angle, and the lit side does
not follow the hemisphere (owner decision): the crescent is "vertical".
- Earthshine is not modelled; the dark side is an 8 % silhouette for legibility.
- The moon does not follow its azimuth — fixed corner of the scene.
- A dense layout (plan filling the screen) shows only the part of the disc in
the margins.
- Coordinates come from Home Assistant: an installation that kept the default
home location gets someone else's moon, as it gets someone else's `sun.sun`.
- A wrong tablet clock gives a wrong phase and moment — the same class as the
background's clock fallback.
## Weather independence and legacy `weather_entity`
Weather never changes the window rays. Once the feature, compass,
@@ -417,6 +503,12 @@ saving General settings removes it.
inheritance); unit-tested in `test/sun.test.mjs`.
- `src/day-cycle-render.ts` — the shared constant environment layers and
plan-outline variables for full and static cards.
- `src/moon.ts`, `src/moon-runtime.ts`, `src/moon-gate.ts`,
`src/moon-art.generated.ts` — the moon (#661): pure astronomy and mask, the
lazy chunk with the element and its ticker, the initial-graph gate, the
generated art (`node scripts/generate-moon-assets.mjs` from
`assets/moon/houseplan-1.0.0`); unit-tested in `test/moon.test.mjs`,
end-to-end in `demo/smoke_moon.mjs`.
- `src/houseplan-card.ts` — the memoised wedge layer, four-phase background
lifecycle, and both settings dialogs (compass dial included).
- `src/space-render.ts` / `src/space-card.ts` — static-card environment and
+29 -2
View File
@@ -37,7 +37,7 @@ relay, and exact plan geometry is attached only after you opt in and preview it.
12. [Device visual states](#12-device-visual-states)
13. [Room fills and light](#13-room-fills-and-light)
14. [Background editor](#14-background-editor)
15. [Sun background and window rays](#15-sun-background-and-window-rays)
15. [Sun and Moon: background, window rays and the moon](#15-sun-and-moon-background-window-rays-and-the-moon)
16. [Robot vacuums](#16-robot-vacuums)
17. [Kiosk](#17-kiosk)
18. [Static space card](#18-static-space-card)
@@ -1603,7 +1603,7 @@ keeps the same repairable placeholder instead of rejecting the whole plan.
![Selected line in the Background editor](images/07-background-editor.png)
## 15. Sun background and window rays
## 15. Sun and Moon: background, window rays and the moon
These are independent features. **Follow the sun** needs no compass: it uses
valid `sun.sun` data and otherwise falls back to the browser clock. North and
@@ -1651,6 +1651,33 @@ static card share the phase; editors keep their normal background. New installs
and spaces default to Follow the sun; upgrades/imports preserve their existing
choice. Shadows from trees, awnings or other building wings are not modelled.
### Moon
On the **Follow the sun** background, at dawn, dusk and night, the moon in its
current phase stands in the top-left corner of the scene — a thin crescent, a
half, a full disc. One switch in General settings turns it on: **Sun and Moon**
→ **Moon over the plan at dusk and night**. It is on for new installations and
off for upgraded ones until switched on: an update never changes how a plan
looks.
| Condition | Result |
|---|---|
| Daytime, moon below 3° above the horizon, or new moon (under 3 % lit) | No moon |
| The moon rises above 3° or sets below it | Fades in/out over 2 seconds; immediately with reduced motion |
| Phase | Changes continuously, every day; the lit side is always on the left, waning runs the same states backwards |
| Space with its own static background | No moon — it belongs to the Follow the sun background |
| The plan covers the corner of the scene | The moon is behind the plan: the part of the disc in the margins shows; the plan, devices and labels are always on top |
| Editors | No moon |
| Full card, kiosk, static space card | The same |
Position and phase are computed in the card from the Home Assistant home
location (Settings → System → General) and the device clock, with no
integration or external data. A Home Assistant that kept the default location
shows someone else's moon — as with `sun.sun`; a wrong tablet clock gives a
wrong phase and moment. Position accuracy is about 1°, so crossing 3° may
differ from ephemerides by a few minutes. The terminator tilt and earthshine
are not modelled; the dark side is a faint silhouette so a crescent reads.
## 16. Robot vacuums
Live position needs finite `vacuum_position` or `robot_position` coordinates.
+30 -3
View File
@@ -38,7 +38,7 @@ Assistant. Эта полноэкранная панель — основной
12. [Визуальные состояния устройств](#12-визуальные-состояния-устройств)
13. [Заливки комнат и свет](#13-заливки-комнат-и-свет)
14. [Редактор подложки](#14-редактор-подложки)
15. [Солнце: фон и оконные лучи](#15-солнце-фон-и-оконные-лучи)
15. [Солнце и Луна: фон, оконные лучи и луна](#15-солнце-и-луна-фон-оконные-лучи-и-луна)
16. [Роботы-пылесосы](#16-роботы-пылесосы)
17. [Киоск-режим](#17-киоск-режим)
18. [Статическая карточка пространства](#18-статическая-карточка-пространства)
@@ -1790,9 +1790,9 @@ WebP и безопасный SVG; лимит 2 МиБ относится к со
недостающего содержимого и после согласия оставляет ту же восстанавливаемую
рамку, а не отклоняет весь план.
## 15. Солнце: фон и оконные лучи
## 15. Солнце и Луна: фон, оконные лучи и луна
Это две независимые функции. Фон **Следует за Солнцем** работает без компаса:
Фон и оконные лучи — две независимые функции; луна живёт на фоне. Фон **Следует за Солнцем** работает без компаса:
он использует корректные данные `sun.sun`, а при их отсутствии автоматически
переходит на локальные часы браузера. Направление севера и `sun.sun` обязательны
только для оконных лучей. Общие значения можно переопределить в конкретном
@@ -1850,6 +1850,33 @@ WebP и безопасный SVG; лимит 2 МиБ относится к со
Тени от других крыльев здания и внешних объектов не моделируются.
### Луна
При фоне **Следует за Солнцем** в сумерках (утренних и вечерних) и ночью в левом
верхнем углу сцены видна луна в текущей фазе — тонкий серп, половина, полная.
Включается одним переключателем в общих настройках, раздел **Солнце и Луна** →
**Луна на плане в сумерках и ночью**. У новых установок переключатель включён,
у обновлённых старых — выключен, пока его не включат: обновление не меняет вид
плана.
| Условие | Результат |
|---|---|
| День, луна ниже 3° над горизонтом или новолуние (освещено меньше 3 %) | Луны нет |
| Луна поднимается выше 3° или уходит ниже | Плавно появляется/исчезает за 2 секунды; при системном уменьшении движения — сразу |
| Фаза | Меняется непрерывно, каждый день; освещённая сторона всегда слева, убывание идёт теми же состояниями в обратном порядке |
| Пространство с собственным статическим фоном | Луны нет — она часть фона «Следует за Солнцем» |
| План закрывает угол сцены | Луна за планом: видна та часть диска, что в полях; план, устройства и подписи всегда поверх неё |
| Редакторы | Луны нет |
| Полная карточка, киоск, статическая карточка пространства | Одинаково |
Положение и фаза считаются в карточке по координатам дома из Home Assistant
(Настройки → Система → Общие) и часам устройства, без интеграций и внешних
данных. Если координаты дома в Home Assistant не заданы, луна будет «чужой» —
как и данные `sun.sun`; с неверными часами планшета неверны фаза и момент
появления. Точность положения около 1°: момент пересечения 3° может разойтись с
эфемеридами на несколько минут. Наклон терминатора и пепельный свет не
воспроизводятся, тёмная сторона — едва заметный силуэт для читаемости серпа.
## 16. Роботы-пылесосы
Живая позиция доступна для маркера пылесоса, если источник отдаёт конечные