PURPOSE
Orchestrates the sandwich of parallax background/foreground layers for a biome. Holds the active biome recipe, buckets its layers into four draw slots (back, mid, near, fg), and exposes a single drawSlot entry point that the renderer calls four times per frame. Also handles biome swaps, perf-tier pruning for mobile, and explicit teardown for tests and hot reload.
OWNS
- Module-level singleton state:
_activeRecipe,_bySlot(four-slot bucket ofParallaxLayer[]),_perfTier. - The slot-bucketing + depth-sort logic (
rebucket). - The perf-tier filter (
filterForPerf) that decides which layers survive onlow/mid/highdevices. - The
PerfTiertype alias.
READS FROM
./layer-typesforParallaxLayer,ParallaxFrame,ParallaxSlot,BiomeParallaxRecipetype shapes../biome-recipesviagetBiomeRecipe(biomeId)to resolve a biome id into its layer list.../../core/statecameraforcamera.x,camera.y,camera.zoom, packed into eachParallaxFramepassed to layers.
PUSHES TO
- The
CanvasRenderingContext2Dpassed intodrawSlot— actual drawing is delegated to each layer’sdraw(frame). - Each layer’s optional
prepare()hook on biome activation, anddispose()hook on biome swap or teardown.
DOES NOT
- Does not own a render loop or schedule its own frames — callers decide when to invoke
drawSlot. - Does not detect biome boundaries or perf tier — both are pushed in via
setBiome/setPerfTier. - Does not produce any pixels itself; all drawing lives inside individual layers.
- Does not cache or memoize per-frame work beyond the slot bucket; per-frame state is rebuilt into a fresh
ParallaxFrameeach call. - Does not interact with WebGL, terrain, sprites, or HUD — it only knows about the canvas 2D context it is handed.
Signals
setBiome(biomeId)— no-op if the biome is already active; otherwise disposes previous layers, fetches the new recipe, callsprepare()on each new layer, and rebuckets.setPerfTier(tier)— no-op if tier unchanged; otherwise rebuckets, re-running the perf filter against the current recipe.disposeParallax()— disposes all layers of the active recipe, clears the active recipe, and empties the slot buckets.
Entry points
setBiome(biomeId: string): void— call at mission start and on biome-boundary crossings.setPerfTier(tier: PerfTier): void— call when device tier is detected or changed.drawSlot(ctx, slot, tSeconds, viewW, viewH): void— call four times per frame, once per slot, in the documented render order (backafter WebGL nebula and before terrain;midbefore terrain;nearafter terrain and before sprite batch;fgafter gameplay and before HUD).disposeParallax(): void— explicit teardown for tests and hot reload.getActiveBiomeId(): string | null— read-only accessor used by debug UIs and bydraw-3d-terrain.tsto select biome-specific terrain variants (e.g. sunrise building vs. old_earth grounded).getActiveLayerCount(): number— total layer count after perf filtering, for debug overlays.
Pattern notes
- Module-singleton state; no class, no instance, no DI. Single global parallax stack per game session.
- Layers inside a slot are drawn in ascending
depthorder — lower depth values render first and appear further back. - The perf-tier filter recognises layer-id prefixes:
stamp_(silhouettes),fbm_(noise fields),stars_(starfields), and substring matchesdust_streaks/fine_grainforfgdecoration thatmidtier drops. - On
lowtier the filter keeps at most one stamp, one fbm, and one starfield per slot, dropping everything else. setBiomeis idempotent on the same id — safe to call every frame defensively.- The slot bucket is the only data structure that depends on perf tier; switching tier triggers a full rebucket, not a per-frame filter.
- Each
drawSlotcall allocates a freshParallaxFrameobject; layers must not retain references to it across frames. - Layer lifecycle hooks (
prepare,dispose) are optional — the system uses optional-chaining when invoking them.