{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"pixel-scene","type":"registry:ui","title":"PixelScene","description":"An animated pixel-art landscape on a dot grid: sparse ink dots at rest, full colour under a round lens that follows the pointer, separate day and night palettes, and on HDR displays lit windows, stars and the sun drawn as squares brighter than SDR white. Four scenes: alpine, meadow, sea and mountains.","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["effects"],"devDependencies":["@webgpu/types@^0.1.71"],"registryDependencies":["https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/pixel-scene/PixelScene.tsx","content":"\"use client\";\n\nimport { forwardRef, useEffect, useRef, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\n\nimport { cn } from \"../../utils\";\nimport { useComposedRefs } from \"../../internals/dom/use-composed-refs\";\nimport { useElementRef } from \"../../internals/dom/use-element-ref\";\nimport { useLiveRef } from \"../../internals/dom/use-live-ref\";\nimport { createPixelScene, type PixelSceneEngine } from \"./pixel-scene-core\";\nimport { getScenePainter, type PixelSceneName } from \"./pixel-scene-scenes\";\nimport {\n\tPIXEL_SCENE_HDR_RESTART_MS,\n\tPIXEL_SCENE_HDR_RESTARTS,\n\tpixelSceneHdrAvailable,\n\tstartPixelSceneHdr,\n} from \"./pixel-scene-hdr\";\nimport type { PixelSceneHdrLook } from \"./pixel-scene-hdr\";\n\nexport type { PixelSceneName };\n\n/**\n * PixelScene — an animated pixel-art landscape on a dot grid.\n *\n * At rest only sparse ink dots and the lit cells show; a round lens that\n * follows the pointer reveals the picture in full colour, with a dithered\n * rim. Day and night are two complete palettes. On HDR displays the lit\n * cells (sun, moon, windows, stars, fireflies, the lens field) are redrawn\n * as hard squares brighter than SDR white in a WebGPU layer.\n *\n * The engine, the scenes and the HDR layer live in three framework-free\n * cores; this file owns the markup and pushes props into the engine.\n */\nexport interface PixelSceneProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n\t/** Which landscape to paint */\n\tscene?: PixelSceneName;\n\t/** Night palette (stars, moon, lit windows) instead of day. Never changed by the component. */\n\tnight?: boolean;\n\t/** Grid pitch in CSS px (clamped 4–32; grows on very large boxes) */\n\tcell?: number;\n\t/** Square dot edge in CSS px, at most `cell` */\n\tdot?: number;\n\t/** Show full colour under a round lens that follows the pointer */\n\tlens?: boolean;\n\t/** Lens radius in cells */\n\tlensRadius?: number;\n\t/** On HDR displays, draw lit cells brighter than SDR white in a WebGPU layer */\n\thdr?: boolean;\n\t/** HDR rendering: `calibrated` (glow 1 = the SDR dot, sRGB) or `vivid` (the prototype's hotter, display-p3 look) */\n\thdrLook?: PixelSceneHdrLook;\n\t/** Colour of the resting dots; any CSS colour, `currentColor` resolved on the host */\n\tink?: string;\n\t/** Freeze the scene's animations (the lens still follows the pointer) */\n\tpaused?: boolean;\n\t/** Animate a change of `night` as a dithered wave from the sun (never under reduced motion) */\n\twave?: boolean;\n\t/** `[day, night]` background painted by the scene itself; the wave carries it. Default: transparent */\n\tsurfaces?: readonly [day: string, night: string];\n\t/** `[day, night]` dot colours, swapped by the wave; overrides `ink` */\n\tinks?: readonly [day: string, night: string];\n\t/** Additional classes on the host — give it a height */\n\tclassName?: string;\n\t/** Copy rendered above the picture */\n\tchildren?: ReactNode;\n}\n\n/** The options the engine is created with, read once at creation time. */\ninterface CreateProps {\n\tscene: PixelSceneName;\n\tnight: boolean;\n\tcell: number;\n\tdot: number;\n\tlens: boolean;\n\tlensRadius: number;\n\tink: string;\n\tpaused: boolean;\n\twave: boolean;\n\tsurfaces: readonly [string, string] | null;\n\tinks: readonly [string, string] | null;\n}\n\nexport const PixelScene = forwardRef<HTMLDivElement, PixelSceneProps>(function PixelScene(\n\t{\n\t\tscene = \"alpine\",\n\t\tnight = false,\n\t\tcell = 5,\n\t\tdot = 4,\n\t\tlens = true,\n\t\tlensRadius = 21,\n\t\thdr = true,\n\t\thdrLook = \"calibrated\",\n\t\tink = \"currentColor\",\n\t\tpaused = false,\n\t\twave = true,\n\t\tsurfaces,\n\t\tinks,\n\t\tclassName,\n\t\tchildren,\n\t\t...restProps\n\t},\n\tforwardedRef\n) {\n\t// The nodes, not refs: the mount effect is keyed on them, so it runs the\n\t// moment both exist rather than racing the first commit.\n\tconst [host, hostRef] = useElementRef<HTMLDivElement>();\n\tconst [canvas, canvasRef] = useElementRef<HTMLCanvasElement>();\n\tconst [hdrCanvas, hdrCanvasRef] = useElementRef<HTMLCanvasElement>();\n\tconst composedHostRef = useComposedRefs<HTMLDivElement>(forwardedRef, hostRef);\n\n\t/** Plain ref on purpose: nothing rendered depends on the engine. */\n\tconst engineRef = useRef<PixelSceneEngine | null>(null);\n\tconst [engineReady, setEngineReady] = useState(false);\n\t/** False on the server and the first client render: the HDR canvas appears only after mount. */\n\tconst [hdrCapable, setHdrCapable] = useState(false);\n\tconst [hdrOn, setHdrOn] = useState(false);\n\t/** Bumped to start the HDR layer again after a device loss; the effect below depends on it. */\n\tconst [hdrRun, setHdrRun] = useState(0);\n\tconst hdrRestarts = useRef(0);\n\n\t// The Svelte source creates the engine in `onMount` from `untrack`ed props:\n\t// the creation reads the current values without becoming a dependency.\n\tconst createProps = useLiveRef<CreateProps>({\n\t\tscene,\n\t\tnight,\n\t\tcell,\n\t\tdot,\n\t\tlens,\n\t\tlensRadius,\n\t\tink,\n\t\tpaused,\n\t\twave,\n\t\tsurfaces: surfaces ?? null,\n\t\tinks: inks ?? null,\n\t});\n\n\tuseEffect(() => {\n\t\tif (!host || !canvas) return;\n\t\tconst p = createProps.current;\n\t\tconst engine = createPixelScene(\n\t\t\t{ host, canvas },\n\t\t\t{\n\t\t\t\tpainter: getScenePainter(p.scene),\n\t\t\t\tnight: p.night,\n\t\t\t\tcell: p.cell,\n\t\t\t\tdot: p.dot,\n\t\t\t\tlens: p.lens,\n\t\t\t\tlensRadius: p.lensRadius,\n\t\t\t\tink: p.ink,\n\t\t\t\tpaused: p.paused,\n\t\t\t\twave: p.wave,\n\t\t\t\tsurfaces: p.surfaces,\n\t\t\t\tinks: p.inks,\n\t\t\t}\n\t\t);\n\t\tengineRef.current = engine;\n\t\tsetEngineReady(engine !== null);\n\t\treturn () => {\n\t\t\tengine?.destroy();\n\t\t\tengineRef.current = null;\n\t\t\tsetEngineReady(false);\n\t\t};\n\t}, [host, canvas, createProps]);\n\n\t// --- live props: each effect pushes its own keys; the engine ignores absent ones\n\n\tuseEffect(() => {\n\t\tengineRef.current?.setOptions({ painter: getScenePainter(scene), night });\n\t}, [scene, night, engineReady]);\n\n\tuseEffect(() => {\n\t\tengineRef.current?.setOptions({ cell, dot });\n\t}, [cell, dot, engineReady]);\n\n\t// Tuples by value: a new array with the same colours is no change. The key is\n\t// JSON so a colour holding a newline or a quote cannot split or collide; the\n\t// engine gets the original tuples from the live ref, never a re-split string.\n\tconst surfacesKey = surfaces ? JSON.stringify(surfaces) : \"\";\n\tconst inksKey = inks ? JSON.stringify(inks) : \"\";\n\tuseEffect(() => {\n\t\tengineRef.current?.setOptions({\n\t\t\tlens,\n\t\t\tlensRadius,\n\t\t\tink,\n\t\t\tpaused,\n\t\t\twave,\n\t\t\tsurfaces: createProps.current.surfaces,\n\t\t\tinks: createProps.current.inks,\n\t\t});\n\t}, [lens, lensRadius, ink, paused, wave, surfacesKey, inksKey, engineReady, createProps]);\n\n\t// HDR: decided after mount only, so SSR and hydration render the 2D canvas alone.\n\tuseEffect(() => {\n\t\tsetHdrCapable(hdr && pixelSceneHdrAvailable());\n\t}, [hdr]);\n\n\tuseEffect(() => {\n\t\tconst engine = engineRef.current;\n\t\tif (!hdrCanvas || !engineReady || !hdrCapable || !engine) return;\n\t\tlet cancelled = false;\n\t\tlet stop: (() => void) | null = null;\n\t\tlet retry: ReturnType<typeof setTimeout> | null = null;\n\t\t// A lost device usually comes back (GPU process restarted, driver reset):\n\t\t// start again after a growing pause, a few times, before giving the layer up.\n\t\tconst restart = () => {\n\t\t\tif (hdrRestarts.current >= PIXEL_SCENE_HDR_RESTARTS) {\n\t\t\t\tsetHdrCapable(false);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\thdrRestarts.current++;\n\t\t\tretry = setTimeout(() => {\n\t\t\t\tretry = null;\n\t\t\t\tsetHdrRun((n) => n + 1);\n\t\t\t}, PIXEL_SCENE_HDR_RESTART_MS * hdrRestarts.current);\n\t\t};\n\t\tvoid startPixelSceneHdr(hdrCanvas, hdrLook).then((layer) => {\n\t\t\tif (cancelled || engineRef.current !== engine) {\n\t\t\t\tlayer?.destroy();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!layer) {\n\t\t\t\t// Nothing to start on the first try (no adapter, tone mapping refused): give\n\t\t\t\t// up at once. During a restart the GPU may still be on its way back.\n\t\t\t\tif (hdrRestarts.current === 0) setHdrCapable(false);\n\t\t\t\telse restart();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tengine.setOptions({ onFrame: (frame) => layer.render(frame) });\n\t\t\tsetHdrOn(true);\n\t\t\tstop = () => {\n\t\t\t\tengine.setOptions({ onFrame: null });\n\t\t\t\tlayer.destroy();\n\t\t\t\tsetHdrOn(false);\n\t\t\t};\n\t\t\tvoid layer.lost.then(() => {\n\t\t\t\tif (cancelled) return;\n\t\t\t\tstop?.();\n\t\t\t\tstop = null;\n\t\t\t\trestart();\n\t\t\t});\n\t\t});\n\t\treturn () => {\n\t\t\tcancelled = true;\n\t\t\tstop?.();\n\t\t\tstop = null;\n\t\t\tif (retry !== null) clearTimeout(retry);\n\t\t};\n\t\t// hdrRun is read by no line above: it re-runs the effect after a device loss.\n\t}, [hdrCanvas, engineReady, hdrCapable, hdrRun, hdrLook]);\n\n\treturn (\n\t\t<div\n\t\t\tref={composedHostRef}\n\t\t\tclassName={cn(\"pixel-scene relative overflow-hidden\", className)}\n\t\t\tdata-scene={scene}\n\t\t\tdata-night={night ? \"\" : undefined}\n\t\t\tdata-hdr={hdrOn ? \"\" : undefined}\n\t\t\t{...restProps}\n\t\t>\n\t\t\t<canvas\n\t\t\t\tref={canvasRef}\n\t\t\t\tclassName=\"pointer-events-none absolute inset-0 block h-full w-full\"\n\t\t\t\taria-hidden=\"true\"\n\t\t\t/>\n\t\t\t{hdrCapable ? (\n\t\t\t\t// Lit cells above SDR white, over the 2D picture, on HDR displays only.\n\t\t\t\t<canvas\n\t\t\t\t\tref={hdrCanvasRef}\n\t\t\t\t\tclassName=\"pixel-scene__hdr pointer-events-none absolute inset-0 block h-full w-full\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t/>\n\t\t\t) : null}\n\t\t\t{children != null ? (\n\t\t\t\t<div className=\"pixel-scene__content relative z-[1] h-full w-full\">{children}</div>\n\t\t\t) : null}\n\t\t</div>\n\t);\n});\n","type":"registry:ui","target":"@ui/fancy/components/pixel-scene/PixelScene.tsx"},{"path":"react/src/components/pixel-scene/index.ts","content":"\"use client\";\n\nexport { PixelScene } from \"./PixelScene\";\nexport type { PixelSceneProps, PixelSceneName } from \"./PixelScene\";\nexport { PIXEL_SCENES } from \"./pixel-scene-scenes\";\nexport type { PixelSceneHdrLook } from \"./pixel-scene-hdr\";\n","type":"registry:ui","target":"@ui/fancy/components/pixel-scene/index.ts"},{"path":"react/src/components/pixel-scene/pixel-scene-core.ts","content":"\"use client\";\n\n/**\n * PixelScene core — the framework-free engine, byte-identical in the Svelte,\n * React and Vue trees (see shared-cores.json).\n *\n * A picture is a grid of square cells. Every frame a scene painter fills the\n * grid through `writeCell` (the `put()` grammar below), `pixelHalos` adds the\n * dithered light cells around emitters, and `paintGrid` draws one square dot\n * per cell into a 2D canvas: sparse ink dots outside the lens, full colour\n * under it, lit cells in colour everywhere. Cells with `glow > 0` are handed\n * to the optional HDR layer through `onFrame`.\n *\n * First the pure helpers (hash, Bayer, noise, packed colours), then the grid,\n * the lens, the painter, and `createPixelScene`, the engine that owns the\n * canvas, the rAF loop, the pointer listeners and every observer. No\n * framework imports, and nothing touches `window` or `document` at module\n * scope, so the file is safe to import on the server.\n *\n * Colours are packed `0xRRGGBB` integers (`-1` = empty cell). Scenes may still\n * pass `\"#hex\"` literals; `parseColour` caches them. No CSS string is built\n * per cell.\n *\n * Compiles under `noUncheckedIndexedAccess`. The `!` assertions are in bounds\n * by construction (indices come from loops over the arrays' own sizes).\n */\n\n// --- pure helpers ------------------------------------------------------------\n\n/** 4×4 ordered-dither thresholds, centred in (0, 1). */\nexport const BAYER: readonly number[] = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5].map(\n\t(v) => (v + 0.5) / 16\n);\n\n/** Ordered-dither threshold of a cell, one of 16 levels in (0, 1). */\nexport function bayer(x: number, y: number): number {\n\treturn BAYER[(((y % 4) + 4) % 4) * 4 + (((x % 4) + 4) % 4)]!;\n}\n\n/** Deterministic hash of a cell and a seed, in [0, 1). */\nexport function hash(x: number, y: number, s = 0): number {\n\tlet h = (x * 374761393 + y * 668265263 + s * 2246822519) >>> 0;\n\th = Math.imul(h ^ (h >>> 13), 1274126177) >>> 0;\n\treturn ((h ^ (h >>> 16)) >>> 0) / 4294967296;\n}\n\nexport function clamp(v: number, a: number, b: number): number {\n\treturn Math.max(a, Math.min(b, v));\n}\n\n/** Smoothstep on [0, 1], clamped. */\nexport function smooth(t: number): number {\n\tt = clamp(t, 0, 1);\n\treturn t * t * (3 - 2 * t);\n}\n\n/** 1D value noise in [0, 1). */\nexport function noise1(x: number, seed: number): number {\n\tconst i = Math.floor(x);\n\tconst f = x - i;\n\tconst a = hash(i, 0, seed);\n\tconst b = hash(i + 1, 0, seed);\n\treturn a + (b - a) * smooth(f);\n}\n\n/** Two octaves of value noise: a ridge line `amp` tall over `scale` cells. */\nexport function ridge(x: number, seed: number, scale: number, amp: number): number {\n\treturn noise1(x / scale, seed) * amp + noise1(x / (scale * 0.37), seed + 1) * amp * 0.35;\n}\n\n// --- packed colours ----------------------------------------------------------\n\n/** A colour as scenes write it: a `\"#hex\"` / `rgb()` string or a packed int. */\nexport type Colour = string | number;\n\nconst PARSED = new Map<string, number>();\nconst RGB_RE = /^rgba?\\(\\s*(\\d+(?:\\.\\d+)?)[\\s,]+(\\d+(?:\\.\\d+)?)[\\s,]+(\\d+(?:\\.\\d+)?)/i;\n\n/** `#rgb`, `#rrggbb` or `rgb()` → `0xRRGGBB`; a packed int passes through; anything else → -1. */\nexport function parseColour(c: Colour): number {\n\tif (typeof c === \"number\") return c;\n\tconst hit = PARSED.get(c);\n\tif (hit !== undefined) return hit;\n\tlet v = -1;\n\tconst s = c.trim();\n\tif (/^#[0-9a-f]{6}$/i.test(s)) v = parseInt(s.slice(1), 16);\n\telse if (/^#[0-9a-f]{3}$/i.test(s)) {\n\t\tconst r = parseInt(s[1]!, 16);\n\t\tconst g = parseInt(s[2]!, 16);\n\t\tconst b = parseInt(s[3]!, 16);\n\t\tv = ((r * 17) << 16) | ((g * 17) << 8) | (b * 17);\n\t} else {\n\t\tconst m = RGB_RE.exec(s);\n\t\tif (m) {\n\t\t\tconst r = clamp(Math.round(Number(m[1])), 0, 255);\n\t\t\tconst g = clamp(Math.round(Number(m[2])), 0, 255);\n\t\t\tconst b = clamp(Math.round(Number(m[3])), 0, 255);\n\t\t\tv = (r << 16) | (g << 8) | b;\n\t\t}\n\t}\n\tif (PARSED.size < 4096) PARSED.set(c, v);\n\treturn v;\n}\n\n/** Linear mix of two colours, channels truncated as the prototype's `rgb()` did. */\nexport function mixHex(a: Colour, b: Colour, t: number): number {\n\tconst A = parseColour(a);\n\tconst B = parseColour(b);\n\tconst ar = (A >> 16) & 255;\n\tconst ag = (A >> 8) & 255;\n\tconst ab = A & 255;\n\tconst r = clamp((ar + (((B >> 16) & 255) - ar) * t) | 0, 0, 255);\n\tconst g = clamp((ag + (((B >> 8) & 255) - ag) * t) | 0, 0, 255);\n\tconst bl = clamp((ab + ((B & 255) - ab) * t) | 0, 0, 255);\n\treturn (r << 16) | (g << 8) | bl;\n}\n\n/** Gradient through `[position, colour]` stops at `t` in [0, 1]. */\nexport function grad(stops: readonly (readonly [number, Colour])[], t: number): number {\n\tt = clamp(t, 0, 1);\n\tfor (let i = 1; i < stops.length; i++) {\n\t\tconst s1 = stops[i]!;\n\t\tif (t <= s1[0]) {\n\t\t\tconst s0 = stops[i - 1]!;\n\t\t\treturn mixHex(s0[1], s1[1], (t - s0[0]) / (s1[0] - s0[0] || 1));\n\t\t}\n\t}\n\tconst last = stops[stops.length - 1];\n\treturn last ? parseColour(last[1]) : 0;\n}\n\nconst CSS_CACHE = new Map<number, string>();\n\n/** Packed colour → cached `#rrggbb`. */\nexport function cssOf(c: number): string {\n\tlet s = CSS_CACHE.get(c);\n\tif (s === undefined) {\n\t\ts = \"#\" + (c & 0xffffff).toString(16).padStart(6, \"0\");\n\t\tif (CSS_CACHE.size < 4096) CSS_CACHE.set(c, s);\n\t}\n\treturn s;\n}\n\nconst LINEAR = new Float32Array(256);\nfor (let i = 0; i < 256; i++) {\n\tconst s = i / 255;\n\tLINEAR[i] = s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);\n}\n\n/** sRGB channel 0–255 → linear light 0–1 (lookup table). */\nexport function srgbToLinear(c8: number): number {\n\treturn LINEAR[clamp(Math.round(c8), 0, 255)]!;\n}\n\n/** Linear light → extended-sRGB encoded value; keeps going above 1 for HDR. */\nexport function encodeExt(v: number): number {\n\treturn v <= 0.0031308 ? 12.92 * v : 1.055 * Math.pow(v, 1 / 2.4) - 0.055;\n}\n\n// --- grid --------------------------------------------------------------------\n\nexport interface PixelGrid {\n\tcols: number;\n\trows: number;\n\t/** -1 = empty, else 0xRRGGBB — what the lens shows. */\n\tcolor: Int32Array;\n\t/** Dot alpha outside the lens (0 = no dot). */\n\tinkA: Float32Array;\n\t/** 1 = colour shown outside the lens too. */\n\tlit: Uint8Array;\n\t/** HDR multiple of SDR white, 0 = SDR only. */\n\tglow: Float32Array;\n\t/** Emitter halo radius in cells (0 = not an emitter). */\n\thalo: Float32Array;\n\t/** Halo colour override for the HDR layer, -1 = none. */\n\thcol: Int32Array;\n\t/**\n\t * Soft golden halo radius in cells, HDR only (0 = none). The one exception to\n\t * the hard-square rule, kept for the lit windows of houses.\n\t */\n\tsoft: Float32Array;\n\t/** Flat `[x, y, colour, glow, haloCells] × n`. */\n\temitters: number[];\n\t/**\n\t * The copy corner, in cells from the top-left (0 = none): lens cells inside\n\t * it are not boosted, so text over the picture keeps SDR contrast. Set by\n\t * the painter each frame.\n\t */\n\tcopyW: number;\n\tcopyH: number;\n\t/** Centre of the sun (or moon) in cells, -1 when the frame has none. Set by the painter. */\n\tsunX: number;\n\tsunY: number;\n}\n\nexport const INK_ALPHA_NIGHT = 0.6;\nexport const INK_ALPHA_DAY = 0.85;\n/** Glow of every cell revealed by the lens. */\nexport const LENS_BOOST = 1.6;\n/** Glow of a cell written with `l = true`. */\nexport const LIT_GLOW = 1.5;\n/** Highest glow a cell may carry (≈ 1000 nits over a 203-nit SDR white). */\nexport const GLOW_PEAK = 4.9;\nexport const MAX_CELLS = 72_000;\nexport const MAX_DPR = 2;\n/** Backing-store budget per canvas, device px (2048²): past it the backing drops to DPR 1. */\nexport const MAX_BACKING_PX = 4_200_000;\nexport const MIN_CELL = 4;\nexport const MAX_CELL = 32;\n\nexport function createGrid(cols: number, rows: number): PixelGrid {\n\tconst n = Math.max(0, cols * rows);\n\tconst g: PixelGrid = {\n\t\tcols,\n\t\trows,\n\t\tcolor: new Int32Array(n),\n\t\tinkA: new Float32Array(n),\n\t\tlit: new Uint8Array(n),\n\t\tglow: new Float32Array(n),\n\t\thalo: new Float32Array(n),\n\t\thcol: new Int32Array(n),\n\t\tsoft: new Float32Array(n),\n\t\temitters: [],\n\t\tcopyW: 0,\n\t\tcopyH: 0,\n\t\tsunX: -1,\n\t\tsunY: -1,\n\t};\n\tclearGrid(g);\n\treturn g;\n}\n\nexport function clearGrid(g: PixelGrid): void {\n\tg.color.fill(-1);\n\tg.inkA.fill(0);\n\tg.lit.fill(0);\n\tg.glow.fill(0);\n\tg.halo.fill(0);\n\tg.hcol.fill(-1);\n\tg.soft.fill(0);\n\tg.emitters.length = 0;\n\tg.copyW = 0;\n\tg.copyH = 0;\n\tg.sunX = -1;\n\tg.sunY = -1;\n}\n\n/**\n * The prototype's `put()`. `a` is the dot alpha outside the lens. `l`: false =\n * plain cell, true = lit (colour outside the lens, glow {@link LIT_GLOW}),\n * number = lit and HDR at that multiple of SDR white (clamped to\n * {@link GLOW_PEAK}). `hl > 0` with a numeric `l` registers an emitter whose\n * dithered halo `pixelHalos` adds. Out-of-bounds cells are ignored.\n */\nexport function writeCell(\n\tg: PixelGrid,\n\tx: number,\n\ty: number,\n\tc: Colour,\n\ta = 0,\n\tl: boolean | number = false,\n\thl = 0\n): void {\n\tx = Math.round(x);\n\ty = Math.round(y);\n\tif (!(x >= 0 && y >= 0 && x < g.cols && y < g.rows)) return;\n\tconst i = y * g.cols + x;\n\tconst col = typeof c === \"number\" ? c : parseColour(c);\n\tg.color[i] = col;\n\tg.inkA[i] = a;\n\tg.lit[i] = l ? 1 : 0;\n\tconst glow = typeof l === \"number\" ? Math.min(GLOW_PEAK, l) : l ? LIT_GLOW : 0;\n\tg.glow[i] = glow;\n\tg.halo[i] = hl;\n\tg.hcol[i] = -1;\n\tg.soft[i] = 0;\n\tif (hl > 0 && typeof l === \"number\" && glow > 0) g.emitters.push(x, y, col, glow, hl);\n}\n\n/**\n * Around every emitter, light neighbouring cells as hard squares, Bayer\n * dithered so density and brightness fall with distance. No blur anywhere.\n */\nexport function pixelHalos(g: PixelGrid): void {\n\tconst { cols, rows, emitters, glow, halo, color, hcol } = g;\n\tfor (let e = 0; e + 4 < emitters.length; e += 5) {\n\t\tconst ex = emitters[e]!;\n\t\tconst ey = emitters[e + 1]!;\n\t\tconst c = emitters[e + 2]!;\n\t\tconst l = emitters[e + 3]!;\n\t\tconst r = emitters[e + 4]! * 2.2;\n\t\tconst y0 = Math.max(0, Math.floor(ey - r));\n\t\tconst y1 = Math.min(rows - 1, Math.ceil(ey + r));\n\t\tconst x0 = Math.max(0, Math.floor(ex - r));\n\t\tconst x1 = Math.min(cols - 1, Math.ceil(ex + r));\n\t\tfor (let y = y0; y <= y1; y++) {\n\t\t\tfor (let x = x0; x <= x1; x++) {\n\t\t\t\tconst i = y * cols + x;\n\t\t\t\tif (halo[i]! > 0) continue; // emitters stay as they are\n\t\t\t\tconst d = Math.hypot(x - ex, y - ey) / r;\n\t\t\t\tif (d >= 1) continue;\n\t\t\t\tconst dens = Math.pow(1 - d, 1.7);\n\t\t\t\tif (bayer(x, y) >= dens) continue;\n\t\t\t\tconst gl = l * (0.3 + 0.55 * dens);\n\t\t\t\tif (gl <= glow[i]!) continue;\n\t\t\t\tglow[i] = gl;\n\t\t\t\thcol[i] = c;\n\t\t\t\tif (color[i]! < 0) color[i] = c;\n\t\t\t}\n\t\t}\n\t}\n}\n\n// --- day / night wave --------------------------------------------------------\n\n/** Duration of a day / night wave, ms (ease in-out cubic). */\nexport const WAVE_MS = 1300;\n/** Thickness of the dithered halo that leads the wave front, in cells. */\nexport const WAVE_HALO_CELLS = 9;\n/** Width of the dithered HDR glow on each side of the halo band, in cells. */\nexport const WAVE_GLOW_CELLS = 7;\n/** Halo colours from the sparse outer fringe to the dense inner rim. */\nexport const WAVE_PALETTE: readonly number[] = [0xf7cf4a, 0xf6a24e, 0xef7a5e, 0xea7d9c];\n\nexport function easeInOutCubic(t: number): number {\n\tif (!(t > 0)) return 0;\n\tif (t >= 1) return 1;\n\treturn t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;\n}\n\n/** Glow of a halo cell at depth D (1 = inner rim, 0 = outer fringe). */\nexport function waveGlow(D: number): number {\n\treturn 1.5 + 3.4 * clamp(D, 0, 1);\n}\n\n/**\n * Glow of a cell in the band around the halo, `d` in [0, 1) from the halo edge\n * outward, or 0 when the Bayer threshold leaves it dark: density `(1 − d)^1.7`.\n */\nexport function waveBandGlow(x: number, y: number, d: number, inner: boolean): number {\n\tif (!(d >= 0 && d < 1)) return 0;\n\tconst dens = Math.pow(1 - d, 1.7);\n\tif (bayer(x, y) >= dens) return 0;\n\treturn inner ? 1.2 + 2.2 * dens : 1.1 + 1.6 * dens;\n}\n\n/** Radius (cells) at which a front from (ox, oy) has passed every cell, halo included. */\nexport function waveCover(ox: number, oy: number, cols: number, rows: number): number {\n\treturn (\n\t\tMath.max(\n\t\t\tMath.hypot(ox, oy),\n\t\t\tMath.hypot(cols - ox, oy),\n\t\t\tMath.hypot(ox, rows - oy),\n\t\t\tMath.hypot(cols - ox, rows - oy)\n\t\t) +\n\t\tWAVE_HALO_CELLS +\n\t\tWAVE_GLOW_CELLS +\n\t\t1\n\t);\n}\n\n/**\n * Composites one wave frame into `cur` (the old picture): cells the front has\n * passed (`dist < R − halo`) take `next`'s content, the halo band is\n * Bayer-dithered with `D^0.7` and lit at {@link waveGlow} (SDR inside the copy\n * corner), the rest keeps the old picture. Emitters follow their side.\n * `side[i]` is set to 1 for cells on the new side, 0 elsewhere.\n */\nexport function composeWave(\n\tcur: PixelGrid,\n\tnext: PixelGrid,\n\tside: Uint8Array,\n\tox: number,\n\toy: number,\n\tR: number\n): void {\n\tconst { cols, rows } = cur;\n\tconst Rin = R - WAVE_HALO_CELLS;\n\tconst em: number[] = [];\n\tfor (let k = 0; k + 4 < cur.emitters.length; k += 5)\n\t\tif (Math.hypot(cur.emitters[k]! + 0.5 - ox, cur.emitters[k + 1]! + 0.5 - oy) >= Rin)\n\t\t\tem.push(...cur.emitters.slice(k, k + 5));\n\tfor (let k = 0; k + 4 < next.emitters.length; k += 5)\n\t\tif (Math.hypot(next.emitters[k]! + 0.5 - ox, next.emitters[k + 1]! + 0.5 - oy) < Rin)\n\t\t\tem.push(...next.emitters.slice(k, k + 5));\n\tconst copyW = next.copyW || cur.copyW || 0;\n\tconst copyH = next.copyH || cur.copyH || 0;\n\tfor (let y = 0; y < rows; y++) {\n\t\tfor (let x = 0; x < cols; x++) {\n\t\t\tconst i = y * cols + x;\n\t\t\tconst dist = Math.hypot(x + 0.5 - ox, y + 0.5 - oy);\n\t\t\tconst inCopy = x < copyW && y < copyH;\n\t\t\tif (dist < Rin) {\n\t\t\t\tside[i] = 1;\n\t\t\t\tcur.color[i] = next.color[i]!;\n\t\t\t\tcur.inkA[i] = next.inkA[i]!;\n\t\t\t\tcur.lit[i] = next.lit[i]!;\n\t\t\t\tcur.glow[i] = next.glow[i]!;\n\t\t\t\tcur.halo[i] = next.halo[i]!;\n\t\t\t\tcur.hcol[i] = next.hcol[i]!;\n\t\t\t\tcur.soft[i] = next.soft[i]!;\n\t\t\t\t// HDR glow inside the rim: pink, dithered, fading toward the origin\n\t\t\t\tconst g = inCopy ? 0 : waveBandGlow(x, y, (Rin - dist) / WAVE_GLOW_CELLS, true);\n\t\t\t\tif (g > cur.glow[i]!) {\n\t\t\t\t\tcur.glow[i] = g;\n\t\t\t\t\tcur.hcol[i] = WAVE_PALETTE[3]!;\n\t\t\t\t\tif (cur.color[i]! < 0) cur.color[i] = WAVE_PALETTE[3]!;\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tside[i] = 0;\n\t\t\tif (dist >= R) {\n\t\t\t\t// HDR glow past the fringe: yellow, sparser\n\t\t\t\tconst g = inCopy ? 0 : waveBandGlow(x, y, (dist - R) / WAVE_GLOW_CELLS, false);\n\t\t\t\tif (g > cur.glow[i]!) {\n\t\t\t\t\tcur.glow[i] = g;\n\t\t\t\t\tcur.hcol[i] = WAVE_PALETTE[0]!;\n\t\t\t\t\tif (cur.color[i]! < 0) cur.color[i] = WAVE_PALETTE[0]!;\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst D = (R - dist) / WAVE_HALO_CELLS;\n\t\t\tif (bayer(x, y) > Math.pow(D, 0.7)) continue;\n\t\t\tcur.color[i] = WAVE_PALETTE[clamp(Math.floor(D * 4), 0, 3)]!;\n\t\t\tcur.inkA[i] = 0.9;\n\t\t\tcur.lit[i] = 1;\n\t\t\tcur.glow[i] = inCopy ? 1 : waveGlow(D);\n\t\t\tcur.halo[i] = 0;\n\t\t\tcur.hcol[i] = -1;\n\t\t}\n\t}\n\tcur.emitters.length = 0;\n\tcur.emitters.push(...em);\n}\n\n// --- lens --------------------------------------------------------------------\n\nexport interface Lens {\n\tx: number;\n\ty: number;\n\ttx: number;\n\tty: number;\n\tr: number;\n\ttr: number;\n}\n\nexport function createLens(): Lens {\n\treturn { x: -999, y: -999, tx: -999, ty: -999, r: 0, tr: 0 };\n}\n\n/**\n * Moves the lens toward its target, frame-rate independent (`k = 1 − 0.6^(dt/16.667)`;\n * the radius opens 1.3× and closes 0.7× as fast). `snap` jumps to the target.\n * Returns whether it is still moving.\n */\nexport function stepLens(l: Lens, dt: number, snap: boolean): boolean {\n\tif (snap) {\n\t\tl.x = l.tx;\n\t\tl.y = l.ty;\n\t\tl.r = l.tr;\n\t\treturn false;\n\t}\n\tconst f = Math.max(0, dt) / 16.667;\n\tconst k = 1 - Math.pow(0.6, f);\n\tl.x += (l.tx - l.x) * k;\n\tl.y += (l.ty - l.y) * k;\n\tconst kr = 1 - Math.pow(0.6, f * (l.tr > l.r ? 1.3 : 0.7));\n\tl.r += (l.tr - l.r) * kr;\n\tlet moving = false;\n\tif (Math.abs(l.tx - l.x) > 0.01) moving = true;\n\telse l.x = l.tx;\n\tif (Math.abs(l.ty - l.y) > 0.01) moving = true;\n\telse l.y = l.ty;\n\tif (Math.abs(l.tr - l.r) > 0.01) moving = true;\n\telse l.r = l.tr;\n\treturn moving;\n}\n\n/** Whether a cell is revealed: inside the radius, with a Bayer-dithered rim. */\nexport function inLens(l: Lens, x: number, y: number): boolean {\n\treturn l.r > 0.5 && Math.hypot(x + 0.5 - l.x, y + 0.5 - l.y) < l.r - 2 + bayer(x, y) * 4;\n}\n\n// --- painter -----------------------------------------------------------------\n\nexport interface PaintParams {\n\t/** Grid pitch in CSS px. */\n\tcell: number;\n\t/** Dot edge in CSS px. */\n\tdot: number;\n\tdpr: number;\n\t/** Resting dot colour, any canvas colour string. */\n\tink: string;\n\tnight: boolean;\n\t/** During a wave: cells with `side[i] === 1` use `sideInk` at the ink alpha of `sideNight`. */\n\tside?: Uint8Array | null;\n\tsideNight?: boolean;\n\tsideInk?: string;\n\t/** Fills the canvas before the dots (the scene owns its background); null = transparent. */\n\tsurface?: string | null;\n\t/** During a wave: whole cells on the new side are filled with it (a grid-aligned staircase). */\n\tsideSurface?: string | null;\n\t/** Backing-store size to clear, device px (defaults to `ctx.canvas`). */\n\twidth?: number;\n\theight?: number;\n}\n\n/** Device-pixel origin of cell `x` (or row `y`); the HDR layer uses the same expression. */\nexport function cellOrigin(x: number, cell: number, dpr: number): number {\n\treturn Math.round(x * cell * dpr);\n}\n\n/**\n * One 2D pass over the grid. Raises the glow of lens cells to at least\n * {@link LENS_BOOST} (mutates `g.glow`, as the HDR layer reads it next),\n * except inside the copy corner (`g.copyW × g.copyH`).\n */\nexport function paintGrid(\n\tctx: CanvasRenderingContext2D,\n\tg: PixelGrid,\n\tlens: Lens,\n\tp: PaintParams\n): void {\n\tconst { cols, rows, color, inkA, lit, glow } = g;\n\tconst copyW = g.copyW || 0;\n\tconst copyH = g.copyH || 0;\n\tconst w = p.width ?? ctx.canvas?.width ?? 0;\n\tconst h = p.height ?? ctx.canvas?.height ?? 0;\n\tctx.setTransform(1, 0, 0, 1, 0, 0);\n\tctx.clearRect(0, 0, w, h);\n\tconst size = Math.max(1, Math.round(p.dot * p.dpr));\n\tconst inkScale = p.night ? INK_ALPHA_NIGHT : INK_ALPHA_DAY;\n\tconst side = p.side ?? null;\n\tconst sideInk = p.sideInk ?? p.ink;\n\tctx.globalAlpha = 1;\n\tif (p.surface) {\n\t\tctx.fillStyle = p.surface;\n\t\tctx.fillRect(0, 0, w, h);\n\t}\n\tif (side && p.sideSurface) {\n\t\tctx.fillStyle = p.sideSurface;\n\t\tfor (let y = 0; y < rows; y++) {\n\t\t\tconst y0 = cellOrigin(y, p.cell, p.dpr);\n\t\t\tconst y1 = cellOrigin(y + 1, p.cell, p.dpr);\n\t\t\tlet x0 = -1;\n\t\t\tfor (let x = 0; x <= cols; x++) {\n\t\t\t\tconst on = x < cols && side[y * cols + x] === 1;\n\t\t\t\tif (on && x0 < 0) x0 = x;\n\t\t\t\telse if (!on && x0 >= 0) {\n\t\t\t\t\tconst a = cellOrigin(x0, p.cell, p.dpr);\n\t\t\t\t\tctx.fillRect(a, y0, cellOrigin(x, p.cell, p.dpr) - a, y1 - y0);\n\t\t\t\t\tx0 = -1;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tconst sideScale = p.sideNight ? INK_ALPHA_NIGHT : INK_ALPHA_DAY;\n\tconst open = lens.r > 0.5;\n\t// lens bounding box in cells (rim reaches r + 2)\n\tconst lx0 = open ? Math.floor(lens.x - lens.r - 3) : 1;\n\tconst lx1 = open ? Math.ceil(lens.x + lens.r + 3) : 0;\n\tconst ly0 = open ? Math.floor(lens.y - lens.r - 3) : 1;\n\tconst ly1 = open ? Math.ceil(lens.y + lens.r + 3) : 0;\n\tlet curStyle = -2; // -2 = nothing set, -1 = ink, -3 = side ink, else the colour\n\tlet curAlpha = -1;\n\tctx.globalAlpha = 1;\n\tfor (let y = 0; y < rows; y++) {\n\t\tconst oy = cellOrigin(y, p.cell, p.dpr);\n\t\tconst rowLens = y >= ly0 && y <= ly1;\n\t\tfor (let x = 0; x < cols; x++) {\n\t\t\tconst i = y * cols + x;\n\t\t\tconst c = color[i]!;\n\t\t\tif (c < 0) continue;\n\t\t\tlet style: number;\n\t\t\tlet alpha: number;\n\t\t\tif (rowLens && x >= lx0 && x <= lx1 && inLens(lens, x, y)) {\n\t\t\t\tstyle = c;\n\t\t\t\talpha = 1;\n\t\t\t\tif (glow[i]! < LENS_BOOST && !(x < copyW && y < copyH)) glow[i] = LENS_BOOST;\n\t\t\t} else if (lit[i]) {\n\t\t\t\tstyle = c;\n\t\t\t\talpha = 1;\n\t\t\t} else {\n\t\t\t\tconst a = inkA[i]!;\n\t\t\t\tif (!(a > 0)) continue;\n\t\t\t\tconst onSide = !!side && side[i] === 1;\n\t\t\t\tstyle = onSide ? -3 : -1;\n\t\t\t\talpha = Math.min(1, a * (onSide ? sideScale : inkScale));\n\t\t\t}\n\t\t\tif (style !== curStyle) {\n\t\t\t\tctx.fillStyle = style === -1 ? p.ink : style === -3 ? sideInk : cssOf(style);\n\t\t\t\tcurStyle = style;\n\t\t\t}\n\t\t\tif (alpha !== curAlpha) {\n\t\t\t\tctx.globalAlpha = alpha;\n\t\t\t\tcurAlpha = alpha;\n\t\t\t}\n\t\t\tctx.fillRect(cellOrigin(x, p.cell, p.dpr), oy, size, size);\n\t\t}\n\t}\n\tctx.globalAlpha = 1;\n}\n\n/**\n * Clamps `cell` to [4, 32] and `dot` to [1, cell], then grows the cell on big\n * boxes so the grid stays under {@link MAX_CELLS}; the dot keeps its ratio.\n */\nexport function effectiveCell(\n\tcssW: number,\n\tcssH: number,\n\tcell: number,\n\tdot: number\n): { cell: number; dot: number } {\n\tlet c = Number.isFinite(cell) ? clamp(cell, MIN_CELL, MAX_CELL) : 5;\n\tlet d = Number.isFinite(dot) ? clamp(dot, 1, c) : Math.min(4, c);\n\tconst W = Math.max(0, cssW);\n\tconst H = Math.max(0, cssH);\n\tif (Math.ceil(W / c) * Math.ceil(H / c) > MAX_CELLS) {\n\t\tconst base = c;\n\t\tc = Math.max(c, Math.ceil(Math.sqrt((W * H) / MAX_CELLS)));\n\t\twhile (Math.ceil(W / c) * Math.ceil(H / c) > MAX_CELLS) c++;\n\t\td = Math.max(1, Math.min(c, Math.round((d * c) / base)));\n\t}\n\treturn { cell: c, dot: d };\n}\n\n/**\n * The backing-store DPR for a box: the device DPR (already capped at\n * {@link MAX_DPR}), or 1 when that would exceed {@link MAX_BACKING_PX}. The\n * canvas is then upscaled with `image-rendering: pixelated`, so the squares\n * stay hard.\n */\nexport function backingDpr(cssW: number, cssH: number, deviceDpr: number): number {\n\tconst d = Number.isFinite(deviceDpr) && deviceDpr > 0 ? deviceDpr : 1;\n\treturn d > 1 && Math.max(0, cssW) * Math.max(0, cssH) * d * d > MAX_BACKING_PX ? 1 : d;\n}\n\n// --- engine ------------------------------------------------------------------\n\n/** Paints one frame of a scene into the grid. Must be synchronous. */\nexport type ScenePainter = (t: number, g: PixelGrid, night: boolean) => void;\n\n/** What the HDR layer reads after each painted frame (device px, live views — read synchronously). */\nexport interface PixelSceneFrame {\n\tcols: number;\n\trows: number;\n\t/** Backing-store size, device px. */\n\twidth: number;\n\theight: number;\n\t/** `cell × dpr`; a cell's origin is `round(x · cellCss · dpr)`. */\n\tpitch: number;\n\t/** Dot edge, device px: `round(dot · dpr)`. */\n\tsize: number;\n\tcellCss: number;\n\tdpr: number;\n\t/** True when the backing is below the device resolution (see {@link backingDpr}). */\n\tpixelated?: boolean;\n\tcolor: Int32Array;\n\tglow: Float32Array;\n\thcol: Int32Array;\n\t/** Soft halo radius per cell, in cells (see {@link PixelGrid.soft}). */\n\tsoft: Float32Array;\n}\n\nexport interface PixelSceneElements {\n\thost: HTMLElement;\n\tcanvas: HTMLCanvasElement;\n}\n\nexport interface PixelSceneOptions {\n\tpainter: ScenePainter;\n\tnight?: boolean;\n\tcell?: number;\n\tdot?: number;\n\tlens?: boolean;\n\tlensRadius?: number;\n\tink?: string;\n\tpaused?: boolean;\n\t/** Animate a change of `night` as a wave from the sun (default true; never under reduced motion). */\n\twave?: boolean;\n\t/** `[day, night]` background painted by the canvas; null = transparent (the host shows). */\n\tsurfaces?: readonly [string, string] | null;\n\t/** `[day, night]` dot colours; null = `ink`. */\n\tinks?: readonly [string, string] | null;\n\t/** Override for tests; default = the live `prefers-reduced-motion` query. */\n\treducedMotion?: boolean;\n\t/** HDR hook; a throwing callback is dropped and the 2D picture keeps going. */\n\tonFrame?: ((f: PixelSceneFrame) => void) | null;\n}\n\nexport interface PixelSceneEngine {\n\t/** Absent or undefined keys are left untouched; `onFrame: null` clears the hook. */\n\tsetOptions(next: Partial<PixelSceneOptions>): void;\n\tresize(): void;\n\tdestroy(): void;\n}\n\nexport function normalizeLensRadius(r: number | undefined): number {\n\treturn typeof r === \"number\" && Number.isFinite(r) ? clamp(r, 2, 200) : 21;\n}\n\nconst HEX_RE = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;\nconst INK_FALLBACK = \"#808080\";\n\n/** null when there is no 2D context (server, jsdom) — the component then keeps an inert canvas. */\nexport function createPixelScene(\n\tel: PixelSceneElements,\n\toptions: PixelSceneOptions\n): PixelSceneEngine | null {\n\tconst { host, canvas } = el;\n\tlet ctx: CanvasRenderingContext2D | null = null;\n\ttry {\n\t\tctx = canvas.getContext(\"2d\");\n\t} catch {\n\t\tctx = null;\n\t}\n\tif (!ctx) return null;\n\tconst c2d = ctx;\n\n\tlet painter = options.painter;\n\tlet night = !!options.night;\n\tlet cellOpt = options.cell ?? 5;\n\tlet dotOpt = options.dot ?? 4;\n\tlet lensOn = options.lens ?? true;\n\tlet lensRadius = normalizeLensRadius(options.lensRadius);\n\tlet inkOpt = options.ink ?? \"currentColor\";\n\tlet paused = !!options.paused;\n\tlet waveOn = options.wave ?? true;\n\tlet surfaces = options.surfaces ?? null;\n\tlet inks = options.inks ?? null;\n\tlet rmOverride: boolean | undefined = options.reducedMotion;\n\tlet onFrame = options.onFrame ?? null;\n\n\tlet mq: MediaQueryList | null = null;\n\tlet mqReduced = false;\n\tif (typeof window !== \"undefined\" && typeof window.matchMedia === \"function\") {\n\t\ttry {\n\t\t\tmq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n\t\t\tmqReduced = mq.matches;\n\t\t} catch {\n\t\t\tmq = null;\n\t\t}\n\t}\n\tconst reduced = () => (rmOverride !== undefined ? rmOverride : mqReduced);\n\n\tlet grid = createGrid(0, 0);\n\t/** The new theme's picture during a wave. */\n\tlet nextGrid = createGrid(0, 0);\n\tlet side = new Uint8Array(0);\n\tconst wave = { on: false, from: false, ox: 0, oy: 0, start: -1, cover: 0, oldInk: \"\" };\n\tlet cssW = 0;\n\tlet cssH = 0;\n\tlet dpr = 1;\n\t/** The device DPR `dpr` was derived from; a change triggers a relayout. */\n\tlet deviceDpr = 1;\n\tlet cell = 5;\n\tlet dot = 4;\n\tlet t = 0;\n\tlet last = 0;\n\tlet raf = 0;\n\tlet dirty = true;\n\tlet lensMoving = false;\n\tlet inView = true;\n\tlet destroyed = false;\n\tlet ink = INK_FALLBACK;\n\tlet inkAt = -Infinity;\n\tlet inkTimer: ReturnType<typeof setTimeout> | 0 = 0;\n\tconst lens = createLens();\n\tconst frame: PixelSceneFrame = {\n\t\tcols: 0,\n\t\trows: 0,\n\t\twidth: 0,\n\t\theight: 0,\n\t\tpitch: 0,\n\t\tsize: 0,\n\t\tcellCss: 0,\n\t\tdpr: 1,\n\t\tpixelated: false,\n\t\tcolor: grid.color,\n\t\tglow: grid.glow,\n\t\thcol: grid.hcol,\n\t\tsoft: grid.soft,\n\t};\n\n\tconst readDpr = () =>\n\t\ttypeof window !== \"undefined\" && window.devicePixelRatio > 0\n\t\t\t? Math.min(MAX_DPR, window.devicePixelRatio)\n\t\t\t: 1;\n\n\tfunction resolveInk(now: number, force: boolean): void {\n\t\tif (inks) {\n\t\t\tink = inks[night ? 1 : 0];\n\t\t\treturn;\n\t\t}\n\t\tif (HEX_RE.test(inkOpt.trim())) {\n\t\t\tink = inkOpt.trim();\n\t\t\treturn;\n\t\t}\n\t\tif (!force && now - inkAt < 250) return;\n\t\tinkAt = now;\n\t\ttry {\n\t\t\tif (inkOpt.trim().toLowerCase() !== \"currentcolor\") {\n\t\t\t\t// any other CSS colour the canvas understands goes through as is\n\t\t\t\tink = inkOpt;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst col = typeof getComputedStyle === \"function\" ? getComputedStyle(host).color : \"\";\n\t\t\tink = col || INK_FALLBACK;\n\t\t} catch {\n\t\t\tink = INK_FALLBACK;\n\t\t}\n\t}\n\n\t/** `currentColor` is the only ink that can change under a sleeping engine. */\n\tconst inkFollowsHost = () => inkOpt.trim().toLowerCase() === \"currentcolor\";\n\n\t/** Re-reads a `currentColor` ink and repaints once if it changed. */\n\tfunction refreshInk(): void {\n\t\tif (destroyed || !inkFollowsHost()) return;\n\t\tconst before = ink;\n\t\tresolveInk(typeof performance !== \"undefined\" ? performance.now() : 0, true);\n\t\tif (ink !== before) wake();\n\t}\n\n\t/**\n\t * While the loop sleeps in view (paused, reduced motion, lens at rest), a\n\t * slow timer re-reads `currentColor`, so a theme change applied anywhere\n\t * up the tree (or through a colour transition) still reaches the dots.\n\t */\n\tfunction armInkPoll(): void {\n\t\tif (inkTimer || destroyed || !inkFollowsHost() || typeof setTimeout !== \"function\") return;\n\t\tinkTimer = setTimeout(() => {\n\t\t\tinkTimer = 0;\n\t\t\tif (destroyed || raf || !awake()) return;\n\t\t\trefreshInk();\n\t\t\tif (!raf) armInkPoll();\n\t\t}, 500);\n\t}\n\tfunction clearInkPoll(): void {\n\t\tif (inkTimer && typeof clearTimeout === \"function\") clearTimeout(inkTimer);\n\t\tinkTimer = 0;\n\t}\n\n\tfunction layout(): void {\n\t\tcssW = host.clientWidth || 0;\n\t\tcssH = host.clientHeight || 0;\n\t\tdeviceDpr = readDpr();\n\t\tdpr = backingDpr(cssW, cssH, deviceDpr);\n\t\tconst pixelated = dpr < deviceDpr;\n\t\tframe.pixelated = pixelated;\n\t\tconst rendering = pixelated ? \"pixelated\" : \"\";\n\t\tif (canvas.style && canvas.style.imageRendering !== rendering)\n\t\t\tcanvas.style.imageRendering = rendering;\n\t\tconst eff = effectiveCell(cssW, cssH, cellOpt, dotOpt);\n\t\tcell = eff.cell;\n\t\tdot = eff.dot;\n\t\tconst cols = cssW > 0 ? Math.ceil(cssW / cell) : 0;\n\t\tconst rows = cssH > 0 ? Math.ceil(cssH / cell) : 0;\n\t\tif (cols !== grid.cols || rows !== grid.rows) {\n\t\t\tgrid = createGrid(cols, rows);\n\t\t\tnextGrid = createGrid(cols, rows);\n\t\t\tside = new Uint8Array(cols * rows);\n\t\t\tframe.color = grid.color;\n\t\t\tframe.glow = grid.glow;\n\t\t\tframe.hcol = grid.hcol;\n\t\t\tframe.soft = grid.soft;\n\t\t}\n\t\tconst bw = Math.round(cssW * dpr);\n\t\tconst bh = Math.round(cssH * dpr);\n\t\tif (canvas.width !== bw) canvas.width = bw;\n\t\tif (canvas.height !== bh) canvas.height = bh;\n\t\tdirty = true;\n\t}\n\n\tconst docHidden = () => typeof document !== \"undefined\" && document.hidden === true;\n\tconst awake = () => inView && !docHidden() && grid.cols * grid.rows > 0 && !destroyed;\n\tconst animating = () => !paused && !reduced();\n\n\tfunction schedule(): void {\n\t\tif (raf || destroyed) return;\n\t\tif (!awake() || !(animating() || lensMoving || dirty || wave.on)) {\n\t\t\tlast = 0;\n\t\t\tif (awake()) armInkPoll();\n\t\t\telse clearInkPoll();\n\t\t\treturn;\n\t\t}\n\t\tif (typeof requestAnimationFrame !== \"function\") return;\n\t\tclearInkPoll();\n\t\traf = requestAnimationFrame(tick);\n\t}\n\n\t/** Starts a wave from the sun of either picture, else the lens, else the top right. */\n\tfunction startWave(from: boolean, to: boolean): void {\n\t\tclearGrid(nextGrid);\n\t\tpainter(t, nextGrid, to);\n\t\tlet ox = grid.sunX >= 0 ? grid.sunX : nextGrid.sunX;\n\t\tlet oy = grid.sunX >= 0 ? grid.sunY : nextGrid.sunY;\n\t\tif (!(ox >= 0)) {\n\t\t\tconst lensed = lens.r > 0.5 || lens.tr > 0;\n\t\t\tox = lensed ? lens.tx : grid.cols * 0.85;\n\t\t\toy = lensed ? lens.ty : grid.rows * 0.2;\n\t\t}\n\t\twave.on = true;\n\t\twave.oldInk = inks ? inks[from ? 1 : 0] : ink;\n\t\twave.from = from;\n\t\twave.ox = ox;\n\t\twave.oy = oy;\n\t\twave.start = -1;\n\t\twave.cover = waveCover(ox, oy, grid.cols, grid.rows);\n\t}\n\n\tfunction render(now: number): void {\n\t\tresolveInk(now, false);\n\t\tclearGrid(grid);\n\t\tlet waving = false;\n\t\tif (wave.on) {\n\t\t\tif (wave.start < 0) wave.start = now;\n\t\t\tconst p = Math.min(1, Math.max(0, (now - wave.start) / WAVE_MS));\n\t\t\tif (p >= 1) wave.on = false;\n\t\t\telse {\n\t\t\t\twaving = true;\n\t\t\t\tpainter(t, grid, wave.from);\n\t\t\t\tclearGrid(nextGrid);\n\t\t\t\tpainter(t, nextGrid, night);\n\t\t\t\tcomposeWave(grid, nextGrid, side, wave.ox, wave.oy, easeInOutCubic(p) * wave.cover);\n\t\t\t}\n\t\t}\n\t\tif (!waving) painter(t, grid, night);\n\t\tpixelHalos(grid);\n\t\tpaintGrid(c2d, grid, lens, {\n\t\t\tcell,\n\t\t\tdot,\n\t\t\tdpr,\n\t\t\tink: waving ? wave.oldInk : ink,\n\t\t\tnight: waving ? wave.from : night,\n\t\t\tside: waving ? side : null,\n\t\t\tsideNight: night,\n\t\t\tsideInk: ink,\n\t\t\tsurface: surfaces ? surfaces[(waving ? wave.from : night) ? 1 : 0] : null,\n\t\t\tsideSurface: surfaces && waving ? surfaces[night ? 1 : 0] : null,\n\t\t\twidth: canvas.width,\n\t\t\theight: canvas.height,\n\t\t});\n\t\tif (onFrame) {\n\t\t\tframe.cols = grid.cols;\n\t\t\tframe.rows = grid.rows;\n\t\t\tframe.width = canvas.width;\n\t\t\tframe.height = canvas.height;\n\t\t\tframe.pitch = cell * dpr;\n\t\t\tframe.size = Math.max(1, Math.round(dot * dpr));\n\t\t\tframe.cellCss = cell;\n\t\t\tframe.dpr = dpr;\n\t\t\ttry {\n\t\t\t\tonFrame(frame);\n\t\t\t} catch {\n\t\t\t\tonFrame = null;\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction tick(now: number): void {\n\t\traf = 0;\n\t\tif (destroyed) return;\n\t\tif (!awake()) {\n\t\t\tlast = 0;\n\t\t\treturn;\n\t\t}\n\t\tconst dt = last ? Math.min(64, Math.max(0, now - last)) : 16.667;\n\t\tlast = now;\n\t\tif (readDpr() !== deviceDpr) layout();\n\t\tif (animating()) t += dt;\n\t\tlensMoving = stepLens(lens, dt, reduced());\n\t\trender(now);\n\t\tdirty = false;\n\t\tschedule();\n\t}\n\n\tfunction wake(): void {\n\t\tdirty = true;\n\t\tschedule();\n\t}\n\n\t// --- pointer -------------------------------------------------------------\n\n\tfunction aim(e: PointerEvent): void {\n\t\tif (!lensOn) return;\n\t\tconst b = canvas.getBoundingClientRect();\n\t\tif (!(b.width > 0 && b.height > 0)) return;\n\t\tconst sx = cssW > 0 ? cssW / b.width : 1;\n\t\tconst sy = cssH > 0 ? cssH / b.height : 1;\n\t\tlens.tx = ((e.clientX - b.left) * sx) / cell;\n\t\tlens.ty = ((e.clientY - b.top) * sy) / cell;\n\t\tif (lens.r < 0.5) {\n\t\t\t// a closed lens opens where the pointer is instead of flying in\n\t\t\tlens.x = lens.tx;\n\t\t\tlens.y = lens.ty;\n\t\t}\n\t\tlens.tr = lensRadius;\n\t\tlensMoving = true;\n\t\twake();\n\t}\n\tfunction close(): void {\n\t\tlens.tr = 0;\n\t\tlensMoving = true;\n\t\twake();\n\t}\n\tconst onMove = (e: PointerEvent) => aim(e);\n\tconst onUp = (e: PointerEvent) => {\n\t\tif (e.pointerType === \"touch\") close();\n\t};\n\tconst onLeave = () => close();\n\thost.addEventListener(\"pointermove\", onMove);\n\thost.addEventListener(\"pointerdown\", onMove);\n\thost.addEventListener(\"pointerup\", onUp);\n\thost.addEventListener(\"pointerleave\", onLeave);\n\thost.addEventListener(\"pointercancel\", onLeave);\n\n\t// --- observers -----------------------------------------------------------\n\n\tlet ro: ResizeObserver | null = null;\n\tif (typeof ResizeObserver !== \"undefined\") {\n\t\tro = new ResizeObserver(() => {\n\t\t\tconst w = host.clientWidth || 0;\n\t\t\tconst h = host.clientHeight || 0;\n\t\t\tif (w === cssW && h === cssH && readDpr() === deviceDpr) return;\n\t\t\tlayout();\n\t\t\tschedule();\n\t\t});\n\t\tro.observe(host);\n\t}\n\tlet io: IntersectionObserver | null = null;\n\tif (typeof IntersectionObserver !== \"undefined\") {\n\t\tio = new IntersectionObserver(\n\t\t\t(entries) => {\n\t\t\t\tconst entry = entries[entries.length - 1];\n\t\t\t\tif (!entry) return;\n\t\t\t\tinView = entry.isIntersecting;\n\t\t\t\tif (inView) schedule();\n\t\t\t\telse clearInkPoll();\n\t\t\t},\n\t\t\t{ rootMargin: \"128px\" }\n\t\t);\n\t\tio.observe(host);\n\t}\n\tconst onVisibility = () => {\n\t\tif (!docHidden()) schedule();\n\t\telse clearInkPoll();\n\t};\n\tif (typeof document !== \"undefined\") document.addEventListener(\"visibilitychange\", onVisibility);\n\tconst onMq = (e: MediaQueryListEvent) => {\n\t\tmqReduced = e.matches;\n\t\twake();\n\t};\n\tmq?.addEventListener?.(\"change\", onMq);\n\n\t// A theme switch on <html> (class, style, data-theme) or in the OS repaints\n\t// a sleeping engine whose ink is `currentColor`.\n\tlet themeMo: MutationObserver | null = null;\n\tif (typeof MutationObserver !== \"undefined\" && typeof document !== \"undefined\") {\n\t\ttry {\n\t\t\tthemeMo = new MutationObserver(() => refreshInk());\n\t\t\tthemeMo.observe(document.documentElement, {\n\t\t\t\tattributes: true,\n\t\t\t\tattributeFilter: [\"class\", \"style\", \"data-theme\"],\n\t\t\t});\n\t\t} catch {\n\t\t\tthemeMo = null;\n\t\t}\n\t}\n\tlet schemeMq: MediaQueryList | null = null;\n\tconst onScheme = () => refreshInk();\n\tif (typeof window !== \"undefined\" && typeof window.matchMedia === \"function\") {\n\t\ttry {\n\t\t\tschemeMq = window.matchMedia(\"(prefers-color-scheme: dark)\");\n\t\t\tschemeMq.addEventListener?.(\"change\", onScheme);\n\t\t} catch {\n\t\t\tschemeMq = null;\n\t\t}\n\t}\n\t// A DPR change (zoom, another screen) fires no resize: relayout on it.\n\tlet dprMq: MediaQueryList | null = null;\n\tconst onDpr = () => {\n\t\tif (destroyed) return;\n\t\tlayout();\n\t\twatchDpr();\n\t\twake();\n\t};\n\tfunction watchDpr(): void {\n\t\tdprMq?.removeEventListener?.(\"change\", onDpr);\n\t\tdprMq = null;\n\t\tif (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return;\n\t\tconst raw = window.devicePixelRatio > 0 ? window.devicePixelRatio : 1;\n\t\ttry {\n\t\t\tdprMq = window.matchMedia(`(resolution: ${raw}dppx)`);\n\t\t\tdprMq.addEventListener?.(\"change\", onDpr);\n\t\t} catch {\n\t\t\tdprMq = null;\n\t\t}\n\t}\n\twatchDpr();\n\n\tresolveInk(0, true);\n\tlayout();\n\tschedule();\n\n\treturn {\n\t\tsetOptions(next) {\n\t\t\tif (destroyed) return;\n\t\t\tlet relayout = false;\n\t\t\tif (next.painter !== undefined && next.painter !== painter) painter = next.painter;\n\t\t\tif (next.wave !== undefined) waveOn = !!next.wave;\n\t\t\tif (next.night !== undefined && !!next.night !== night) {\n\t\t\t\tconst to = !!next.night;\n\t\t\t\t// A change mid-wave lands the running one: the new wave starts from its target.\n\t\t\t\twave.on = false;\n\t\t\t\tif (waveOn && !reduced() && grid.cols * grid.rows > 0) startWave(night, to);\n\t\t\t\tnight = to;\n\t\t\t\tif (inks) ink = inks[to ? 1 : 0];\n\t\t\t}\n\t\t\tif (next.surfaces !== undefined) surfaces = next.surfaces;\n\t\t\tif (next.inks !== undefined) {\n\t\t\t\tinks = next.inks;\n\t\t\t\tresolveInk(0, true);\n\t\t\t}\n\t\t\tif (next.cell !== undefined && next.cell !== cellOpt) {\n\t\t\t\tcellOpt = next.cell;\n\t\t\t\trelayout = true;\n\t\t\t}\n\t\t\tif (next.dot !== undefined && next.dot !== dotOpt) {\n\t\t\t\tdotOpt = next.dot;\n\t\t\t\trelayout = true;\n\t\t\t}\n\t\t\tif (next.lens !== undefined) {\n\t\t\t\tlensOn = !!next.lens;\n\t\t\t\tif (!lensOn) lens.tr = 0;\n\t\t\t}\n\t\t\tif (next.lensRadius !== undefined) {\n\t\t\t\tlensRadius = normalizeLensRadius(next.lensRadius);\n\t\t\t\tif (lens.tr > 0) lens.tr = lensRadius;\n\t\t\t}\n\t\t\tif (next.ink !== undefined) {\n\t\t\t\tinkOpt = next.ink;\n\t\t\t\tresolveInk(0, true);\n\t\t\t\tif (!inkFollowsHost()) clearInkPoll();\n\t\t\t}\n\t\t\tif (next.paused !== undefined) paused = !!next.paused;\n\t\t\tif (next.reducedMotion !== undefined) rmOverride = next.reducedMotion;\n\t\t\tif (\"onFrame\" in next && next.onFrame !== undefined) onFrame = next.onFrame;\n\t\t\tif (relayout) layout();\n\t\t\twake();\n\t\t},\n\t\tresize() {\n\t\t\tif (destroyed) return;\n\t\t\tlayout();\n\t\t\tschedule();\n\t\t},\n\t\tdestroy() {\n\t\t\tif (destroyed) return;\n\t\t\tdestroyed = true;\n\t\t\tif (raf && typeof cancelAnimationFrame === \"function\") cancelAnimationFrame(raf);\n\t\t\traf = 0;\n\t\t\thost.removeEventListener(\"pointermove\", onMove);\n\t\t\thost.removeEventListener(\"pointerdown\", onMove);\n\t\t\thost.removeEventListener(\"pointerup\", onUp);\n\t\t\thost.removeEventListener(\"pointerleave\", onLeave);\n\t\t\thost.removeEventListener(\"pointercancel\", onLeave);\n\t\t\tro?.disconnect();\n\t\t\tio?.disconnect();\n\t\t\tif (typeof document !== \"undefined\")\n\t\t\t\tdocument.removeEventListener(\"visibilitychange\", onVisibility);\n\t\t\tmq?.removeEventListener?.(\"change\", onMq);\n\t\t\tthemeMo?.disconnect();\n\t\t\tschemeMq?.removeEventListener?.(\"change\", onScheme);\n\t\t\tdprMq?.removeEventListener?.(\"change\", onDpr);\n\t\t\tdprMq = null;\n\t\t\tclearInkPoll();\n\t\t\tonFrame = null;\n\t\t},\n\t};\n}\n","type":"registry:ui","target":"@ui/fancy/components/pixel-scene/pixel-scene-core.ts"},{"path":"react/src/components/pixel-scene/pixel-scene-hdr.ts","content":"\"use client\";\n\n/// <reference types=\"@webgpu/types\" />\n/**\n * PixelScene HDR layer — framework-free, byte-identical in the Svelte, React\n * and Vue trees (see shared-cores.json).\n *\n * The picture is painted in a 2D canvas, which stops at SDR white. This module\n * lays a WebGPU canvas over it (`rgba16float`, `colorSpace: \"srgb\"`,\n * `toneMapping: { mode: \"extended\" }`) and redraws every cell whose glow is\n * above zero as a hard square at `glow ×` its colour, alpha 1: one instanced\n * quad per cell, no blur, no falloff term in the shader. Halos are made of\n * cells by `pixelHalos` in the core.\n *\n * Encoding: the canvas reads its values as extended (gamma-encoded) sRGB, so\n * each channel is sent as `encodeExt(srgbToLinear(c) × glow)`, computed on\n * the CPU. A glow of exactly 1 reproduces the 2D dot.\n *\n * One GPUDevice per page, reference-counted across instances; each instance\n * owns its context, uniform, instance buffer and bind group. Nothing touches\n * `window` or `navigator` at import.\n */\n\nimport { encodeExt, srgbToLinear, type PixelSceneFrame } from \"./pixel-scene-core\";\n\n/** SDR white ≈ 203 nits; 1000 nits / 203 ≈ 4.9. Every glow is clamped to it. */\nexport const PIXEL_SCENE_HDR_PEAK = 4.9;\n\n/**\n * A lost device usually comes back (GPU process restarted, driver reset, tab\n * woken from a discarded state). The components start the layer again after\n * `PIXEL_SCENE_HDR_RESTART_MS × attempt`, up to `PIXEL_SCENE_HDR_RESTARTS`\n * times per mount, before dropping the HDR canvas for good.\n */\nexport const PIXEL_SCENE_HDR_RESTARTS = 3;\nexport const PIXEL_SCENE_HDR_RESTART_MS = 600;\n\n/** Floats per instance: `x0, y0, soft radius px, 0, r, g, b, 1`. */\nconst FPI = 8;\nconst START_CAPACITY = 4096;\nconst FORMAT = \"rgba16float\" as GPUTextureFormat;\n\nconst WGSL = /* wgsl */ `\nstruct Frame { size: vec2f, dot: f32, pad: f32 };\n@group(0) @binding(0) var<uniform> frame: Frame;\nstruct In { @location(0) cell: vec4f, @location(1) colour: vec4f };\nstruct Out { @builtin(position) pos: vec4f, @location(0) colour: vec4f };\nstruct Out2 { @builtin(position) pos: vec4f, @location(0) colour: vec4f, @location(1) p: vec2f, @location(2) cell: vec4f };\n@vertex fn vs(@builtin(vertex_index) vi: u32, inst: In) -> Out2 {\n\tlet corners = array<vec2f, 6>(vec2f(0, 0), vec2f(1, 0), vec2f(0, 1), vec2f(0, 1), vec2f(1, 0), vec2f(1, 1));\n\t// a soft cell (cell.z > 0, px) widens its quad to carry its halo\n\tlet reach = select(0.0, inst.cell.z * 4.0 + 1.0, inst.cell.z > 0.0);\n\tlet p = inst.cell.xy - vec2f(reach) + corners[vi] * (frame.dot + 2.0 * reach);\n\tvar o: Out2;\n\to.pos = vec4f(p.x / frame.size.x * 2.0 - 1.0, 1.0 - p.y / frame.size.y * 2.0, 0.0, 1.0);\n\to.colour = inst.colour;\n\to.p = p;\n\to.cell = inst.cell;\n\treturn o;\n}\n// a hard square at the pre-encoded colour; soft cells (house windows) also get\n// the first prototype's golden halo, exp(-d / radius) around the square\n@fragment fn fs(o: Out2) -> @location(0) vec4f {\n\tlet half = vec2f(frame.dot * 0.5);\n\tlet q = abs(o.p - (o.cell.xy + half)) - half;\n\tlet d = length(max(q, vec2f(0.0)));\n\tlet core = select(0.0, 1.0, d <= 0.001);\n\tlet h = select(0.0, exp(-d / max(o.cell.z, 0.001)) * 0.55, o.cell.z > 0.0);\n\tlet v = core + h * (1.0 - core);\n\treturn vec4f(o.colour.rgb * v, max(core, clamp(h * 0.25, 0.0, 1.0)));\n}\n`;\n\n/**\n * Sync and SSR-safe: WebGPU is exposed and the display reports\n * `(dynamic-range: high)`. Decides whether the HDR canvas is rendered at all.\n */\nexport function pixelSceneHdrAvailable(): boolean {\n\ttry {\n\t\tif (typeof navigator === \"undefined\" || typeof window === \"undefined\") return false;\n\t\tif (!(navigator as Navigator & { gpu?: GPU }).gpu) return false;\n\t\treturn (\n\t\t\ttypeof window.matchMedia === \"function\" && window.matchMedia(\"(dynamic-range: high)\").matches\n\t\t);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Two HDR looks, kept side by side to compare:\n * - `calibrated`: `colorSpace: \"srgb\"`, each channel sent as\n *   `encodeExt(linear(c) × glow)`, so a glow of 1 is the 2D dot exactly and\n *   a glow of 4.9 is 4.9 × SDR white;\n * - `vivid`: the prototype's transfer, `colorSpace: \"display-p3\"`, each channel\n *   sent as `linear(c) × g` with `g = 1 + (glow − 1) × VIVID_SCALE`. The canvas\n *   reads that as encoded, so lights land far hotter (lamps reach the panel\n *   peak) and colours more saturated; the scale brings the calibrated levels\n *   back to the prototype's.\n */\nexport type PixelSceneHdrLook = \"calibrated\" | \"vivid\";\nexport const PIXEL_SCENE_HDR_LOOKS: readonly PixelSceneHdrLook[] = [\"calibrated\", \"vivid\"];\n/** Maps the calibrated glow levels onto the prototype's (star 3.5 → 2.1, sun 4.9 → 2.8). */\nexport const VIVID_SCALE = 0.45;\n\n/** One channel (0–255) at `glow ×` SDR, encoded for the canvas of `look`. */\nexport function encodeGlowChannel(\n\tc8: number,\n\tglow: number,\n\tlook: PixelSceneHdrLook = \"calibrated\"\n): number {\n\tconst g = Math.min(PIXEL_SCENE_HDR_PEAK, Math.max(0, glow));\n\tif (look === \"vivid\") return srgbToLinear(c8) * (g > 0 ? 1 + (g - 1) * VIVID_SCALE : 0);\n\treturn encodeExt(srgbToLinear(c8) * g);\n}\n\nexport interface PixelSceneHdr {\n\t/** Draws the HDR squares for one painted frame. */\n\trender(frame: PixelSceneFrame): void;\n\tdestroy(): void;\n\t/** Resolves when the shared device is lost; the 2D picture carries on. */\n\treadonly lost: Promise<void>;\n\t/** Instances drawn by the last `render` (diagnostics, tests). */\n\treadonly count: number;\n}\n\n// --- one device per page -----------------------------------------------------\n\ninterface SharedDevice {\n\tdevice: Promise<GPUDevice | null>;\n\trefs: number;\n\tonLost: Set<() => void>;\n}\n\nlet shared: SharedDevice | null = null;\nconst PIPELINES = new WeakMap<GPUDevice, GPURenderPipeline>();\n\nfunction acquire(gpu: GPU): SharedDevice {\n\tif (!shared) {\n\t\tconst s: SharedDevice = { device: Promise.resolve(null), refs: 0, onLost: new Set() };\n\t\ts.device = (async () => {\n\t\t\tconst adapter = await gpu.requestAdapter();\n\t\t\tif (!adapter) return null;\n\t\t\tconst device = await adapter.requestDevice();\n\t\t\tvoid device.lost.then(() => {\n\t\t\t\tif (shared === s) shared = null;\n\t\t\t\tfor (const fn of [...s.onLost]) fn();\n\t\t\t\ts.onLost.clear();\n\t\t\t});\n\t\t\treturn device;\n\t\t})().catch(() => null);\n\t\tshared = s;\n\t}\n\tshared.refs++;\n\treturn shared;\n}\n\nfunction release(s: SharedDevice): void {\n\ts.refs--;\n\tif (s.refs > 0) return;\n\tif (shared === s) shared = null;\n\ts.onLost.clear();\n\tvoid s.device.then((d) => d?.destroy());\n}\n\nfunction pipelineFor(device: GPUDevice): GPURenderPipeline {\n\tlet p = PIPELINES.get(device);\n\tif (p) return p;\n\tconst module = device.createShaderModule({ code: WGSL });\n\tp = device.createRenderPipeline({\n\t\tlayout: \"auto\",\n\t\tvertex: {\n\t\t\tmodule,\n\t\t\tentryPoint: \"vs\",\n\t\t\tbuffers: [\n\t\t\t\t{\n\t\t\t\t\tarrayStride: FPI * 4,\n\t\t\t\t\tstepMode: \"instance\",\n\t\t\t\t\tattributes: [\n\t\t\t\t\t\t{ shaderLocation: 0, offset: 0, format: \"float32x4\" },\n\t\t\t\t\t\t{ shaderLocation: 1, offset: 16, format: \"float32x4\" },\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\tfragment: {\n\t\t\tmodule,\n\t\t\tentryPoint: \"fs\",\n\t\t\ttargets: [\n\t\t\t\t{\n\t\t\t\t\tformat: FORMAT,\n\t\t\t\t\tblend: {\n\t\t\t\t\t\tcolor: { srcFactor: \"one\", dstFactor: \"one\", operation: \"max\" },\n\t\t\t\t\t\talpha: { srcFactor: \"one\", dstFactor: \"one\", operation: \"max\" },\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\tprimitive: { topology: \"triangle-list\" },\n\t});\n\tPIPELINES.set(device, p);\n\treturn p;\n}\n\n/** Next power of two ≥ n, at least the start capacity. */\nexport function hdrCapacityFor(n: number): number {\n\tlet c = START_CAPACITY;\n\twhile (c < n) c *= 2;\n\treturn c;\n}\n\n/**\n * Starts the layer on `canvas`. Resolves null when WebGPU, the adapter, the\n * context or extended tone mapping is missing; never throws.\n */\nexport async function startPixelSceneHdr(\n\tcanvas: HTMLCanvasElement,\n\tlook: PixelSceneHdrLook = \"calibrated\"\n): Promise<PixelSceneHdr | null> {\n\tif (!pixelSceneHdrAvailable()) return null;\n\tconst gpu = (navigator as Navigator & { gpu?: GPU }).gpu;\n\tif (!gpu) return null;\n\tconst s = acquire(gpu);\n\tlet held = true;\n\tconst drop = () => {\n\t\tif (held) {\n\t\t\theld = false;\n\t\t\trelease(s);\n\t\t}\n\t};\n\ttry {\n\t\tconst device = await s.device;\n\t\tif (!device) {\n\t\t\tdrop();\n\t\t\treturn null;\n\t\t}\n\t\tconst context = canvas.getContext(\"webgpu\") as GPUCanvasContext | null;\n\t\tif (!context) {\n\t\t\tdrop();\n\t\t\treturn null;\n\t\t}\n\t\tcontext.configure({\n\t\t\tdevice,\n\t\t\tformat: FORMAT,\n\t\t\talphaMode: \"premultiplied\",\n\t\t\tcolorSpace: look === \"vivid\" ? \"display-p3\" : \"srgb\",\n\t\t\ttoneMapping: { mode: \"extended\" },\n\t\t} as GPUCanvasConfiguration);\n\t\tlet extended = false;\n\t\ttry {\n\t\t\tconst applied = context.getConfiguration?.() as\n\t\t\t\t| (GPUCanvasConfiguration & { toneMapping?: { mode?: string } })\n\t\t\t\t| null;\n\t\t\textended = applied?.toneMapping?.mode === \"extended\";\n\t\t} catch {\n\t\t\textended = false;\n\t\t}\n\t\tif (!extended) {\n\t\t\ttry {\n\t\t\t\tcontext.unconfigure();\n\t\t\t} catch {\n\t\t\t\t// nothing to undo\n\t\t\t}\n\t\t\tdrop();\n\t\t\treturn null;\n\t\t}\n\n\t\tconst pipeline = pipelineFor(device);\n\t\tconst uniform = device.createBuffer({\n\t\t\tsize: 16,\n\t\t\tusage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,\n\t\t});\n\t\tconst uniformData = new Float32Array(4);\n\t\tconst bindGroup = device.createBindGroup({\n\t\t\tlayout: pipeline.getBindGroupLayout(0),\n\t\t\tentries: [{ binding: 0, resource: { buffer: uniform } }],\n\t\t});\n\t\tlet capacity = START_CAPACITY;\n\t\tlet data = new Float32Array(capacity * FPI);\n\t\tlet instances = device.createBuffer({\n\t\t\tsize: data.byteLength,\n\t\t\tusage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,\n\t\t});\n\t\tlet destroyed = false;\n\t\tlet count = 0;\n\n\t\tlet resolveLost: () => void = () => {};\n\t\tconst lost = new Promise<void>((r) => {\n\t\t\tresolveLost = r;\n\t\t});\n\t\tconst onLost = () => {\n\t\t\tdestroyed = true;\n\t\t\tresolveLost();\n\t\t};\n\t\ts.onLost.add(onLost);\n\n\t\tconst layer: PixelSceneHdr = {\n\t\t\tget lost() {\n\t\t\t\treturn lost;\n\t\t\t},\n\t\t\tget count() {\n\t\t\t\treturn count;\n\t\t\t},\n\t\t\trender(frame) {\n\t\t\t\tif (destroyed || frame.width <= 0 || frame.height <= 0) return;\n\t\t\t\tif (canvas.width !== frame.width) canvas.width = frame.width;\n\t\t\t\tif (canvas.height !== frame.height) canvas.height = frame.height;\n\t\t\t\tconst rendering = frame.pixelated ? \"pixelated\" : \"\";\n\t\t\t\tif (canvas.style && canvas.style.imageRendering !== rendering)\n\t\t\t\t\tcanvas.style.imageRendering = rendering;\n\t\t\t\tconst { cols, rows, color, glow, hcol, cellCss, dpr } = frame;\n\t\t\t\tconst soft = frame.soft;\n\t\t\t\tconst n = cols * rows;\n\t\t\t\tlet k = 0;\n\t\t\t\tfor (let y = 0; y < rows; y++) {\n\t\t\t\t\tconst oy = Math.round(y * cellCss * dpr);\n\t\t\t\t\tfor (let x = 0; x < cols; x++) {\n\t\t\t\t\t\tconst i = y * cols + x;\n\t\t\t\t\t\tif (i >= n) break;\n\t\t\t\t\t\tconst g = glow[i]!;\n\t\t\t\t\t\tif (!(g > 0)) continue;\n\t\t\t\t\t\tconst h = hcol[i]!;\n\t\t\t\t\t\tconst c = h >= 0 ? h : color[i]!;\n\t\t\t\t\t\tif (c < 0) continue;\n\t\t\t\t\t\tif (k >= capacity) {\n\t\t\t\t\t\t\tcapacity = hdrCapacityFor(k + 1);\n\t\t\t\t\t\t\tconst next = new Float32Array(capacity * FPI);\n\t\t\t\t\t\t\tnext.set(data);\n\t\t\t\t\t\t\tdata = next;\n\t\t\t\t\t\t\tinstances.destroy();\n\t\t\t\t\t\t\tinstances = device.createBuffer({\n\t\t\t\t\t\t\t\tsize: data.byteLength,\n\t\t\t\t\t\t\t\tusage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst o = k * FPI;\n\t\t\t\t\t\tdata[o] = Math.round(x * cellCss * dpr);\n\t\t\t\t\t\tdata[o + 1] = oy;\n\t\t\t\t\t\tdata[o + 2] = soft ? soft[i]! * cellCss * dpr : 0;\n\t\t\t\t\t\tdata[o + 3] = 0;\n\t\t\t\t\t\tdata[o + 4] = encodeGlowChannel((c >> 16) & 255, g, look);\n\t\t\t\t\t\tdata[o + 5] = encodeGlowChannel((c >> 8) & 255, g, look);\n\t\t\t\t\t\tdata[o + 6] = encodeGlowChannel(c & 255, g, look);\n\t\t\t\t\t\tdata[o + 7] = 1;\n\t\t\t\t\t\tk++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcount = k;\n\t\t\t\tuniformData[0] = frame.width;\n\t\t\t\tuniformData[1] = frame.height;\n\t\t\t\tuniformData[2] = frame.size;\n\t\t\t\tuniformData[3] = 0;\n\t\t\t\ttry {\n\t\t\t\t\tdevice.queue.writeBuffer(uniform, 0, uniformData);\n\t\t\t\t\tif (k) device.queue.writeBuffer(instances, 0, data, 0, k * FPI);\n\t\t\t\t\tconst encoder = device.createCommandEncoder();\n\t\t\t\t\tconst pass = encoder.beginRenderPass({\n\t\t\t\t\t\tcolorAttachments: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tview: context.getCurrentTexture().createView(),\n\t\t\t\t\t\t\t\tclearValue: { r: 0, g: 0, b: 0, a: 0 },\n\t\t\t\t\t\t\t\tloadOp: \"clear\",\n\t\t\t\t\t\t\t\tstoreOp: \"store\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t});\n\t\t\t\t\tif (k) {\n\t\t\t\t\t\tpass.setPipeline(pipeline);\n\t\t\t\t\t\tpass.setBindGroup(0, bindGroup);\n\t\t\t\t\t\tpass.setVertexBuffer(0, instances);\n\t\t\t\t\t\tpass.draw(6, k);\n\t\t\t\t\t}\n\t\t\t\t\tpass.end();\n\t\t\t\t\tdevice.queue.submit([encoder.finish()]);\n\t\t\t\t} catch {\n\t\t\t\t\t// a lost or reconfigured context: the next frame tries again\n\t\t\t\t}\n\t\t\t},\n\t\t\tdestroy() {\n\t\t\t\tif (destroyed && !held) return;\n\t\t\t\tdestroyed = true;\n\t\t\t\ts.onLost.delete(onLost);\n\t\t\t\ttry {\n\t\t\t\t\tcontext.unconfigure();\n\t\t\t\t} catch {\n\t\t\t\t\t// already gone\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tuniform.destroy();\n\t\t\t\t\tinstances.destroy();\n\t\t\t\t} catch {\n\t\t\t\t\t// device already lost\n\t\t\t\t}\n\t\t\t\tdrop();\n\t\t\t},\n\t\t};\n\t\treturn layer;\n\t} catch {\n\t\tdrop();\n\t\treturn null;\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/pixel-scene/pixel-scene-hdr.ts"},{"path":"react/src/components/pixel-scene/pixel-scene-scenes.ts","content":"\"use client\";\n\n/**\n * PixelScene scenes — framework-free, byte-identical in the Svelte, React and\n * Vue trees (see shared-cores.json).\n *\n * Four painters (alpine, meadow, sea, mountains) with complete day and night\n * palettes, the sprites they use and the drawing helpers. A painter fills the\n * whole grid every frame through `put(x, y, colour, inkAlpha, lit, haloCells)`:\n *\n * - `colour` is what the lens shows;\n * - `inkAlpha` is the resting dot outside the lens (0 = none);\n * - `lit`: false = plain, true = shown in colour outside the lens too (HDR\n *   {@link LIT_GLOW}), a number = lit and HDR at that multiple of SDR white;\n * - `haloCells > 0` makes the cell an emitter: `pixelHalos` lights the cells\n *   around it as Bayer-dithered hard squares.\n *\n * Layout is by fractions of COLS / ROWS so a scene fits any box. Everything is\n * a square on the grid: no blur, no gradient between cells.\n *\n * Re-entrancy: the drawing target (`G`, `COLS`, `ROWS`, `NIGHT`) is module\n * state. Painting is synchronous and two instances paint one after the other\n * inside their own animation frames, so this is safe — never add an `await`\n * inside a painter. Per-size caches are keyed on ROWS / NIGHT.\n *\n * Compiles under `noUncheckedIndexedAccess`; the `!` assertions are in bounds\n * by construction.\n */\n\nimport {\n\tbayer,\n\tclamp,\n\tcreateGrid,\n\tgrad,\n\thash,\n\tmixHex,\n\tnoise1,\n\tparseColour,\n\tridge,\n\tsmooth,\n\twriteCell,\n\ttype Colour,\n\ttype PixelGrid,\n\ttype ScenePainter,\n} from \"./pixel-scene-core\";\n\nexport type PixelSceneName = \"alpine\" | \"meadow\" | \"sea\" | \"mountains\";\nexport const PIXEL_SCENES: readonly PixelSceneName[] = [\"alpine\", \"meadow\", \"sea\", \"mountains\"];\n\n// --- HDR levels (multiples of SDR white; the core clamps at 4.9) -------------\n\nconst G_STAR = 3.5;\nconst G_MOON = 3.5;\nconst G_SUN = 4.9;\n/** Windows, lantern, pier lamp, groomer headlight, cabin windows: 4.6–4.9 with flicker. */\nconst G_LAMP = 4.6;\nconst G_LAMP_FLICKER = 0.3;\nconst G_SNOWCAP = 1.8;\nconst G_FLOWER = 1.8;\nconst G_SPARKLE = 2.2;\nconst G_DAYLAMP = 1.6;\n/**\n * Soft golden halo radius (cells) around lit house windows at night, HDR only:\n * the one place a soft glow is allowed (owner ruling); everything else is squares.\n */\nconst SOFT_CABIN = 1.6;\nconst SOFT_CHALET = 1.2;\n\n/**\n * The copy corner every scene keeps open for text (kicker, title, paragraph),\n * as fractions of the grid from the top-left. Lens cells inside it stay at\n * SDR level so the text keeps its contrast; alpine also thins its ink there.\n */\nconst COPY_W = 0.42;\nconst COPY_H = 0.52;\n\n// --- drawing target ----------------------------------------------------------\n\nlet G: PixelGrid = createGrid(0, 0);\nlet COLS = 0;\nlet ROWS = 0;\nlet NIGHT = false;\n\nconst night = () => NIGHT;\nconst h = parseColour;\n\nfunction put(x: number, y: number, c: Colour, a = 0, l: boolean | number = false, hl = 0): void {\n\twriteCell(G, x, y, c, a, l, hl);\n}\n\n/** Gives a lit cell the soft golden house halo (radius in cells, HDR only). */\nfunction softGlow(x: number, y: number, r: number): void {\n\tx = Math.round(x);\n\ty = Math.round(y);\n\tif (x < 0 || y < 0 || x >= COLS || y >= ROWS) return;\n\tG.soft[y * COLS + x] = r;\n}\n\n/** Whether a cell already holds a colour. */\nfunction filled(x: number, y: number): boolean {\n\tx = Math.round(x);\n\ty = Math.round(y);\n\tif (x < 0 || y < 0 || x >= COLS || y >= ROWS) return false;\n\treturn G.color[y * COLS + x]! >= 0;\n}\n\n/** Whether a cell is shown in colour outside the lens. */\nfunction isLit(x: number, y: number): boolean {\n\tx = Math.round(x);\n\ty = Math.round(y);\n\tif (x < 0 || y < 0 || x >= COLS || y >= ROWS) return false;\n\treturn G.lit[y * COLS + x]! > 0;\n}\n\n/** Calls `fn` for every cell within `r` of `(cx, cy)`. */\nfunction disc(\n\tcx: number,\n\tcy: number,\n\tr: number,\n\tfn: (x: number, y: number, d: number) => void\n): void {\n\tfor (let y = Math.floor(cy - r); y <= Math.ceil(cy + r); y++)\n\t\tfor (let x = Math.floor(cx - r); x <= Math.ceil(cx + r); x++) {\n\t\t\tconst d = Math.hypot(x - cx, y - cy);\n\t\t\tif (d <= r + 0.001) fn(x, y, d);\n\t\t}\n}\n\n/** A sprite cell: a colour (dot alpha = the sprite's), or `[colour, inkAlpha, lit, haloCells]`. */\ntype SpriteCell = Colour | readonly [Colour, number, boolean | number, number?];\ntype SpriteMap = Record<string, SpriteCell | undefined>;\n\nfunction sprite(\n\trows: readonly string[],\n\tmap: SpriteMap,\n\tox: number,\n\toy: number,\n\ta = 0.9,\n\tflip = false\n): void {\n\tfor (let j = 0; j < rows.length; j++) {\n\t\tconst row = rows[j]!;\n\t\tfor (let i = 0; i < row.length; i++) {\n\t\t\tconst ch = row[i]!;\n\t\t\tif (ch === \".\") continue;\n\t\t\tconst c = map[ch];\n\t\t\tif (c === undefined) continue;\n\t\t\tconst x = ox + (flip ? row.length - 1 - i : i);\n\t\t\tif (typeof c === \"string\" || typeof c === \"number\") put(x, oy + j, c, a);\n\t\t\telse put(x, oy + j, c[0], c[1], c[2], c[3] ?? 0);\n\t\t}\n\t}\n}\n\n/** Lamp level with a slow per-light flicker in [G_LAMP, G_LAMP + 0.3]. */\nfunction lamp(t: number, seed: number): number {\n\treturn G_LAMP + G_LAMP_FLICKER * (0.5 + 0.5 * Math.sin(t * 0.006 + seed * 2.3));\n}\n\n// --- shared palettes ---------------------------------------------------------\n\nconst SUN = [h(\"#f7cf4a\"), h(\"#f6a24e\"), h(\"#ef7a5e\"), h(\"#ea7d9c\")] as const;\nconst SKY = {\n\tday: [\n\t\t[0, h(\"#79b6f2\")],\n\t\t[0.55, h(\"#bcdcf7\")],\n\t\t[1, h(\"#f6e6c9\")],\n\t],\n\tnight: [\n\t\t[0, h(\"#17143a\")],\n\t\t[0.35, h(\"#3a2a5e\")],\n\t\t[0.7, h(\"#8e4a6a\")],\n\t\t[1, h(\"#e39a5a\")],\n\t],\n} as const satisfies Record<string, readonly (readonly [number, number])[]>;\nconst GROUND = {\n\tday: [\n\t\t[0, h(\"#8fbf6e\")],\n\t\t[1, h(\"#4e7f45\")],\n\t],\n\tnight: [\n\t\t[0, h(\"#28384a\")],\n\t\t[1, h(\"#111a26\")],\n\t],\n} as const;\nconst WATER = { day: [h(\"#3f86d8\"), h(\"#9ed0f6\")], night: [h(\"#243a73\"), h(\"#7aa9d8\")] } as const;\nconst CLOUD = { day: h(\"#ffffff\"), night: h(\"#c98ea0\") } as const;\nconst BIRDC = { day: h(\"#2b2b2b\"), night: h(\"#b89ad8\") } as const;\nconst STAR = h(\"#fff3c4\");\nconst BLACK = 0x000000;\nconst WHITE = 0xffffff;\n\nconst mode = () => (NIGHT ? \"night\" : \"day\");\n\nfunction skyFill(horizon: number): void {\n\tconst stops = SKY[mode()];\n\tfor (let y = 0; y < horizon; y++) {\n\t\tconst c = grad(stops, y / horizon);\n\t\tfor (let x = 0; x < COLS; x++) put(x, y, c, 0);\n\t}\n}\n\nfunction stars(t: number, horizon: number): void {\n\tif (!night()) return;\n\tfor (let y = 0; y < horizon * 0.8; y++)\n\t\tfor (let x = 0; x < COLS; x++)\n\t\t\tif (hash(x, y, 41) > 0.988 && Math.sin(t * 0.003 + hash(x, y, 42) * 30) > 0.2)\n\t\t\t\tput(x, y, STAR, 0.6, G_STAR);\n}\n\nconst MOON_A = h(\"#f4ead0\");\nconst MOON_B = h(\"#c9b89a\");\nfunction sunDisc(sx: number, sy: number, R: number, cut = 1e9): void {\n\tG.sunX = sx + 0.5;\n\tG.sunY = sy + 0.5;\n\tdisc(sx, sy, R, (x, y) => {\n\t\tif (y >= cut) return;\n\t\tconst band = clamp(Math.floor(((y - (sy - R)) / (2 * R)) * 4), 0, 3);\n\t\tif (night()) put(x, y, mixHex(MOON_A, MOON_B, band / 3), 0.5, G_MOON, 0.6);\n\t\telse put(x, y, SUN[band]!, 0.9, G_SUN, 0.8);\n\t});\n}\n\nfunction clouds(t: number, list: readonly (readonly [number, number, number])[]): void {\n\tconst c = CLOUD[mode()];\n\tlist.forEach(([ox, cy, w], i) => {\n\t\tconst cx = ((ox * COLS + t * 0.0011 * (1 + i * 0.3)) % (COLS + 2 * w)) - w;\n\t\tfor (let y = -2; y <= 2; y++)\n\t\t\tfor (let x = -w; x <= w; x++) {\n\t\t\t\tconst e = (x * x) / (w * w) + (y * y) / 4.5;\n\t\t\t\tif (e < 1 && hash(Math.round(cx) + x, cy + y, 5 + i) > e * 0.8)\n\t\t\t\t\tput(cx + x, cy + y, c, 0.22);\n\t\t\t}\n\t});\n}\n\nfunction bird(x: number, y: number, phase: number, alpha = 0.8): void {\n\tconst c = BIRDC[mode()];\n\tconst f = Math.sin(phase);\n\tconst wing = f > 0.4 ? -1 : f < -0.4 ? 1 : 0;\n\tput(x, y, c, alpha);\n\tput(x - 1, y + wing, c, alpha);\n\tput(x + 1, y + wing, c, alpha);\n\tput(x - 2, y + wing * 2 || y, c, alpha * 0.8);\n\tput(x + 2, y + wing * 2 || y, c, alpha * 0.8);\n}\n\nfunction flock(t: number, n: number, baseY: number, speed = 0.009): void {\n\tfor (let i = 0; i < n; i++) {\n\t\tconst lead = ((t * speed + 40) % (COLS + 60)) - 30;\n\t\tbird(\n\t\t\tlead - i * 5 - (i % 2) * 2,\n\t\t\tbaseY + Math.abs(i - (n - 1) / 2) * 1.8 + Math.sin(t * 0.0025 + i * 0.8) * 1.2,\n\t\t\tt * 0.011 + i * 0.9\n\t\t);\n\t}\n}\n\n// --- sprites -----------------------------------------------------------------\n\nconst LANTERN = h(\"#ffd15c\");\nconst PERSON_MAP = (t: number): SpriteMap => ({\n\tk: \"#2a1c1c\",\n\ts: \"#f3cba6\",\n\tr: \"#e0433f\",\n\tb: \"#2f4b94\",\n\tn: \"#6b4a2b\",\n\tw: \"#f6f6f6\",\n\ty: [LANTERN, 0.9, lamp(t, 1), 1.6],\n\td: \"#a3673a\",\n\tD: \"#6f4322\",\n\tt: \"#e9a0a0\",\n});\nconst PERSON = [\n\t[\n\t\t\".kkk.\",\n\t\t\".kkk.\",\n\t\t\".sss.\",\n\t\t\"..s..\",\n\t\t\".rrr.\",\n\t\t\"rrrrr\",\n\t\t\"r.r.y\",\n\t\t\".bbb.\",\n\t\t\".b.b.\",\n\t\t\".b.b.\",\n\t\t\".n.n.\",\n\t],\n\t[\n\t\t\".kkk.\",\n\t\t\".kkk.\",\n\t\t\".sss.\",\n\t\t\"..s..\",\n\t\t\".rrr.\",\n\t\t\"rrrrr\",\n\t\t\"r.r.y\",\n\t\t\".bbb.\",\n\t\t\".b.b.\",\n\t\t\"b...b\",\n\t\t\"n...n\",\n\t],\n] as const;\nconst DOG = [\n\t[\"......dd\", \"..ddddd.\", \".dddddd.\", \"dd.d..d.\", \"d..d..d.\"],\n\t[\".....dd.\", \"..ddddd.\", \".dddddd.\", \"d..d..d.\", \"d..dd.d.\"],\n] as const;\nconst BOAT = [\n\t\"....w....\",\n\t\"...ww....\",\n\t\"..www....\",\n\t\".wwww....\",\n\t\"wwwww....\",\n\t\"....n....\",\n\t\"nnnnnnnnn\",\n\t\".nnnnnnn.\",\n];\nconst CABIN = [\"...n...\", \"..nnn..\", \".nnnnn.\", \"nnnnnnn\", \".DyDDD.\", \".DyDDD.\", \".DDDDD.\"];\n\n// --- 0. ALPINE ---------------------------------------------------------------\n// A snowy ridge in the top band, wide pistes with a lift on the left, autumn\n// tree belts, a chalet village bottom-right, dense forest along the bottom.\n// Slow life: cirrus drift, cabins on the cable, chimney smoke, a hawk by day,\n// a groomer headlight by night. No sun by day; a moon by night.\n\ninterface AlpinePalette {\n\tsky: readonly (readonly [number, number])[];\n\tcirrus: number;\n\trockLit: number;\n\trockShade: number;\n\tsnow: number;\n\tsnowShade: number;\n\tpiste: number;\n\tfold: number;\n\ttrack: number;\n\tshrub: number;\n\tconifer: readonly [number, number];\n\tautumn: readonly [number, number, number, number];\n\ttrunk: number;\n\troof: number;\n\troofShade: number;\n\teave: number;\n\twall: number;\n\twallD: number;\n\twin: number;\n\tlamp: number;\n\troad: number;\n\tstone: number;\n\tpylon: number;\n\tcable: number;\n\tforest: readonly [number, number, number, number];\n\tglint: number;\n\tgroomer: number;\n\tmoon: number;\n\tmoonRim: number;\n\tstar: number;\n\thawk: number;\n\tsmoke: number;\n\tcabin: readonly [number, number];\n\tcabinLamp: number;\n\tbeam: number;\n\tbeacon: number;\n\ttracks: number;\n}\n\nconst ALP: Record<\"day\" | \"night\", AlpinePalette> = {\n\tday: {\n\t\tsky: [\n\t\t\t[0, h(\"#4f93e6\")],\n\t\t\t[0.7, h(\"#86b8ef\")],\n\t\t\t[1, h(\"#a9cdf3\")],\n\t\t],\n\t\tcirrus: h(\"#ffffff\"),\n\t\trockLit: h(\"#6b7383\"),\n\t\trockShade: h(\"#454a58\"),\n\t\tsnow: h(\"#f3f6fb\"),\n\t\tsnowShade: h(\"#c6d4ea\"),\n\t\tpiste: h(\"#eef3f9\"),\n\t\tfold: h(\"#d6e2f1\"),\n\t\ttrack: h(\"#bccde6\"),\n\t\tshrub: h(\"#5b5a3e\"),\n\t\tconifer: [h(\"#2f5a3a\"), h(\"#1f3f2a\")],\n\t\tautumn: [h(\"#d9712a\"), h(\"#b5402a\"), h(\"#e0a33a\"), h(\"#8a9340\")],\n\t\ttrunk: h(\"#6e665e\"),\n\t\troof: h(\"#f6f8fc\"),\n\t\troofShade: h(\"#cfdbee\"),\n\t\teave: h(\"#2e2622\"),\n\t\twall: h(\"#7a4a2a\"),\n\t\twallD: h(\"#5a3520\"),\n\t\twin: h(\"#2b2420\"),\n\t\tlamp: h(\"#ffd15c\"),\n\t\troad: h(\"#8a8d93\"),\n\t\tstone: h(\"#a9a59c\"),\n\t\tpylon: h(\"#3a3d44\"),\n\t\tcable: h(\"#565a63\"),\n\t\tforest: [h(\"#2a4a33\"), h(\"#1f3a28\"), h(\"#8f9a3a\"), h(\"#c9a53a\")],\n\t\tglint: h(\"#ffffff\"),\n\t\tgroomer: h(\"#c0392b\"),\n\t\tmoon: h(\"#fff4d2\"),\n\t\tmoonRim: h(\"#d6ccb0\"),\n\t\tstar: h(\"#fff3c4\"),\n\t\thawk: h(\"#3a2e28\"),\n\t\tsmoke: h(\"#a3adbd\"),\n\t\tcabin: [h(\"#c8402e\"), h(\"#e0a33a\")],\n\t\tcabinLamp: h(\"#ffcf7a\"),\n\t\tbeam: h(\"#e9eeff\"),\n\t\tbeacon: h(\"#ffa040\"),\n\t\ttracks: h(\"#14161f\"),\n\t},\n\tnight: {\n\t\tsky: [\n\t\t\t[0, h(\"#090b26\")],\n\t\t\t[0.55, h(\"#151b4a\")],\n\t\t\t[1, h(\"#2c3772\")],\n\t\t],\n\t\tcirrus: h(\"#58629a\"),\n\t\trockLit: h(\"#3c4668\"),\n\t\trockShade: h(\"#20263f\"),\n\t\tsnow: h(\"#cad6f2\"),\n\t\tsnowShade: h(\"#7484b4\"),\n\t\tpiste: h(\"#9eacd4\"),\n\t\tfold: h(\"#7c8bbb\"),\n\t\ttrack: h(\"#66759f\"),\n\t\tshrub: h(\"#1c2030\"),\n\t\tconifer: [h(\"#17301f\"), h(\"#0d1f15\")],\n\t\tautumn: [h(\"#6e4a3a\"), h(\"#5a3236\"), h(\"#7a6844\"), h(\"#4a5238\")],\n\t\ttrunk: h(\"#3a3a48\"),\n\t\troof: h(\"#c6d3f1\"),\n\t\troofShade: h(\"#8f9fcc\"),\n\t\teave: h(\"#120f16\"),\n\t\twall: h(\"#3b2a24\"),\n\t\twallD: h(\"#291d19\"),\n\t\twin: h(\"#17121a\"),\n\t\tlamp: h(\"#ffc650\"),\n\t\troad: h(\"#3a3f52\"),\n\t\tstone: h(\"#4b5068\"),\n\t\tpylon: h(\"#090b14\"),\n\t\tcable: h(\"#2a2f45\"),\n\t\tforest: [h(\"#0f2018\"), h(\"#0a1711\"), h(\"#2e3a24\"), h(\"#4a4428\")],\n\t\tglint: h(\"#e6eeff\"),\n\t\tgroomer: h(\"#3a2a2a\"),\n\t\tmoon: h(\"#f1ecd8\"),\n\t\tmoonRim: h(\"#d6ccb0\"),\n\t\tstar: h(\"#fff3c4\"),\n\t\thawk: h(\"#b89ad8\"),\n\t\tsmoke: h(\"#6f78a0\"),\n\t\tcabin: [h(\"#4a2a2a\"), h(\"#5a4a2a\")],\n\t\tcabinLamp: h(\"#ffcf7a\"),\n\t\tbeam: h(\"#e9eeff\"),\n\t\tbeacon: h(\"#ffa040\"),\n\t\ttracks: h(\"#14161f\"),\n\t},\n};\n\nconst ALP_DARK = new Map<number, number>();\nfunction darker(c: number): number {\n\tlet v = ALP_DARK.get(c);\n\tif (v === undefined) {\n\t\tv = mixHex(c, BLACK, 0.28);\n\t\tALP_DARK.set(c, v);\n\t}\n\treturn v;\n}\n/** Terrain lines, keyed on the grid size: they depend on neither time nor theme. */\nlet ALP_TERRAIN: {\n\tkey: string;\n\tcrest: Float32Array;\n\trockBase: Float32Array;\n\tforestTop: Float32Array;\n} = {\n\tkey: \"\",\n\tcrest: new Float32Array(0),\n\trockBase: new Float32Array(0),\n\tforestTop: new Float32Array(0),\n};\n\nfunction alpineTerrain(C: number, R: number): typeof ALP_TERRAIN {\n\tconst key = C + \"x\" + R;\n\tif (ALP_TERRAIN.key === key) return ALP_TERRAIN;\n\tconst crest = new Float32Array(C);\n\tconst rockBase = new Float32Array(C);\n\tconst forestTop = new Float32Array(C);\n\tfor (let x = 0; x < C; x++) {\n\t\tconst u = x / C;\n\t\tconst base = R * (0.52 - 0.33 * smooth((u - 0.16) / 0.5) + 0.1 * smooth((u - 0.74) / 0.26));\n\t\tconst peaks =\n\t\t\tR * 0.07 * Math.max(0, 1 - Math.abs(u - 0.64) / 0.05) +\n\t\t\tR * 0.05 * Math.max(0, 1 - Math.abs(u - 0.5) / 0.04) +\n\t\t\tR * 0.04 * Math.max(0, 1 - Math.abs(u - 0.82) / 0.035);\n\t\tconst cr =\n\t\t\tbase -\n\t\t\tpeaks +\n\t\t\t(ridge(x, 71, 8, R * 0.06) - R * 0.04) +\n\t\t\t(noise1(x / 1.6, 73) - 0.5) * R * 0.035;\n\t\tcrest[x] = cr;\n\t\trockBase[x] =\n\t\t\tMath.max(cr + 3, Math.min(R * 0.56, cr + R * (0.17 + noise1(x / 6, 78) * 0.1))) +\n\t\t\t(noise1(x / 7, 77) - 0.5) * R * 0.06;\n\t\t// the lower-left edge stays low so the lift slope keeps its snow\n\t\tforestTop[x] =\n\t\t\tR * (0.92 - 0.015 * (1 - smooth(u / 0.32)) - 0.15 * smooth((u - 0.86) / 0.14)) -\n\t\t\tnoise1(x / 4, 51) * R * 0.04;\n\t}\n\tALP_TERRAIN = { key, crest, rockBase, forestTop };\n\treturn ALP_TERRAIN;\n}\n\n/** Sky row colours, keyed on theme and row count (two instances may differ). */\nlet ALP_SKY: { key: string; rows: Int32Array } = { key: \"\", rows: new Int32Array(0) };\n\n/** 0..1..0 triangle wave. */\nconst tri = (v: number) => {\n\tconst f = v - Math.floor(v);\n\treturn 1 - Math.abs(f * 2 - 1);\n};\n\n/** Cirrus streaks: `[x origin as a fraction of the span, y as a fraction of ROWS, length]`. */\nconst CIRRUS = [\n\t[0.08, 0.1, 30],\n\t[0.34, 0.2, 22],\n\t[0.55, 0.06, 34],\n\t[0.78, 0.16, 26],\n\t[0.96, 0.27, 18],\n] as const;\n\n/** Tree belts: `[u, v, radius u, radius v, weight]`. */\nconst BELTS = [\n\t[0.2, 0.7, 0.08, 0.11, 0.95],\n\t[0.72, 0.62, 0.12, 0.07, 0.95],\n\t[0.43, 0.65, 0.04, 0.04, 0.75],\n\t[0.92, 0.63, 0.05, 0.05, 0.7],\n\t[0.56, 0.6, 0.05, 0.035, 0.65],\n\t[0.33, 0.78, 0.05, 0.04, 0.6],\n] as const;\n\nconst VILLAGE_ROWS = [\n\t[0.745, 0.585, 0.85],\n\t[0.805, 0.57, 0.86],\n\t[0.865, 0.6, 0.84],\n] as const;\n\nconst HAWK_SHAPE = [\n\t[-3, -1, 2],\n\t[-2, -1, 1],\n\t[-1, 0, 0],\n\t[0, 0, 0],\n\t[1, 0, 0],\n\t[2, -1, 1],\n\t[3, -1, 2],\n] as const;\n\nfunction alpine(t: number): void {\n\tconst N = night();\n\tconst P = ALP[N ? \"night\" : \"day\"];\n\tconst C = COLS;\n\tconst R = ROWS;\n\t// the copy corner (kicker + H1 + paragraph): ink thinned, no snow-cap HDR\n\tconst copyW = C * COPY_W;\n\tconst copyH = R * COPY_H;\n\tconst inCopy = (x: number, y: number) => x < copyW && y < copyH;\n\n\t// ---- terrain lines (by fractions of the grid, cached per size) ----\n\tconst { crest, rockBase, forestTop } = alpineTerrain(C, R);\n\tconst rockAt = (x: number) => rockBase[clamp(Math.round(x), 0, C - 1)]!;\n\n\t// ---- sky (row colours cached per theme and size) ----\n\tconst skyH = Math.max(1, Math.floor(R * 0.58));\n\tconst key = (N ? \"n\" : \"d\") + R;\n\tif (ALP_SKY.key !== key) {\n\t\tconst rows = new Int32Array(R);\n\t\tfor (let y = 0; y < R; y++) rows[y] = grad(P.sky, y / skyH);\n\t\tALP_SKY = { key, rows };\n\t}\n\tfor (let y = 0; y < R; y++) {\n\t\tconst c = ALP_SKY.rows[y]!;\n\t\tfor (let x = 0; x < C; x++) if (y < crest[x]! + 1) put(x, y, c, 0);\n\t}\n\tif (N) {\n\t\tfor (let y = 0; y < skyH; y++)\n\t\t\tfor (let x = 0; x < C; x++) {\n\t\t\t\tif (y >= crest[x]! - 1) continue;\n\t\t\t\tif (hash(x, y, 41) < (inCopy(x, y) ? 0.995 : 0.986)) continue;\n\t\t\t\tif (Math.sin(t * 0.003 + hash(x, y, 42) * 30) > 0.15) put(x, y, P.star, 0.55, G_STAR);\n\t\t\t}\n\t\t// moon: hard crescent disc, HDR emitter\n\t\tconst mx = Math.floor(C * 0.9);\n\t\tconst my = Math.max(4, Math.floor(R * 0.13));\n\t\tconst mr = 3.2;\n\t\tG.sunX = mx + 0.5;\n\t\tG.sunY = my + 0.5;\n\t\tdisc(mx, my, mr, (x, y, dd) => {\n\t\t\tif (x - mx > mr * 0.25 && dd > mr * 0.55) return;\n\t\t\tput(x, y, dd > mr * 0.7 ? P.moonRim : P.moon, 0.6, G_MOON, 0.6);\n\t\t});\n\t}\n\n\t// ---- cirrus: thin diagonal streaks drifting right very slowly ----\n\tCIRRUS.forEach(([ox, oy, len], k) => {\n\t\tconst span = C + 2 * len;\n\t\tconst x0 = ((ox * span + t * 0.0005 * (1 + k * 0.15)) % span) - len;\n\t\tconst y0 = R * oy;\n\t\tfor (let i = 0; i < len; i++) {\n\t\t\tconst x = Math.round(x0 + i);\n\t\t\tconst y = Math.round(y0 - i * 0.22 + Math.sin(i * 0.25 + k) * 0.6);\n\t\t\tif (x < 0 || x >= C || y < 0 || y >= crest[x]! - 1) continue;\n\t\t\tconst edge = Math.min(i, len - 1 - i) / (len * 0.5);\n\t\t\tif (hash(i, k, 520) > edge * 1.4) continue;\n\t\t\tput(x, y, P.cirrus, hash(x, y, 521) > 0.45 ? (N ? 0.12 : 0.2) : 0);\n\t\t\tif (edge > 0.5 && hash(i, k, 522) > 0.6 && x + 1 < C && y - 1 < crest[x + 1]! - 1)\n\t\t\t\tput(x + 1, y - 1, P.cirrus, 0);\n\t\t}\n\t});\n\n\t// ---- ridge: rock and snow in diagonal stripes and couloirs ----\n\tfor (let x = 0; x < C; x++) {\n\t\tconst top = crest[x]!;\n\t\tconst bottom = rockBase[x]!;\n\t\tconst band = bottom - top;\n\t\tfor (let y = Math.max(0, Math.floor(top)); y < bottom && y < R; y++) {\n\t\t\tconst fromTop = y - top;\n\t\t\tconst depth = fromTop / band;\n\t\t\tconst face = noise1((x - y * 0.6) / 5, 95) > 0.42;\n\t\t\tconst stripe = noise1((x + y * 1.4) / 2.6, 91);\n\t\t\tconst couloir = noise1(x / 1.7, 93);\n\t\t\tconst cap = fromTop < 2 && bayer(x, y) < 0.8 - fromTop * 0.3;\n\t\t\tconst snowy = cap || stripe > 0.64 - depth * 0.32 || couloir > 0.8;\n\t\t\tconst c = snowy ? (face ? P.snow : P.snowShade) : face ? P.rockLit : P.rockShade;\n\t\t\tlet a = 0;\n\t\t\tif (fromTop < 1) a = 0.8;\n\t\t\telse if (!snowy) a = bayer(x, y) < (face ? 0.2 : 0.42) ? (face ? 0.38 : 0.52) : 0;\n\t\t\telse if (!face) a = bayer(x, y) < 0.1 ? 0.22 : 0;\n\t\t\tlet l: boolean | number = false;\n\t\t\tif (cap && !inCopy(x, y) && bayer(x, y) < 0.55 - fromTop * 0.25) l = G_SNOWCAP;\n\t\t\telse if (N && snowy && face && hash(x, y, 96) > 0.965) l = true;\n\t\t\tput(x, y, c, a, l);\n\t\t}\n\t}\n\n\t// ---- pistes: soft white, pale-blue folds, curving tracks, glints, shrubs ----\n\tfor (let x = 0; x < C; x++) {\n\t\tconst y0 = Math.ceil(rockBase[x]!);\n\t\tconst bend = noise1(x / 14, 61) * 12;\n\t\tfor (let y = y0; y < R; y++) {\n\t\t\tconst fold = noise1((x * 0.6 + y * 2.2 + bend) / 6, 63) > 0.7;\n\t\t\tconst c = y - y0 < 2 ? P.snowShade : fold ? P.fold : P.piste;\n\t\t\tlet a = 0;\n\t\t\tif (y === y0) a = bayer(x, y) < 0.6 ? 0.4 : 0;\n\t\t\telse if (hash(x, y, 64) > 0.984) a = 0.16;\n\t\t\tput(x, y, c, a);\n\t\t\tif (hash(x, y, 68) > 0.994 && y > y0 + 2) put(x, y, P.shrub, 0.45);\n\t\t}\n\t}\n\tconst trackY = (k: number, x: number) =>\n\t\tR * (0.63 + k * 0.075) + Math.sin(x * 0.045 + k * 2.1) * R * 0.035;\n\tfor (let k = 0; k < 3; k++)\n\t\tfor (let x = Math.floor(C * (0.3 + k * 0.05)); x < C * 0.98; x++)\n\t\t\tif (hash(x, k, 65) > 0.22) put(x, trackY(k, x), P.track, 0.2);\n\tfor (let y = 0; y < R; y++)\n\t\tfor (let x = 0; x < C; x++) {\n\t\t\tif (y <= rockBase[x]! + 1 || hash(x, y, 66) < 0.9965) continue;\n\t\t\tif (Math.sin(t * 0.0035 + hash(x, y, 67) * 40) > 0.9) put(x, y, P.glint, 0.2, G_SPARKLE);\n\t\t}\n\n\t// ---- lift line (left slope): the corridor through the trees follows it ----\n\tconst L0x = C * 0.05;\n\tconst L0y = R * 0.87;\n\tconst L1x = C * 0.33;\n\tconst L1y = R * 0.58;\n\t/** In the cut corridor: along the line, from 4 cells below the ground to 11 above it. */\n\tconst inCorridor = (x: number, y: number) => {\n\t\tconst f = (x - L0x) / (L1x - L0x);\n\t\tif (f < -0.04 || f > 1.04) return false;\n\t\tconst dy = y - (L0y + (L1y - L0y) * clamp(f, 0, 1));\n\t\treturn dy > -11 && dy < 4;\n\t};\n\n\t// ---- tree belts: conifers and autumn canopies, drawn back to front ----\n\tconst conifer = (x: number, by: number, hgt: number, side: number) => {\n\t\tfor (let k = 0; k < hgt; k++) {\n\t\t\tconst half = Math.floor(k * 0.45);\n\t\t\tfor (let dx = -half; dx <= half; dx++)\n\t\t\t\tput(\n\t\t\t\t\tx + dx,\n\t\t\t\t\tby - hgt + k,\n\t\t\t\t\tdx * side > 0 ? P.conifer[0] : P.conifer[1],\n\t\t\t\t\tk === 0 || Math.abs(dx) === half ? 0.8 : 0.6\n\t\t\t\t);\n\t\t}\n\t\tput(x, by, P.trunk, 0.55);\n\t};\n\tconst canopy = (x: number, by: number, r: number, col: number, seed: number) => {\n\t\tconst cy = by - r - 1;\n\t\tdisc(x, cy, r, (px, py, dd) => {\n\t\t\tif (hash(px, py, seed) > (dd / r) * 0.55 + 0.05)\n\t\t\t\tput(px, py, px < x && py > cy - 0.5 ? darker(col) : col, dd > r - 1 ? 0.75 : 0.55);\n\t\t});\n\t\tput(x, by, P.trunk, 0.6);\n\t\tput(x, by - 1, P.trunk, 0.6);\n\t};\n\tfor (let gy = 0; gy < R; gy += 3)\n\t\tfor (let gx = 0; gx < C; gx += 3) {\n\t\t\tconst x = gx + Math.floor(hash(gx, gy, 101) * 3);\n\t\t\tconst y = gy + Math.floor(hash(gx, gy, 102) * 3);\n\t\t\tif (y < rockAt(Math.min(C - 1, x)) + 4 || y > R * 0.86) continue;\n\t\t\tif (inCorridor(x, y)) continue;\n\t\t\tconst u = x / C;\n\t\t\tconst v = y / R;\n\t\t\tlet p = 0.015;\n\t\t\tfor (const [bu, bv, ru, rv, w] of BELTS) {\n\t\t\t\tconst e = ((u - bu) / ru) ** 2 + ((v - bv) / rv) ** 2;\n\t\t\t\tif (e < 1) p = Math.max(p, w * 0.8 * (1 - e * 0.8));\n\t\t\t}\n\t\t\tif (hash(x, y, 103) > p) continue;\n\t\t\tconst kind = hash(x, y, 104);\n\t\t\tif (kind < 0.45) conifer(x, y, 4 + Math.floor(hash(x, y, 105) * 4), 1);\n\t\t\telse if (kind < 0.94)\n\t\t\t\tcanopy(x, y, 1.5 + hash(x, y, 106) * 1.5, P.autumn[Math.floor(hash(x, y, 107) * 4)]!, 108);\n\t\t\telse\n\t\t\t\tfor (let k = 0; k < 4; k++) {\n\t\t\t\t\tput(x, y - k, P.trunk, 0.5);\n\t\t\t\t\tif (k === 2) {\n\t\t\t\t\t\tput(x - 1, y - 3, P.trunk, 0.4);\n\t\t\t\t\t\tput(x + 1, y - 3, P.trunk, 0.4);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t}\n\n\t// ---- lift: pylons 1 cell wide, two sagging cables, cabins up and down ----\n\tconst tops: [number, number][] = [];\n\tfor (let k = 0; k <= 5; k++) {\n\t\tconst s = k / 5;\n\t\tconst px = Math.round(L0x + (L1x - L0x) * s);\n\t\tconst gy = Math.round(L0y + (L1y - L0y) * s);\n\t\tconst hgt = 6 + Math.floor(hash(k, 0, 111) * 4);\n\t\tfor (let j = 0; j < hgt; j++) put(px, gy - j, P.pylon, 0.75);\n\t\tput(px - 1, gy - hgt, P.pylon, 0.75);\n\t\tput(px + 1, gy - hgt, P.pylon, 0.75);\n\t\ttops.push([px, gy - hgt]);\n\t}\n\tconst first = tops[0]!;\n\tconst lastTop = tops[tops.length - 1]!;\n\tconst cableAt = (x: number): number | null => {\n\t\tfor (let k = 0; k < tops.length - 1; k++) {\n\t\t\tconst [ax, ay] = tops[k]!;\n\t\t\tconst [bx, by] = tops[k + 1]!;\n\t\t\tif (x >= ax && x <= bx) {\n\t\t\t\tconst f = (x - ax) / (bx - ax || 1);\n\t\t\t\treturn ay + (by - ay) * f + Math.sin(Math.PI * f) * 1.2;\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t};\n\tfor (let x = first[0]; x <= lastTop[0]; x++) {\n\t\tconst cy = cableAt(x);\n\t\tif (cy === null) continue;\n\t\tput(x - 1, cy, P.cable, 0.35);\n\t\tput(x + 1, cy, P.cable, 0.35);\n\t}\n\t// cabins: about 110 s per trip, five per cable, a warm window at night\n\tfor (let k = 0; k < 5; k++)\n\t\tfor (const dir of [1, -1]) {\n\t\t\tconst ph = (t / 110000 + k / 5 + (dir < 0 ? 0.1 : 0)) % 1;\n\t\t\tconst s = dir > 0 ? ph : 1 - ph;\n\t\t\tif (s < 0.03 || s > 0.97) continue;\n\t\t\tconst off = dir > 0 ? 1 : -1;\n\t\t\tconst cxm = Math.round(first[0] + (lastTop[0] - first[0]) * s);\n\t\t\tconst cy = cableAt(cxm);\n\t\t\tif (cy === null) continue;\n\t\t\tconst x = cxm + off;\n\t\t\tconst y = Math.round(cy);\n\t\t\tconst col = P.cabin[k % 2]!;\n\t\t\tput(x, y + 1, P.cable, 0.5);\n\t\t\tput(x, y + 2, col, 0.85);\n\t\t\tput(x + 1, y + 2, col, 0.85);\n\t\t\tput(x, y + 3, col, 0.85);\n\t\t\tput(x + 1, y + 3, col, 0.85);\n\t\t\tif (N)\n\t\t\t\tput(x + (dir > 0 ? 1 : 0), y + 2, P.cabinLamp, 0.9, lamp(t, k * 2 + (dir > 0 ? 0 : 1)));\n\t\t}\n\n\t// ---- village: snowy gable roofs, timber walls, windows (warm ones are HDR emitters) ----\n\tconst winLit = N ? 0.4 : 0.12;\n\tconst smokers: [number, number][] = [];\n\tconst chalet = (x: number, by: number, w: number, storeys: number, seed: number, row: number) => {\n\t\tconst rh = (w + 1) >> 1;\n\t\tconst wallH = storeys * 2;\n\t\tconst roofY = by - wallH - rh;\n\t\tfor (let r = 0; r < rh; r++) {\n\t\t\tconst a = rh - 1 - r;\n\t\t\tfor (let dx = a; dx <= w - 1 - a; dx++) {\n\t\t\t\tconst edge = dx === a || dx === w - 1 - a;\n\t\t\t\tput(\n\t\t\t\t\tx + dx,\n\t\t\t\t\troofY + r,\n\t\t\t\t\tdx < w / 2 - 0.5 ? P.roofShade : P.roof,\n\t\t\t\t\tedge ? 0.8 : 0,\n\t\t\t\t\tedge && N && bayer(x + dx, roofY + r) < 0.5\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tfor (let dx = -1; dx <= w; dx++) put(x + dx, by - wallH, P.eave, 0.85);\n\t\tfor (let j = 1; j < wallH; j++)\n\t\t\tfor (let dx = 0; dx < w; dx++) {\n\t\t\t\tconst wx = x + dx;\n\t\t\t\tconst wy = by - wallH + j;\n\t\t\t\tconst isWin = dx % 2 === 1 && dx < w - 1 && j % 2 === 1;\n\t\t\t\tif (isWin) {\n\t\t\t\t\tconst hw = hash(wx, wy, seed);\n\t\t\t\t\t// a few windows switch on and off slowly, the rest are steady\n\t\t\t\t\tconst breathe = hash(wx, wy, seed + 2) > 0.9 ? Math.sin(t * 0.00031 + hw * 50) > 0 : true;\n\t\t\t\t\tif (breathe && hw < winLit) {\n\t\t\t\t\t\tif (N) {\n\t\t\t\t\t\t\tput(wx, wy, P.lamp, 0.9, lamp(t, wx + wy * 7), 0.9);\n\t\t\t\t\t\t\tsoftGlow(wx, wy, SOFT_CHALET);\n\t\t\t\t\t\t} else put(wx, wy, P.lamp, 0.9, G_DAYLAMP);\n\t\t\t\t\t} else put(wx, wy, P.win, 0.6);\n\t\t\t\t} else if (storeys > 1 && j % 2 === 0)\n\t\t\t\t\tput(wx, wy, P.eave, 0.7); // balcony line\n\t\t\t\telse put(wx, wy, dx % 2 ? P.wallD : P.wall, bayer(wx, wy) < 0.3 ? 0.5 : 0);\n\t\t\t}\n\t\tfor (let dx = 0; dx < w; dx++) put(x + dx, by, P.wallD, 0.6);\n\t\tif (row >= 0 && w >= 5 && smokers.length < 6 && hash(x, row, 904) > 0.62)\n\t\t\tsmokers.push([x + w - 3, roofY + Math.max(0, rh - 3) - 1]);\n\t};\n\tchalet(Math.floor(C * 0.5), Math.floor(R * 0.8), 15, 3, 120, -1); // the big timber residence\n\tVILLAGE_ROWS.forEach(([v, u0, u1], row) => {\n\t\tfor (let x = Math.floor(C * u0) + row * 3; x < C * u1; ) {\n\t\t\tconst w = [5, 7, 7, 9][Math.floor(hash(x, row, 121) * 4)]!;\n\t\t\tif (hash(x, row, 122) > 0.12)\n\t\t\t\tchalet(x, Math.floor(R * v) + Math.floor(hash(x, row, 123) * 2), w, 1, 124 + row, row);\n\t\t\tx += w + 2 + Math.floor(hash(x, row, 125) * 3);\n\t\t}\n\t});\n\t// chimney smoke: puffs rise and lean with the wind, thinning out as they go\n\tsmokers.forEach(([sx, sy], k) => {\n\t\tput(sx, sy, P.eave, 0.9);\n\t\tput(sx, sy - 1, P.eave, 0.9);\n\t\tfor (let i = 0; i < 6; i++) {\n\t\t\tconst life = (t * 0.00025 + i / 6 + k * 0.29) % 1;\n\t\t\tput(\n\t\t\t\tsx + life * 5 + Math.sin(life * 6 + i + k) * 0.8,\n\t\t\t\tsy - 2 - life * 9,\n\t\t\t\tP.smoke,\n\t\t\t\t(N ? 0.3 : 0.42) * (1 - life)\n\t\t\t);\n\t\t}\n\t});\n\t// road curving in from the bottom-left with a low wall\n\tfor (let i = 0; i <= 100; i++) {\n\t\tconst s = i / 100;\n\t\tconst x = C * 0.4 + C * 0.12 * s * s + C * 0.02 * s;\n\t\tconst y = R - R * 0.12 * s - R * 0.02;\n\t\tput(x, y, P.road, 0.3);\n\t\tput(x + 1, y, P.road, 0.3);\n\t\tif (i % 2 === 0) put(x - 1, y, P.stone, 0.5);\n\t}\n\n\t// ---- forest along the bottom: dense conifers with yellowing larches ----\n\tfor (let x = 0; x < C; x++) {\n\t\tconst top = Math.round(forestTop[x]!);\n\t\tfor (let y = top; y < R; y++) {\n\t\t\tif (inCorridor(x, y)) continue;\n\t\t\tconst hv = hash(x, y, 131);\n\t\t\tconst c =\n\t\t\t\thv < 0.18 ? P.forest[2] : hv < 0.27 ? P.forest[3] : y - top > 2 ? P.forest[1] : P.forest[0];\n\t\t\tput(x, y, c, y === top ? 0.8 : bayer(x, y) < 0.3 ? 0.45 : 0);\n\t\t}\n\t}\n\tfor (let x = 1; x < C; x += 3 + Math.floor(hash(x, 0, 132) * 2)) {\n\t\tconst hgt = 3 + Math.floor(hash(x, 1, 133) * 5);\n\t\tconst by = Math.round(forestTop[x]!);\n\t\tif (inCorridor(x, by) || inCorridor(x, by - hgt)) continue;\n\t\tconst larch = hash(x, 2, 134) < 0.25;\n\t\tfor (let k = 0; k < hgt; k++) {\n\t\t\tconst half = Math.floor(k * 0.4);\n\t\t\tfor (let dx = -half; dx <= half; dx++)\n\t\t\t\tput(\n\t\t\t\t\tx + dx,\n\t\t\t\t\tby - hgt + k,\n\t\t\t\t\tlarch ? P.forest[2] : dx > 0 ? P.forest[0] : P.forest[1],\n\t\t\t\t\tk === 0 || Math.abs(dx) === half ? 0.8 : 0.6\n\t\t\t\t);\n\t\t}\n\t}\n\n\t// ---- life ----\n\tif (!N) {\n\t\t// a hawk circling slowly over the ridge, gliding, flapping in short bursts\n\t\tconst a = t * 0.00022;\n\t\tconst hx = C * 0.62 + Math.cos(a) * 22;\n\t\tconst hy = R * 0.24 + Math.sin(a) * 4;\n\t\tconst flap = t % 9000 < 1400 ? Math.sin(t * 0.012) : 0;\n\t\tconst wy = flap > 0.3 ? -1 : flap < -0.3 ? 1 : 0;\n\t\tfor (const [dx, dy, wk] of HAWK_SHAPE) put(hx + dx, hy + dy + wy * wk, P.hawk, 0.8);\n\t\tput(hx - Math.sign(-Math.sin(a)), hy + 1, P.hawk, 0.6);\n\t} else {\n\t\t// a groomer working a traverse: headlight and a dithered beam (HDR cells), a blinking beacon\n\t\tconst ph = tri(t * 0.00002);\n\t\tconst dir = (t * 0.00002) % 1 < 0.5 ? 1 : -1;\n\t\tconst gx = Math.round(C * 0.45 + ph * C * 0.17);\n\t\tconst gy = Math.round(Math.max(rockAt(gx) + 3, R * 0.55) + Math.sin(gx * 0.08));\n\t\tfor (let d = 3; d < 34; d++) {\n\t\t\tconst x = gx - dir * d;\n\t\t\tif (hash(x, gy, 950) > 0.15) put(x, gy + 1, (x + gy) % 2 ? P.fold : P.track, 0);\n\t\t}\n\t\tfor (let i = -2; i <= 1; i++) {\n\t\t\tput(gx + i * dir, gy, P.groomer, 0.9);\n\t\t\tput(gx + i * dir, gy + 1, P.tracks, 0.9);\n\t\t}\n\t\tput(gx - dir, gy - 1, P.groomer, 0.9);\n\t\tput(gx - 2 * dir, gy - 1, P.groomer, 0.9);\n\t\tif (t % 1600 < 500) put(gx - dir, gy - 2, P.beacon, 0.9, G_STAR, 0.6);\n\t\tput(gx + 2 * dir, gy, h(\"#fff4cf\"), 0.9, lamp(t, 9), 1.6);\n\t\tfor (let d = 2; d < 13; d++) {\n\t\t\tconst w = Math.floor(d * 0.35);\n\t\t\tconst fall = 1 - d / 13;\n\t\t\tfor (let j = -w; j <= w + 1; j++) {\n\t\t\t\tconst x = gx + (2 + d) * dir;\n\t\t\t\tconst y = gy + j;\n\t\t\t\tif (bayer(x, y) < fall * 0.7) put(x, y, P.beam, 0, 1.6 + 1.0 * fall);\n\t\t\t}\n\t\t}\n\t}\n\n\t// ---- the copy corner stays readable: every ink dot behind the text is thinned ----\n\tconst ink = G.inkA;\n\tfor (let y = 0; y < R && y < copyH; y++)\n\t\tfor (let x = 0; x < C && x < copyW; x++) {\n\t\t\tconst i = y * C + x;\n\t\t\tink[i] = ink[i]! * 0.45;\n\t\t}\n}\n\n// --- 1. MEADOW ---------------------------------------------------------------\n// A person with a lantern and a dog by a pond, a tree, a flock, fireflies.\n\nconst MEADOW = {\n\tday: { hill1: h(\"#8cc27a\"), hill2: h(\"#78b568\"), grass: h(\"#7fc45a\"), lanternGlow: h(\"#f3d9a0\") },\n\tnight: {\n\t\thill1: h(\"#2f3f63\"),\n\t\thill2: h(\"#273553\"),\n\t\tgrass: h(\"#4f7a63\"),\n\t\tlanternGlow: h(\"#8a6a3a\"),\n\t},\n} as const;\nconst TRUNK_A = h(\"#5a3a22\");\nconst TRUNK_B = h(\"#6b4a2b\");\nconst FIREFLY = h(\"#e7f28a\");\n\nfunction meadow(t: number): void {\n\tconst horizon = Math.floor(ROWS * 0.7);\n\tconst m = mode();\n\tconst M = MEADOW[m];\n\tskyFill(horizon);\n\tstars(t, horizon);\n\tsunDisc(COLS - 30, 13, 8);\n\tclouds(t, [\n\t\t[0.15, 7, 10],\n\t\t[0.55, 11, 7],\n\t\t[0.82, 5, 12],\n\t]);\n\t// soft hills behind\n\tfor (let x = 0; x < COLS; x++) {\n\t\tconst h1 = ridge(x + 30, 9, 40, 10) + 2;\n\t\tconst h2 = ridge(x, 19, 24, 7);\n\t\tconst t1 = Math.floor(horizon - h1);\n\t\tconst t2 = Math.floor(horizon - h2);\n\t\tfor (let y = t1; y < horizon; y++) put(x, y, M.hill1, y === t1 ? 0.25 : 0);\n\t\tfor (let y = t2; y < horizon; y++) put(x, y, M.hill2, y === t2 ? 0.3 : 0);\n\t}\n\t// ground\n\tfor (let y = horizon; y < ROWS; y++) {\n\t\tconst c = grad(GROUND[m], (y - horizon) / (ROWS - horizon));\n\t\tfor (let x = 0; x < COLS; x++)\n\t\t\tput(\n\t\t\t\tx,\n\t\t\t\ty,\n\t\t\t\tc,\n\t\t\t\ty === horizon ? 0.75 : y > horizon + 1 && y % 2 === 0 && hash(x, y, 9) > 0.965 ? 0.3 : 0\n\t\t\t);\n\t}\n\t// pond\n\tconst px = Math.floor(COLS * 0.3);\n\tconst py = horizon + 5;\n\tconst pw = 22;\n\tconst ph = 4;\n\tconst W = WATER[m];\n\tfor (let y = -ph; y <= ph; y++)\n\t\tfor (let x = -pw; x <= pw; x++) {\n\t\t\tconst e = (x * x) / (pw * pw) + (y * y) / (ph * ph);\n\t\t\tif (e < 1) {\n\t\t\t\tconst flow = Math.floor(t * 0.004 + y * 3);\n\t\t\t\tconst rip = (((x + flow) % 7) + 7) % 7 < 2 && hash(x, y, 4) > 0.3;\n\t\t\t\tput(\n\t\t\t\t\tpx + x,\n\t\t\t\t\tpy + y,\n\t\t\t\t\trip ? W[1] : W[0],\n\t\t\t\t\trip ? 0.3 : 0,\n\t\t\t\t\trip && m === \"night\" ? G_SPARKLE : false\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t// grass tufts + flowers\n\tfor (let x = 0; x < COLS; x += 2) {\n\t\tconst hh = 1 + Math.floor(hash(x, 1, 21) * 3);\n\t\tconst sway = Math.sin(t * 0.002 + x * 0.18) * 0.9 + Math.sin(t * 0.0033 + x * 0.05) * 0.5;\n\t\tif (Math.abs(x - px) < pw + 2) continue;\n\t\tfor (let k = 1; k <= hh; k++)\n\t\t\tput(x + Math.round(sway * (k / hh) * 1.6), horizon - k, M.grass, 0.55);\n\t\tif (hash(x, 2, 22) > 0.88)\n\t\t\tput(\n\t\t\t\tx + Math.round(sway * 1.6),\n\t\t\t\thorizon - hh - 1,\n\t\t\t\tSUN[Math.floor(hash(x, 3, 23) * 4)]!,\n\t\t\t\t0.9,\n\t\t\t\tG_FLOWER\n\t\t\t);\n\t}\n\t// tree with an autumn canopy\n\tconst tx = Math.floor(COLS * 0.62);\n\tconst top = horizon - 28;\n\tfor (let y = top + 9; y < horizon; y++) {\n\t\tput(tx, y, TRUNK_A, 0.9);\n\t\tput(tx + 1, y, TRUNK_B, 0.9);\n\t}\n\tfor (const [x0, y0, x1, y1] of [\n\t\t[tx - 2, top + 13, tx - 4, top + 11],\n\t\t[tx + 3, top + 12, tx + 5, top + 10],\n\t] as const) {\n\t\tput(x0, y0, TRUNK_A, 0.8);\n\t\tput(x1, y1, TRUNK_A, 0.8);\n\t}\n\tconst sw = Math.sin(t * 0.0017) * 1.1;\n\t(\n\t\t[\n\t\t\t[tx + 0.5, top + 7, 9.5],\n\t\t\t[tx - 7, top + 11, 6],\n\t\t\t[tx + 9, top + 10, 6],\n\t\t\t[tx + 2, top + 1.5, 5],\n\t\t] as const\n\t).forEach(([cx, cy, r], i) =>\n\t\tdisc(cx + sw * (1 - i * 0.15), cy, r, (x, y, dd) => {\n\t\t\tconst n = hash(x, y, 30 + i);\n\t\t\tconst edge = dd / r;\n\t\t\tif (n > edge * 0.5) {\n\t\t\t\tconst band = clamp(Math.floor(((y - top) / 24) * 4), 0, 3);\n\t\t\t\tconst flick = Math.sin(t * 0.004 + x * 0.6 + y * 0.4) > 0.7 && edge > 0.7;\n\t\t\t\tput(x + (flick ? 1 : 0), y, SUN[band]!, 0.8 - edge * 0.3);\n\t\t\t}\n\t\t})\n\t);\n\tfor (let i = 0; i < 6; i++) {\n\t\tconst life = (t * 0.00035 + i * 0.17) % 1;\n\t\tput(\n\t\t\ttx + 2 + Math.sin(life * 9 + i) * 7 + life * 16,\n\t\t\ttop + 6 + life * (horizon - top - 4),\n\t\t\tSUN[1 + (i % 3)]!,\n\t\t\t0.9,\n\t\t\ttrue\n\t\t);\n\t}\n\t// person + dog near the pond\n\tconst fr = Math.floor(t / 420) % 2;\n\tconst bob = Math.floor(t / 840) % 2;\n\tconst map = PERSON_MAP(t);\n\tsprite(PERSON[fr]!, map, px + pw + 6, horizon - 11, 0.9);\n\tsprite(DOG[bob]!, map, px + pw + 13, horizon - 5, 0.85);\n\t// lantern glow on the ground around it\n\tdisc(px + pw + 10.5, horizon - 5, 4.5, (x, y, dd) => {\n\t\tif (hash(x, y, 50) > dd / 4.5 && filled(x, y) && !isLit(x, y)) put(x, y, M.lanternGlow, 0);\n\t});\n\tflock(t, 6, 6);\n\t// fireflies at night\n\tif (m === \"night\")\n\t\tfor (let i = 0; i < 14; i++) {\n\t\t\tconst fx = hash(i, 1, 60) * COLS + Math.sin(t * 0.0007 + i) * 6;\n\t\t\tconst fy = horizon - 2 - hash(i, 2, 60) * 14 + Math.sin(t * 0.0011 + i * 2) * 2;\n\t\t\tconst pulse = Math.sin(t * 0.004 + i * 1.7);\n\t\t\tif (pulse > 0.3)\n\t\t\t\tput(fx, fy, FIREFLY, 0.9, 3.0 + ((pulse - 0.3) / 0.7) * 1.9, 0.8 + pulse * 0.6);\n\t\t}\n}\n\n// --- 2. SEA ------------------------------------------------------------------\n// A setting sun with rays, waves, a sailboat, a pier lamp, gulls.\n\nconst SEA = {\n\tday: {\n\t\tray: SUN[1],\n\t\treflect: SUN,\n\t\tdeep: h(\"#2c6bc0\"),\n\t\tboat: h(\"#fafafa\"),\n\t\tpierLamp: h(\"#f0b840\"),\n\t},\n\tnight: {\n\t\tray: h(\"#d9c89a\"),\n\t\treflect: SUN,\n\t\tdeep: h(\"#101a33\"),\n\t\tboat: h(\"#d8d2c8\"),\n\t\tpierLamp: h(\"#ffd15c\"),\n\t},\n} as const;\nconst PIER = h(\"#5a3a22\");\nconst POLE = h(\"#4a4a4a\");\nconst MOON_GLINT = h(\"#e8d9ae\");\nconst LAMP_GLOW = h(\"#9a7a3a\");\n\nfunction sea(t: number): void {\n\tconst horizon = Math.floor(ROWS * 0.6);\n\tconst m = mode();\n\tconst S = SEA[m];\n\tconst W = WATER[m];\n\tskyFill(horizon);\n\tstars(t, horizon);\n\tconst sx = COLS - 44;\n\tconst sy = horizon - 1;\n\tconst R = 15;\n\t// rays\n\tfor (let r = R + 3; r < R + 22; r++) {\n\t\tconst n = Math.floor((2 * Math.PI * r) / 2.2);\n\t\tfor (let i = 0; i < n; i++) {\n\t\t\tconst a = (i / n) * Math.PI * 2;\n\t\t\tconst x = sx + Math.cos(a) * r;\n\t\t\tconst y = sy + Math.sin(a) * r * 0.92;\n\t\t\tif (y > horizon - 1) continue;\n\t\t\tconst spoke = 0.5 + 0.5 * Math.cos(a * 24 + t * 0.0009 + r * 0.2);\n\t\t\tconst fall = 1 - (r - R - 3) / 19;\n\t\t\tconst breath = 0.7 + 0.3 * Math.sin(t * 0.0016 - r * 0.35);\n\t\t\tif (spoke * fall * breath > 0.42 + bayer(Math.round(x), Math.round(y)) * 0.45)\n\t\t\t\tput(x, y, S.ray, 0.35 * fall + 0.1, m === \"day\" ? 1.6 + 1.0 * fall * breath : false);\n\t\t}\n\t}\n\tsunDisc(sx, sy, R, horizon);\n\tclouds(t, [\n\t\t[0.1, 8, 12],\n\t\t[0.4, 5, 8],\n\t]);\n\t// sea\n\tfor (let y = horizon; y < ROWS; y++) {\n\t\tconst depth = (y - horizon) / (ROWS - horizon);\n\t\tconst speed = 0.0018 + depth * 0.003;\n\t\tconst phase = Math.floor(t * speed + y * 2.3);\n\t\tconst period = 9 + Math.floor(depth * 5);\n\t\tconst base = mixHex(W[0], S.deep, depth);\n\t\tconst band = clamp(Math.floor((1 - depth) * 4), 0, 3);\n\t\tconst halfW = R * (0.95 - depth * 0.55) + 2 * Math.sin(t * 0.002 + y);\n\t\tfor (let x = 0; x < COLS; x++) {\n\t\t\tconst on = (x + phase) % period < 3 && y % 2 === 0;\n\t\t\tconst underSun = Math.abs(x - sx) < halfW;\n\t\t\tif (underSun && on)\n\t\t\t\tput(\n\t\t\t\t\tx,\n\t\t\t\t\ty,\n\t\t\t\t\tm === \"night\" ? MOON_GLINT : S.reflect[band]!,\n\t\t\t\t\t0.55 - depth * 0.3,\n\t\t\t\t\tm === \"night\" ? 2.2 : 3.0 - depth * 0.8\n\t\t\t\t);\n\t\t\telse if (on && hash(x, y, 3) > 0.45) put(x, y, W[1], 0.22 + depth * 0.1);\n\t\t\telse put(x, y, base, y === horizon ? 0.6 : 0);\n\t\t}\n\t}\n\t// sparkles\n\tfor (let y = horizon + 1; y < ROWS; y++)\n\t\tfor (let x = 0; x < COLS; x++)\n\t\t\tif (hash(x, y, 11) > 0.996 && Math.sin(t * 0.006 + hash(x, y, 12) * 20) > 0.85)\n\t\t\t\tput(x, y, WHITE, 0.9, G_SPARKLE);\n\t// sailboat drifting and bobbing\n\tconst bx = Math.floor(COLS * 0.35 + Math.sin(t * 0.00025) * 6);\n\tconst by = horizon + 6 + Math.round(Math.sin(t * 0.0021) * 1);\n\tsprite(BOAT, { w: S.boat, n: TRUNK_B }, bx, by - 7, 0.9);\n\t// pier with a lamp on the left\n\tconst pierY = horizon + 2;\n\tfor (let x = 0; x < 22; x++) put(x, pierY, PIER, 0.85);\n\tfor (let x = 2; x < 22; x += 5) {\n\t\tput(x, pierY + 1, PIER, 0.7);\n\t\tput(x, pierY + 2, PIER, 0.6);\n\t}\n\tfor (let y = pierY - 7; y < pierY; y++) put(20, y, POLE, 0.8);\n\tput(\n\t\t20,\n\t\tpierY - 8,\n\t\tS.pierLamp,\n\t\t0.9,\n\t\tm === \"night\" ? lamp(t, 3) : G_DAYLAMP,\n\t\tm === \"night\" ? 2 : 0.6\n\t);\n\tif (m === \"night\")\n\t\tdisc(20, pierY - 8, 3, (x, y, dd) => {\n\t\t\tif (dd > 0.5 && hash(x, y, 70) > dd / 3) put(x, y, LAMP_GLOW, 0);\n\t\t});\n\tfor (let i = 0; i < 3; i++)\n\t\tbird(\n\t\t\t((t * 0.004 * (0.6 + i * 0.2) + i * 61) % (COLS + 20)) - 10,\n\t\t\t8 + i * 5 + Math.sin(t * 0.002 + i) * 1.5,\n\t\t\tt * 0.012 + i,\n\t\t\t0.5\n\t\t);\n}\n\n// --- 3. MOUNTAINS ------------------------------------------------------------\n// Coloured ridges by depth, a river, a cabin with smoke, pines, a hawk.\n\nconst RIDGE_C = {\n\tday: [h(\"#b7a9d6\"), h(\"#7f87b8\"), h(\"#3f5f6e\")],\n\tnight: [h(\"#4b3b6e\"), h(\"#2e2c58\"), h(\"#1a2238\")],\n} as const;\nconst RIDGES = [\n\t{ seed: 101, scale: 30, amp: 20, lift: 0.2, alpha: 0.28, speed: 0.25, snow: 5, fade: 5 },\n\t{ seed: 202, scale: 22, amp: 16, lift: 0.1, alpha: 0.5, speed: 0.55, snow: 3, fade: 5 },\n\t{ seed: 303, scale: 15, amp: 11, lift: 0.0, alpha: 0.85, speed: 1, snow: 0, fade: 6 },\n] as const;\nconst MTN = {\n\tday: { snow: h(\"#f4f6fa\"), pine: h(\"#1f5a3c\"), smoke: h(\"#d8d8d8\"), floor: h(\"#2b3d45\") },\n\tnight: { snow: h(\"#dfe8ff\"), pine: h(\"#0f2a24\"), smoke: h(\"#6f6f7a\"), floor: h(\"#0d131c\") },\n} as const;\nconst CABIN_WALL = h(\"#5a3a22\");\nconst CABIN_DOOR = h(\"#3b2a1a\");\n\nfunction mountains(t: number): void {\n\tconst base = Math.floor(ROWS * 0.92);\n\tconst m = mode();\n\tconst isNight = m === \"night\";\n\tconst K = MTN[m];\n\tconst W = WATER[m];\n\tconst pan = t * 0.0009;\n\tskyFill(base);\n\tstars(t, base * 0.7);\n\tsunDisc(Math.floor(COLS * 0.8), Math.floor(ROWS * 0.3), 11);\n\tconst RC = RIDGE_C[m];\n\tconst tops = RIDGES.map(() => new Float32Array(COLS));\n\tRIDGES.forEach((r, i) => {\n\t\tconst lit = RC[i]!;\n\t\tconst shaded = mixHex(RC[i]!, BLACK, 0.25 * 0.6);\n\t\tconst row = tops[i]!;\n\t\tfor (let x = 0; x < COLS; x++) {\n\t\t\tconst xs = x + pan * r.speed * 40;\n\t\t\tconst hgt = ridge(xs, r.seed, r.scale, r.amp);\n\t\t\tconst top = base - Math.floor(ROWS * r.lift) - hgt;\n\t\t\trow[x] = top;\n\t\t\tconst shade = Math.sin((xs / r.scale) * 2.5) > 0 ? 1 : 0.75;\n\t\t\tfor (let y = Math.floor(top); y <= base; y++) {\n\t\t\t\tconst fromTop = y - top;\n\t\t\t\tconst snowy =\n\t\t\t\t\tr.snow > 0 &&\n\t\t\t\t\tfromTop < r.snow &&\n\t\t\t\t\thash(Math.round(xs), y, r.seed) > (fromTop / r.snow) * 0.9;\n\t\t\t\tconst dens = fromTop < 1 ? 1 : Math.max(0.05, 0.6 - fromTop / r.fade);\n\t\t\t\tconst c = snowy ? K.snow : shade === 1 ? lit : shaded;\n\t\t\t\tput(\n\t\t\t\t\tx,\n\t\t\t\t\ty,\n\t\t\t\t\tc,\n\t\t\t\t\tsnowy || bayer(x, y) < dens ? (snowy ? 0.95 : r.alpha * shade) : 0,\n\t\t\t\t\tsnowy && fromTop < 2 && bayer(x, y) < 0.55 - fromTop * 0.25 ? G_SNOWCAP : false\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t});\n\tconst front = tops[2]!;\n\t// pines on the front ridge\n\tfor (let x = 6; x < COLS; x += 9) {\n\t\tconst top = front[x]!;\n\t\tif (!top) continue;\n\t\tconst hh = 4 + Math.floor(hash(x, 5, 88) * 3);\n\t\tfor (let k = 0; k < hh; k++)\n\t\t\tfor (let dx = -Math.floor(k / 2); dx <= Math.floor(k / 2); dx++)\n\t\t\t\tput(x + dx, top - hh + k + 1, K.pine, 0.85);\n\t}\n\t// river\n\tfor (let y = base - 10; y < ROWS; y++) {\n\t\tconst depth = (y - (base - 10)) / (ROWS - base + 10);\n\t\tconst rx = COLS * 0.6 + Math.sin(y * 0.35 + 1) * 5 + depth * 8;\n\t\tconst w = 1 + depth * 7;\n\t\tconst flow = Math.floor(t * 0.006 + y * 0.9);\n\t\tfor (let x = Math.floor(rx - w); x <= Math.ceil(rx + w); x++) {\n\t\t\tif (x >= 0 && x < COLS && y < front[x]! + 2 && y < base - 2) continue;\n\t\t\tconst on = (((x - flow) % 5) + 5) % 5 < 2;\n\t\t\tput(x, y, on ? W[1] : W[0], on ? 0.9 : 0.45, on && isNight ? G_SPARKLE : false);\n\t\t}\n\t}\n\t// cabin with a lit window and smoke\n\tconst cx = Math.floor(COLS * 0.3);\n\tconst cy = Math.floor(front[clamp(cx, 0, COLS - 1)] ?? base) - 6;\n\tsprite(\n\t\tCABIN,\n\t\t{\n\t\t\tn: CABIN_WALL,\n\t\t\tD: CABIN_DOOR,\n\t\t\ty: [LANTERN, 0.9, isNight ? lamp(t, 5) : G_DAYLAMP, 0],\n\t\t},\n\t\tcx,\n\t\tcy,\n\t\t0.9\n\t);\n\t// the golden house glow: a soft halo around the lit window (HDR only)\n\tif (isNight) {\n\t\tsoftGlow(cx + 2, cy + 4, SOFT_CABIN);\n\t\tsoftGlow(cx + 2, cy + 5, SOFT_CABIN);\n\t}\n\tfor (let i = 0; i < 5; i++) {\n\t\tconst life = (t * 0.0005 + i * 0.2) % 1;\n\t\tput(cx + 5 + Math.sin(life * 7 + i) * 1.5, cy - 1 - life * 9, K.smoke, 0.4 * (1 - life));\n\t}\n\tfor (let x = 0; x < COLS; x++) put(x, base + 1, K.floor, 0.6);\n\tconst a = t * 0.0006;\n\tbird(COLS * 0.5 + Math.cos(a) * 24, ROWS * 0.28 + Math.sin(a) * 6, t * 0.008, 0.7);\n}\n\n// --- registry ----------------------------------------------------------------\n\nconst SCENES: Record<PixelSceneName, (t: number) => void> = { alpine, meadow, sea, mountains };\nconst PAINTERS = new Map<PixelSceneName, ScenePainter>();\n\n/** The painter for a scene; an unknown name falls back to alpine. */\nexport function getScenePainter(name: PixelSceneName): ScenePainter {\n\tconst key: PixelSceneName = Object.prototype.hasOwnProperty.call(SCENES, name) ? name : \"alpine\";\n\tlet p = PAINTERS.get(key);\n\tif (!p) {\n\t\tconst fn = SCENES[key];\n\t\tp = (t, g, isNight) => {\n\t\t\tG = g;\n\t\t\tCOLS = g.cols;\n\t\t\tROWS = g.rows;\n\t\t\tNIGHT = isNight;\n\t\t\tg.copyW = COLS * COPY_W;\n\t\t\tg.copyH = ROWS * COPY_H;\n\t\t\tif (COLS > 0 && ROWS > 0) fn(t);\n\t\t};\n\t\tPAINTERS.set(key, p);\n\t}\n\treturn p;\n}\n","type":"registry:ui","target":"@ui/fancy/components/pixel-scene/pixel-scene-scenes.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/pixel-scene?framework=react"}}