PURPOSE
Full-screen viewer for browsing all nebula archetypes defined in @starship-survivors/data/nebula-archetypes (exported as DISPLAY_ARCHETYPES). Renders a live NebulaBackground of the currently selected archetype, exposes the same post-fx pipeline used by the playground FX tab, and provides a bake-to-PNG flipbook export. Mounted at route /nebula-viewer and deep-linkable via ?n=<index>.
OWNS
- Local React state:
idx(current archetype index, initialized from URL),showInfo,showFx,baking,bakeStatus,layerFxOpen. stripRef(HTMLDivElement) — ref to the bottom selector strip used for auto-centering the active button.- Index navigation helpers:
setIdxClamped(wrap-around moduloARCHETYPES.length),prev,next,jump(delta),random(re-rolls until different from current). bakeCurrentcallback — invokesnebulaBakeFrameswith a fixed config (tileSize: 1024,frameCount: 64,fps: 12,loopSeconds: 5.0) plus the currentPostFxValues, then triggers a browser download of the returned PNG bytes.- Two module-level constant arrays:
ART_STYLES(11 entries: borderlands, synthwave, vhs, pixelArt, oilPaint, watercolor, comic, blueprint, thermal, stainedGlass, cyberglitch) andPOLISH_KNOBS(5 entries: polishBlur, polishHue, polishSat, polishLight, polishAnimSpeed). - Module-level helpers:
readUrlIndex(),writeUrlIndex(idx),downloadPngBytes(bytes, filename). - Internal sub-components:
FxPanel,FxSlider,FxToggle— all defined inside this file. - Inline style objects:
navBtnStyle,iconBtnStyle,fxRowStyle,fxLabelStyle,fxValueStyle,fxSectionStyle.
READS FROM
DISPLAY_ARCHETYPES(re-exported asARCHETYPES) from@starship-survivors/data/nebula-archetypes— the archetype list and its length.usePostFxStatefrom@starship-survivors/engine/rendering/post-fx-store— subscribes the component (andFxSlider/FxToggle) to live FX state.getPostFxState,getPostFxIdentityfrom the same store — snapshot reads for bake input and per-knob identity values.window.location.searchandwindow.location.hrefon mount and on every index change.PostFxStateandPostFxValuestypes (both from the rendering layer).
PUSHES TO
setPostFxValue(key, value)— called fromFxSlider(range input + reset button),FxToggle, and theRESET ALLbutton (zeros every art style, restores polish knobs to identity, turns off the three layer FX toggles).nebulaBakeFrames(arch, opts)— async call that produces a PNG flipbook; result is downloaded viadownloadPngBytes.window.history.replaceStateviawriteUrlIndex— keeps?n=<idx>in sync on every index change.- DOM mutations in
downloadPngBytes: creates aBlob, an object URL, an<a>element appended todocument.body, dispatches.click(), removes the element, and revokes the object URL after 1 s. strip.scrollBy({ left, behavior: 'smooth' })on the selector strip to keep the active button centered.
DOES NOT
- Does not mutate archetype data —
ARCHETYPES[idx]is read-only. - Does not persist anything outside the URL — no localStorage, no Supabase, no telemetry call.
- Does not register a
popstatelistener; navigating the browser back button does not feed back into local state (URL is push-only viareplaceState). - Does not block key handlers when an info or FX panel is open — the same keys remain bound; only an
HTMLInputElementtarget short-circuits the handler. - Does not call any router; the file is referenced by whichever route table maps
/nebula-viewerto this default export. - Does not import from
@starship-survivors/enginebeyondrendering/post-fx-storeandrendering/nebula-engine. - Does not handle archetype-list mutation at runtime;
setIdxClampedassumesARCHETYPES.lengthis constant for the lifetime of the component.
Signals
useEffect([idx])× 2: one writes the URL, one auto-scrolls the selector strip to center the active button (queries bydata-arch-idxattribute).useEffect([prev, next, jump, random, setIdxClamped, showInfo, showFx])registers a singlekeydownlistener onwindowand cleans it up on unmount.useMemo([arch])buildsinfoRows— a 15-row array of[label, value]tuples covering name, palette, curve, warp, density, speed, thresh, bg, shape, noiseFreq, noiseType, emit, sat, lum, fog.- Bake state machine:
bakingtoggles button enabled/cursor;bakeStatusis a free-form status string set on start, success (frame count, tile size, grid dims, KB), or failure (error message). It is never cleared automatically.
Entry points
- Default export:
NebulaViewerScreen— the route component for/nebula-viewer. - URL contract:
?n=NwhereNis a finite integer in[0, ARCHETYPES.length). Anything else falls back to0. - Keyboard contract:
ArrowLeft/ArrowRightstep ±1;PageUp/PageDownstep ±10;Home/Endjump to first/last;Rrandomizes;Itoggles the info panel;Ftoggles the FX panel;Escapecloses whichever panel is open (info first, then FX). - Top-bar UI: numeric index, archetype name, total count, plus four action buttons —
FX,i,⚄(random),BAKE. - Bottom UI: horizontally-scrolling button strip (one button per archetype, labeled
i: name) and aPREV/NEXTrow.
Pattern notes
- All FX surface state lives in the shared
post-fx-store, so changes made here also affect the ship playground and any other consumer — the file’s own state holds only viewer-local concerns (which panels are open, bake progress, current index). - The FX panel is laid out as a three-layer pipeline mirroring the playground’s
FxTab: art-style sliders (mutually-styled presets) → optional layer-FX foldout (BG stars, sparkles, shooting stars, each with conditional sub-sliders gated on a>= 0.5threshold) → polish knobs (blur, hue, sat, lightness, anim speed). TheRESET ALLbutton zeros art styles and layer toggles but resets polish to per-knob identity rather than zero. FxSliderhighlights its value in amber when the current value differs from identity, providing a visual diff against the default.FxToggletreats the underlying numeric FX value as a boolean by comparing>= 0.5; clicks write0or1.- The selector-strip auto-scroll uses smooth scrolling and recomputes from the strip’s measured center vs. the button’s measured center — works correctly even after resize or when the strip has been manually scrolled.
- Bake output filename pattern:
nebula-<idx>-<slugified-name>-<tileSize>-<frameCount>f.png; the slugifier lowercases and replaces runs of non-alphanumeric characters with a single hyphen. - Type assertions:
setPostFxValue(key, value as never)is used to bypass the generic key-typed signature, because the value type varies per key. - The
keydownhandler bails early whene.target instanceof HTMLInputElement— leaves room for a future search input without changing the dispatcher. - The
BlobPartcast (bytes as BlobPart) is needed becauseUint8Arrayis accepted by theBlobconstructor at runtime but the TS lib types are stricter.