Files
houseplan-card/src/moon-gate.ts
T
Sergey MatyuninandClaude Opus 5.5 c8c470ed57 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
2026-09-30 12:25:46 +03:00

51 lines
2.4 KiB
TypeScript

/**
* The moon's only foothold in the initial View graph (#661, C7): whether the
* lazy `moon-runtime` chunk is wanted, and one load per page for every card on
* it. It is wanted when the moon is switched on, the environment exists (the
* effective background follows the sun, on a View surface) and it is not
* daytime. Everything else — astronomy, art, element, its 30 s ticker — lives
* in the chunk.
*
* The contract of the other lazy runtimes, in the fewest bytes (the initial
* graph sits at its budget): a chunk from another build is never installed and
* never asked for again; a failed load leaves no moon and is retried at most
* every 30 s, on the next render, from the content-hashed URL with a fresh
* query so the browser's cached failure is not replayed.
*/
import { nothing, type TemplateResult } from 'lit';
import { ENTRY_BUILD_FINGERPRINT } from './editor-runtime-loader';
import type { DayCycleState } from './sun';
type MoonRuntime = typeof import('./moon-runtime');
export type MoonHost = import('./moon-runtime').MoonHost;
const MOON_RETRY_ASSET = '__HOUSEPLAN_MOON_RETRY_ASSET__';
let runtime: MoonRuntime | null = null;
/** Earliest next attempt; 0 before the first, Infinity after a foreign build. */
let nextAttempt = 0;
/**
* The moon element for `renderDayCycleEnvironment`. `state` is the
* environment (null: no environment, so no moon); `settings` the global
* settings, where only an explicit `moon: true` counts (C1). Nothing until the
* chunk is here; the card that asked re-renders when it arrives.
*/
export function moonLayer(
host: MoonHost | undefined, settings: unknown, state: DayCycleState | null,
): TemplateResult | typeof nothing {
if (!host || !state || (settings as { moon?: unknown } | null | undefined)?.moon !== true) return nothing;
if (!runtime && state.phase !== 'day' && Date.now() >= nextAttempt) {
const retry = nextAttempt > 0;
nextAttempt = Date.now() + 30_000;
(retry
? import(/* @vite-ignore */ new URL(`${MOON_RETRY_ASSET}?${nextAttempt}`, import.meta.url).href) as Promise<MoonRuntime>
: import('./moon-runtime')
).then((module) => {
if (module.MOON_RUNTIME_FINGERPRINT !== ENTRY_BUILD_FINGERPRINT) { nextAttempt = Infinity; return; }
runtime = module;
host.requestUpdate();
}, () => undefined);
}
return runtime?.renderMoon(host, settings, state.phase) ?? nothing;
}