PURPOSE

Procedural starfield layer for the parallax sandwich. Computes 0..N stars per visible tile from (tx, ty) hashes and draws each as a tiny filled circle. No pre-baking, no images. Twinkle modulation via a slow per-star sine. Deterministic across parallax scroll. Intended for the deepest parallax slots (parallax 0.02–0.20).

OWNS

  • StarfieldLayerConfig interface — id, slot, parallax, depth, tileSize, density, color, accentColor, sizeMul, twinkle, bigRatio, seed.
  • createStarfieldLayer(config) factory — returns a ParallaxLayer with a draw(f) callback.
  • Local resolveColor(colorOrSlot) helper — passes through literal CSS colors (starting with #, r, or R) and routes everything else through resolvePaletteSlot.
  • Per-layer seed mixers sx0 = seed * 1009, sy0 = seed * 2003, sz0 = seed * 3001 — coprime multipliers so sibling starfields with different seeds produce independent patterns rather than the same pattern at different offsets.

READS FROM

  • ./layer-typesParallaxLayer, ParallaxFrame types; hash3i, worldToScreen, forVisibleTiles utilities.
  • ../palette/palette-systemresolvePaletteSlot for slot-name color resolution.
  • ../palette/palette-typesPaletteSlot type.
  • ParallaxFrame fields per draw: ctx, camX, camY, camZoom, viewW, viewH, t.

PUSHES TO

  • CanvasRenderingContext2Dsave/restore, globalCompositeOperation = 'lighter', fillStyle, globalAlpha, beginPath/arc/fill.
  • No external state mutation. Returns a ParallaxLayer object whose draw callback issues canvas commands only.

DOES NOT

  • Does not allocate per frame (no arrays, no object literals beyond the destructured frame fields and the worldToScreen return).
  • Does not pre-bake textures, images, or offscreen canvases.
  • Does not maintain star-position arrays across frames — every star is rederived from hashes each frame.
  • Does not implement prepare or dispose on the returned layer.
  • Does not handle input, collision, audio, or gameplay state.
  • Does not draw outside the visible viewport — culls any star whose screen position falls more than 4 px outside viewW/viewH.

Signals

  • None emitted. Pure rendering layer; no events, callbacks, or external notifications.

Entry points

  • createStarfieldLayer(config: StarfieldLayerConfig): ParallaxLayer — the sole export. Called by biome recipes when assembling the parallax stack.
  • ParallaxLayer.draw(f: ParallaxFrame) — invoked once per frame by the parallax renderer during the matching slot phase.

Pattern notes

  • Color inputs accept either a PaletteSlot name or a literal CSS color (#…, rgb(…), rgba(…)). Slot resolution runs once per frame, not once per star.
  • Tile iteration goes through the shared forVisibleTiles helper, which pads tile bounds by 1 tile on each side to hide pop-in.
  • Star count per tile is jittered by ±20% around density using a per-tile hash, so neighbor tiles don’t all carry exactly the same count.
  • Star positions inside a tile are derived from two independent hash3i calls per star (offsets +1 and +2), then transformed by worldToScreen using the layer’s parallax factor.
  • Big-star branch: sizeRoll < bigRatio (default 0.08) flags ~8% of stars as bigger (radius 1.1 + sizeRoll * 0.7); the rest get radius 0.5 + sizeRoll * 0.5. Both multiplied by sizeMul.
  • Alpha is rolled per star with a separate hash3i call, base 0.35 + brightRoll * 0.6. With twinkle on, alpha is multiplied by 0.75 + 0.25 * sin(t * 1.3 + phase) where phase is yet another per-star hash mapped into [0, 2π).
  • Accent color (when supplied) is used when brightRoll < 0.12, i.e. ~12% of stars. Same resolution rules as the primary color.
  • globalCompositeOperation = 'lighter' is set inside ctx.save()/ctx.restore(), so additive blending applies only to this layer’s draws and doesn’t leak into adjacent layers.
  • Performance budget noted in the source header: ~50 stars per tile × 9 tiles = ~450 arcs per frame ≈ 0.3 ms on mobile.