PURPOSE

Dev tool screen for previewing tileable procedural backgrounds and baking them to multi-resolution flipbook PNGs for runtime use. Live preview uses GPU-side REPEAT wrap on the layer-stack composite so the screen fills with infinitely tiled output of the currently selected BackgroundDef. Parallax toggle and seam-debug overlay help verify tile correctness before running the bake. Mounted at route /backgrounds-viewer.

OWNS

  • Local React state for selection (idx), parallax factor, seamDebug flag, staticMode flag, tilesAcross count, baking flag, and bakeStatus string.
  • A containerRef div hosting the live preview canvas.
  • A previewRef holding the BackgroundLivePreview instance, plus a rafRef for the render loop handle.
  • Ref mirrors (defRef, parallaxRef, seamRef, staticRef, tilesRef) so the long-lived RAF closure reads current values without resubscribe.
  • Tile size constant TILE_SIZE = 1024.
  • Local helper functions readUrlIndex, writeUrlIndex, downloadPngBytes, and the styling helpers pillStyle and navBtn.
  • The bake handler onBake, which iterates baked size results and triggers PNG downloads named <id>-<size>.png.

READS FROM

  • ALL_BACKGROUNDS from @starship-survivors/engine/backgrounds-workbench/background-loader for the selectable list.
  • BackgroundLivePreview class from @starship-survivors/engine/backgrounds-workbench/live-preview for GPU rendering.
  • bakeBackgroundAllSizes from @starship-survivors/engine/backgrounds-workbench/bake-background for offline PNG baking.
  • BackgroundDef type from @starship-survivors/engine/backgrounds-workbench/background-schema.
  • window.location.search and window.location.href for n=<idx> query-param state.
  • window.devicePixelRatio capped at 2 for canvas sizing.

PUSHES TO

  • BackgroundLivePreview instance fields each frame: parallax, seamDebug, staticMode, tilesAcross; then calls preview.render(def).
  • window.history.replaceState to keep ?n=<idx> in sync with the selected background.
  • Browser download via a transient <a> element with object-URL Blob of PNG bytes.
  • DOM: appends the preview canvas to containerRef.current on mount; removes on unmount.

DOES NOT

  • Does not persist selection beyond the URL query param (no localStorage, no Zustand).
  • Does not modify any BackgroundDef data; bake is read-only against the loaded defs.
  • Does not upload baked PNGs anywhere; the user receives them as browser downloads.
  • Does not handle keyboard input while focus is inside an HTMLInputElement.
  • Does not render game UI, HUD, hub, or any non-dev surface.
  • Does not run when ALL_BACKGROUNDS is empty beyond a fallback message div.

Signals

  • Arrow keys (Left/Right) step previous/next background.
  • P toggles parallax between 0 and 0.5.
  • S toggles seam-debug overlay.
  • Top-bar buttons toggle static/animated mode, cycle tilesAcross through 1 -> 2 -> 4 -> 1, toggle parallax, toggle seams, and trigger BAKE.
  • Bottom selector row sets idx directly to any background in ALL_BACKGROUNDS.
  • ResizeObserver on the container and window resize listener drive preview.resize.

Entry points

  • Default export BackgroundsViewerScreen (React component) — mounted by the app router at /backgrounds-viewer.
  • Initial idx read from URL via readUrlIndex, clamped into [0, ALL_BACKGROUNDS.length).
  • Mount effect constructs BackgroundLivePreview({ tileSize: TILE_SIZE }), attaches its canvas, performs initial resize plus a deferred resize on the next animation frame (StrictMode first-paint workaround), and starts the RAF render loop.
  • Keyboard effect attaches a keydown listener at the window level.

Pattern notes

  • Long-lived RAF closure pattern: the tick callback reads from ref mirrors instead of stale state captured at mount.
  • Mount effect runs once with empty dependency array; all live tunables are mirrored to refs and applied to the preview instance every frame.
  • Index navigation uses modulo-based wrapping in setIdxClamped so prev/next wrap at the ends.
  • DPR sizing is capped at 2 to avoid oversized framebuffers on high-DPI displays.
  • StrictMode-double-mount workaround: extra requestAnimationFrame(resize) after the synchronous initial resize, in case clientWidth/clientHeight are stale on the first run.
  • Cleanup tears down ResizeObserver, RAF, resize listener, canvas DOM node, and calls preview.destroy().
  • The bakeStatus string is the single user-facing surface for bake progress and errors; bake errors are caught and rendered, not thrown.
  • Inline styles only; no CSS modules. Pill-style buttons share a pillStyle(active) helper; nav buttons share the navBtn constant.
  • Empty-state fallback renders a black screen with a monospace hint pointing to src/starship-survivors/data/backgrounds/.