PURPOSE

React component that mounts a live WebGL nebula canvas behind the hub UI. Accepts an archetype prop selecting which nebula renders and an optional blackoutProgress (0-1) prop that drives a fade-to-black overlay used during planet transitions.

OWNS

  • A container div (positioned absolute, inset:0, zIndex:0, pointerEvents:none, overflow:hidden, filter:brightness(1.5)).
  • The lifecycle of the singleton nebula engine for the duration of the mount: calls nebulaInit on mount and nebulaDestroy on unmount.
  • The requestAnimationFrame loop that ticks the nebula each frame.
  • A window resize listener that drives nebulaResize to the container’s client size.
  • A subscription to the post-FX store; the unsubscribe function is captured and called on unmount.
  • The blackout overlay div rendered when blackoutProgress > 0 (opacity bound to the prop, zIndex:1).
  • Three refs: containerRef (host div), rafRef (RAF handle), archRef (latest archetype mirror), fxRef (latest PostFxValues bundle).

READS FROM

  • Props: archetype: Archetype, blackoutProgress?: number (defaults to 0).
  • @starship-survivors/engine/rendering/nebula-enginenebulaInit, nebulaResize, nebulaRender, nebulaGetCanvas, nebulaDestroy, PostFxValues type.
  • @starship-survivors/data/nebula-archetypesArchetype type.
  • @starship-survivors/engine/rendering/post-fx-storegetPostFxState, subscribePostFx.
  • performance.now() for the time-since-mount value passed to the renderer.
  • window for the resize event.

PUSHES TO

  • The nebula engine: calls nebulaInit, attaches the canvas returned by nebulaGetCanvas to its own container, calls nebulaResize(width, height) on mount and on every window resize, and calls nebulaRender(archetype, 0, 0, t, 1.0, fxRef.current) once per frame.
  • DOM: appends the engine canvas to containerRef.current and removes it from its parent on cleanup. Sets canvas style.cssText to position:absolute;inset:0;width:100%;height:100%;pointer-events:none.
  • nebulaDestroy is called on unmount to free GPU resources.

DOES NOT

  • Does not own or create the nebula canvas — it is owned by the singleton engine and retrieved via nebulaGetCanvas.
  • Does not write to the post-FX store; only reads from it.
  • Does not re-run the mount effect when archetype changes — the latest archetype is mirrored into archRef and read by the RAF tick.
  • Does not trigger React re-renders when post-FX values change — the subscription writes into fxRef.
  • Must not be mounted while the game is running (engine is a singleton; only one instance at a time).
  • Does not gate rendering on blackoutProgress; the overlay div is conditionally rendered only when blackoutProgress > 0.

Signals

  • archetype prop change — next RAF tick reads the new value via archRef.current.
  • blackoutProgress prop change — triggers a React re-render of the overlay div (opacity is bound to the prop).
  • subscribePostFx callback — refreshes fxRef.current via getPostFxState() whenever the post-FX store changes.
  • window resize event — calls nebulaResize with the container’s current clientWidth/clientHeight.

Entry points

  • Default export: named export NebulaBackground({ archetype, blackoutProgress }).
  • Props interface: NebulaBackgroundProps { archetype: Archetype; blackoutProgress?: number }.

Pattern notes

  • Singleton engine contract: mount/unmount must be balanced; nebulaInit and nebulaDestroy bracket the lifecycle. Early-return paths on !ok or missing containerRef.current / nebulaGetCanvas() return undefined cleanup, so failed init does not register a cleanup.
  • Ref-mirror pattern: live values that change frequently (archetype, post-FX state) are mirrored into refs so the RAF tick reads them without re-running the mount effect. The post-FX subscription explicitly enables slider-drag flows to drive GLSL uniforms with no React re-render.
  • Render call comment references v5.155nebulaRender is passed the archetype, two zeros (camera or offset placeholders), t (seconds since mount), 1.0 (time-scale multiplier), and the full PostFxValues bundle. The engine applies polishAnimSpeed to the time scale internally.
  • The container uses filter: brightness(1.5) to lift the nebula brightness uniformly above the engine’s native output.
  • Blackout overlay is a plain DOM div over the canvas (zIndex:1) rather than a uniform passed to the shader.
  • Empty dependency array on the useEffect — mount logic runs exactly once per component instance.