PURPOSE

Shared type and utility module for the parallax “sandwich” render stack. Defines the slot taxonomy, the per-frame draw context, the layer interface, the biome recipe shape, and the deterministic math helpers every concrete parallax layer reuses. It is the contract that the parallax system, biome recipes, and individual layer implementations (starfield, atmosphere FBM, ground texture, silhouette stamps) all conform to.

OWNS

  • ParallaxSlot — string union of the four render phases: 'back', 'mid', 'near', 'fg'.
  • ParallaxFrame — per-draw context with ctx, game-time t, camera world position camX/camY, camZoom, and CSS-pixel viewport size viewW/viewH.
  • ParallaxLayer — layer contract: id, slot, parallax scroll factor, depth tiebreaker, optional prepare() (cache once on biome entry), required draw(f) (called once per frame inside the matching slot), optional dispose() (cleanup on biome switch out).
  • BiomeParallaxRecipebiomeId, displayName, ordered layers array that defines a biome’s look.
  • hash2i(x, y) — deterministic integer hash returning a float in [0, 1). Used for stamp placement and noise sampling.
  • hash3i(x, y, z) — three-input variant for per-instance seeds derived from (tileX, tileY, index).
  • forVisibleTiles(parallax, tileSize, camX, camY, camZoom, viewW, viewH, cb) — iterates integer (tx, ty) tile coords overlapping the visible viewport at the given parallax factor, with one-tile padding on each side to hide edge pop-in.
  • worldToScreen(worldX, worldY, parallax, camX, camY, camZoom, viewW, viewH) — returns { sx, sy } screen-space coords using the shared parallax + camera math.

READS FROM

  • CanvasRenderingContext2D (DOM) — referenced only via the ParallaxFrame.ctx type.
  • Math.imul, Math.floor, Math.ceil — used inside the hash and tile-iteration helpers.

The module imports nothing else. It is leaf-level.

PUSHES TO

Nothing at runtime — the module exports types and pure utility functions. Downstream consumers (the parallax system, biome recipes, the four concrete layer files in the same directory) import from it; this file does not push state anywhere.

DOES NOT

  • Allocate per frame. The utilities are pure functions over numbers; they return either nothing (forVisibleTiles invokes a callback) or a small literal object (worldToScreen).
  • Maintain hidden state, caches, or singletons. No module-scope mutable bindings.
  • Schedule, batch, or sort layers. Ordering across slots and within a slot (via depth) is the parallax system’s responsibility.
  • Touch WebGL. The render context is Canvas 2D; the WebGL nebula sits below the 'back' slot and is owned elsewhere.
  • Read device pixels. ParallaxFrame.viewW/viewH are explicitly CSS pixels.
  • Provide non-deterministic randomness. All randomness must flow from hash2i / hash3i so parallax scroll never pops content.

Signals

  • parallax field on ParallaxLayer: 0.0 = locked in place (infinitely far), 1.0 = world plane (moves with the world exactly), >1.0 = foreground (moves faster than the camera, e.g. dust or fog between camera and ship).
  • slot field draws in fixed order — 'back' (after WebGL nebula, before terrain — deep background), 'mid' (before terrain — distant silhouettes and atmosphere), 'near' (right before enemies and ship — close silhouettes), 'fg' (after gameplay, before HUD — dust and haze).
  • depth field is the within-slot tiebreaker: smaller draws first (further back).

Entry points

  • ParallaxLayer.prepare() — called once when the layer enters the active biome. Layers cache stamp textures, bake noise fields, and allocate the single reusable dust buffer here.
  • ParallaxLayer.draw(f) — called once per frame inside the matching slot phase.
  • ParallaxLayer.dispose() — called when the biome switches out, for optional cleanup.
  • hash2i, hash3i, forVisibleTiles, worldToScreen — imported directly by concrete layer implementations.

Pattern notes

  • The four-slot sandwich is fixed and forms a hard contract with the parallax system and the WebGL nebula below it. Adding a phase means changing every consumer.
  • Determinism from (tile coords, seed) is load-bearing — both hash helpers use the same Murmur-style mixing (0x1b873593, 0xcc9e2d51, 0x85ebca6b, 0xc2b2ae35) so seeds line up across helpers and layer types.
  • forVisibleTiles applies parallax to the camera (ex = camX * parallax) before computing tile bounds, so a single iteration pattern works for every parallax factor.
  • worldToScreen uses the same (world - cam * parallax) * zoom + viewCenter formula forVisibleTiles is consistent with — layer code does not reimplement camera math.
  • The header comment is the canonical description of the sandwich; concrete layers should not redefine slot semantics.
  • The “never allocate memory per frame” rule is stated in the header and is the reason prepare() exists. Baking textures and noise fields up front, and reusing a single dust buffer, are the patterns this contract enables.
  • The tile loop pads bounds by one tile on each side (- 1 on min, + 1 on max) so content sliding into view is already present.