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(positionedabsolute,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
nebulaIniton mount andnebulaDestroyon unmount. - The
requestAnimationFrameloop that ticks the nebula each frame. - A window
resizelistener that drivesnebulaResizeto the container’s client size. - A subscription to the post-FX store; the unsubscribe function is captured and called on unmount.
- The blackout overlay
divrendered whenblackoutProgress > 0(opacity bound to the prop,zIndex:1). - Three refs:
containerRef(host div),rafRef(RAF handle),archRef(latest archetype mirror),fxRef(latestPostFxValuesbundle).
READS FROM
- Props:
archetype: Archetype,blackoutProgress?: number(defaults to0). @starship-survivors/engine/rendering/nebula-engine—nebulaInit,nebulaResize,nebulaRender,nebulaGetCanvas,nebulaDestroy,PostFxValuestype.@starship-survivors/data/nebula-archetypes—Archetypetype.@starship-survivors/engine/rendering/post-fx-store—getPostFxState,subscribePostFx.performance.now()for the time-since-mount value passed to the renderer.windowfor theresizeevent.
PUSHES TO
- The nebula engine: calls
nebulaInit, attaches the canvas returned bynebulaGetCanvasto its own container, callsnebulaResize(width, height)on mount and on every window resize, and callsnebulaRender(archetype, 0, 0, t, 1.0, fxRef.current)once per frame. - DOM: appends the engine canvas to
containerRef.currentand removes it from its parent on cleanup. Sets canvasstyle.cssTexttoposition:absolute;inset:0;width:100%;height:100%;pointer-events:none. nebulaDestroyis 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
archetypechanges — the latest archetype is mirrored intoarchRefand 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 whenblackoutProgress > 0.
Signals
archetypeprop change — next RAF tick reads the new value viaarchRef.current.blackoutProgressprop change — triggers a React re-render of the overlay div (opacity is bound to the prop).subscribePostFxcallback — refreshesfxRef.currentviagetPostFxState()whenever the post-FX store changes.windowresizeevent — callsnebulaResizewith the container’s currentclientWidth/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;
nebulaInitandnebulaDestroybracket the lifecycle. Early-return paths on!okor missingcontainerRef.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.155—nebulaRenderis passed the archetype, two zeros (camera or offset placeholders),t(seconds since mount),1.0(time-scale multiplier), and the fullPostFxValuesbundle. The engine appliespolishAnimSpeedto 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.