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 of ParallaxLayer[]), _perfTier.
  • The slot-bucketing + depth-sort logic (rebucket).
  • The perf-tier filter (filterForPerf) that decides which layers survive on low/mid/high devices.
  • The PerfTier type alias.

READS FROM

  • ./layer-types for ParallaxLayer, ParallaxFrame, ParallaxSlot, BiomeParallaxRecipe type shapes.
  • ./biome-recipes via getBiomeRecipe(biomeId) to resolve a biome id into its layer list.
  • ../../core/state camera for camera.x, camera.y, camera.zoom, packed into each ParallaxFrame passed to layers.

PUSHES TO

  • The CanvasRenderingContext2D passed into drawSlot — actual drawing is delegated to each layer’s draw(frame).
  • Each layer’s optional prepare() hook on biome activation, and dispose() 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 ParallaxFrame each 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, calls prepare() 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 (back after WebGL nebula and before terrain; mid before terrain; near after terrain and before sprite batch; fg after gameplay and before HUD).
  • disposeParallax(): void — explicit teardown for tests and hot reload.
  • getActiveBiomeId(): string | null — read-only accessor used by debug UIs and by draw-3d-terrain.ts to 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 depth order — 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 matches dust_streaks / fine_grain for fg decoration that mid tier drops.
  • On low tier the filter keeps at most one stamp, one fbm, and one starfield per slot, dropping everything else.
  • setBiome is 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 drawSlot call allocates a fresh ParallaxFrame object; layers must not retain references to it across frames.
  • Layer lifecycle hooks (prepare, dispose) are optional — the system uses optional-chaining when invoking them.