PURPOSE

Builds a hard, fully-opaque ground surface layer for the parallax system. The layer is drawn at a high parallax factor (typically 0.8..0.95) so the camera reads as skimming a surface rather than looking down from orbit. Noise modulates RGB around a tint while alpha stays at 255 so one pass paints the entire viewport. Used by the old_earth biome to provide a hard ground texture underneath the square grid layer.

OWNS

  • GroundTextureLayerConfig interface — id, slot, parallax, depth, tint (palette slot name or hex), optional brightnessRange (default 28), scale (default 2.2), octaves (default 4).
  • createGroundTextureLayer(config) — factory returning a ParallaxLayer with a draw(frame) closure.
  • disposeGroundCache() — clears the noise-canvas cache.
  • Private module-level _cache: Map<string, HTMLCanvasElement> keyed by ${tintHex}:${octaves}:${brightnessRange}.
  • Private helpers resolveTint, parseHex, makeTileableGroundCanvas, getGroundCanvas.
  • A local tileable value-noise implementation: hash, wrap, valueNoise, and an fbm accumulator over octaves.
  • Fixed 512x512 noise tile generation.

READS FROM

  • ./layer-typesParallaxLayer, ParallaxFrame types.
  • ../palette/palette-systemresolvePaletteSlot for converting palette slot names to hex.
  • ../palette/palette-typesPaletteSlot type.
  • The ParallaxFrame it receives at draw time: ctx, camX, camY, viewW, viewH.
  • Global document (guarded with typeof document === 'undefined' so it no-ops in non-DOM environments).

PUSHES TO

  • A CanvasRenderingContext2D supplied by the parallax frame — issues drawImage calls (4 base tiles plus up to 4 widescreen guard tiles), wrapped in ctx.save() / ctx.restore() with globalCompositeOperation = 'source-over' and globalAlpha = 1.
  • The internal _cache map (writes on cache miss inside getGroundCanvas).

DOES NOT

  • Does not time-drift. Scroll is purely camX * parallax and camY * parallax; there is no frame.time or wind-style animation.
  • Does not modulate alpha. Every pixel is alpha=255 so the layer is fully opaque.
  • Does not own its own canvas in the scene graph — it draws into the ctx provided by the parallax system each frame.
  • Does not register itself with any system; the caller (biome recipe) inserts the returned ParallaxLayer into the parallax layer list.
  • Does not handle resizing; the 512 px tile size and scale factor are fixed at construction.
  • Does not share its noise routine with atmosphere-fbm-layer.ts — the algorithm is intentionally duplicated locally so the two layers can diverge.

Signals

  • None. The module emits no events and exposes no observable state beyond the _cache it owns. Cache invalidation is manual via disposeGroundCache().

Entry points

  • createGroundTextureLayer(config: GroundTextureLayerConfig): ParallaxLayer — invoked by biome recipes (e.g. biome-recipes.ts for old_earth) to construct a layer instance.
  • ParallaxLayer.draw(frame: ParallaxFrame): void — invoked once per frame by the parallax system for the returned layer.
  • disposeGroundCache(): void — exposed for teardown or biome swaps that want to free generated tiles.

Pattern notes

  • Tint resolution checks the first character against 0x23 (#) to decide between a hex literal and a palette slot name; non-hex strings flow through resolvePaletteSlot.
  • Noise is value-noise on a torus: hash is seeded by octaves * 13.37 so different octave counts produce different tiles even at the same tint; wrap ensures the lattice wraps cleanly at the tile boundary so the resulting canvas tiles seamlessly.
  • The fbm loop starts at amp = 0.5, f = 1, with amp *= 0.5 and f *= 2 per octave, sampling valueNoise(x/size, y/size, f * 4).
  • Brightness modulation is symmetric: delta = (v - 0.5) * 2 * brightnessRange is added to all three channels (R, G, B) and clamped to [0, 255]. Same delta on all channels means noise reads as luminance variation, not chroma.
  • Cache key is ${tintHex}:${octaves}:${brightnessRange}; scale is intentionally excluded because scaling is applied at draw time via drawImage, not baked into the canvas.
  • Per-frame offset uses double-mod (((rawX % w) + w) % w) to handle negative camera coordinates correctly.
  • Tile coverage is 4 base draws covering one tile’s worth of offset, plus conditional extra draws guarded by viewW > w - ox + w and viewH > h - oy + h to handle viewports wider or taller than two scaled tiles.
  • Module is SSR-safe: makeTileableGroundCanvas returns null when document is undefined, and draw early-returns when no canvas is available.