PURPOSE

Defines the flat-shape stamp families used by the parallax backdrop. Every family produces a single flat-colored silhouette with an optional thin outline and a pre-baked blur — no gradients, no internal detail (craters, windows, rim lights, dots). The backdrop’s job is mood, not reading; detail belongs to gameplay. Each family has N variants; first use of every (variant, tint, blurPx) combo bakes to a small offscreen canvas and caches. Draw time is one drawImage per placed stamp.

OWNS

  • The StampFamilyId union of family ids: asteroids_far, asteroids_mid, asteroids_near, planets_far, ships_distant, buildings, plants_jungle, crystals, debris, spires, obsidian_shards, oil_platforms, rig_beams.
  • VariantFn interface (draw(ctx, size, seed) + bakeSize) and Family interface (variants, baseDensity, minWorldSize, maxWorldSize).
  • The drawing primitives drawJaggedPolygon, drawSmoothAsteroid, drawLeaf.
  • The per-family VariantFn implementations: asteroidSmall, asteroidMedium, asteroidLarge, planetFar, shipDistant, buildingVariant, plantJungle, crystalShard, debrisChunk, spireRock, oilPlatform, rigBeam, obsidianShard.
  • The FAMILIES table mapping each StampFamilyId to its variant list, base density, and world-size range.
  • The _bakeCache (Map<string, HTMLCanvasElement>) keyed by family:variantIndex:tintHex:blurPx.
  • The bake pipeline (bakeVariant): flat-black shape draw source-in tint blit through filter: blur(...) onto a padded outer canvas so the halo never clips.

READS FROM

  • ./layer-types — imports hash3i for all per-vertex / per-pipe / per-greeble / per-arm seeded random rolls.
  • document — guarded typeof document === 'undefined' check so non-browser environments return null from bakeVariant rather than crashing.

PUSHES TO

  • Returns HTMLCanvasElement instances to callers via getStampCanvas for blit at draw time.
  • Exposes read-only family metadata via getFamilyInfo (variantCount, baseDensity, minSize, maxSize).
  • Returns deterministic variant indices via pickVariantIndex for tile-local (tx, ty, i) placements.
  • Provides disposeStampCache for palette swaps and hot-reload/test teardown.

DOES NOT

  • Does not place stamps in the world or choose tile positions — callers (the stamp layer renderer) decide where and how many.
  • Does not blit baked canvases to the screen — only returns the cached HTMLCanvasElement.
  • Does not maintain per-layer density multipliers — only exposes the base density.
  • Does not use createRadialGradient or createLinearGradient for fills (the single exception is rigBeam, which uses a vertical alpha gradient to fade into the water line — still flat-colored, just feathered alpha).
  • Does not draw internal detail: no craters, no windows, no dots, no specks, no rim lights, no second-color highlights.
  • Does not allow more than one simple outline per shape.
  • Does not bake without padding — output canvas size is always bakeSize + 2 * pad where pad = ceil(blurPx * 2).
  • Does not key the bake cache on function identity — variant index drives the shape seed so duplicate VariantFn entries in FAMILIES[*].variants still produce distinct silhouettes.

Signals

None — this module is a pure shape/bake library. No event emission, no observers, no subscribers.

Entry points

  • getFamilyInfo(family) — returns { variantCount, baseDensity, minSize, maxSize } for a family.
  • getStampCanvas(family, variantIndex, tintHex, blurPx) — returns (and caches on first call) the baked tinted+blurred canvas for one variant; returns null if document is absent or a 2D context cannot be obtained.
  • pickVariantIndex(family, tx, ty, i) — deterministic per-stamp variant pick using hash3i(tx * 31 + 7, ty * 37 + 11, i * 13 + 3) modulo variant count.
  • disposeStampCache() — clears _bakeCache; called on palette swap or in tests / hot-reload paths.

Pattern notes

  • Flat-fill-only contract. Every primitive and variant uses ctx.fillStyle = '#000' then a single fill() (or fillRect). The black shape is later tinted by source-in in the bake step. The only gradient in the file is rigBeam’s vertical alpha fade, which preserves the flat-color rule while letting the beam sink into the waterline.
  • Asteroid silhouette anti-symmetry. drawSmoothAsteroid defeats brain shape-matching with three independent variation sources: ellipsoidal stretch in [0.55, 1.55], baked rotation, and two wobble bands (low-freq sinusoids at odd cycle counts 3 and 5 with random phase + amplitude, plus per-vertex high-freq hash noise at ±6%). Side count itself rolls in [22, 30] so silhouette resolution varies.
  • Variant index drives the shape seed. bakeVariant passes (variantIndex + 1) * 0.1234 as the seed to variant.draw. Listing the same VariantFn N times in a family’s variants array produces N visually distinct bakes because every random roll inside the variant is keyed off that seed. The buildings family uses this with 16 entries of buildingVariant; asteroids_* families use it with 9 entries each. Comments in FAMILIES flag this explicitly so future edits don’t “deduplicate” the array.
  • Procedural buildings. buildingVariant rolls main body dimensions, then 0..3 thick pipes (each with independent length, thickness, side, height, end-cap dimensions), then 0..2 side/roof greebles, then an independent roll for a tall antenna mast and a stepped roof cap. Pipe shafts terminate at the cap’s inner edge to avoid double-overdraw artifacts after blur.
  • Bake cache key. ${family}:${variantIndex}:${tintHex}:${blurPx} — palette swaps change tintHex and create new entries; the old ones stay until disposeStampCache is called.
  • Bake padding. pad = ceil(blurPx * 2); output canvas is (bakeSize + 2 * pad)². Two-pixel safety clamps inside buildingVariant keep greebles, masts, and caps from rendering against the canvas edge where the blur halo could clip.
  • bakeSize is per-variant. Asteroid bake sizes scale from 48 (small) to 192 (large); planets 256; ships 64; buildings 192; plants 128; crystals 96; debris 64; spires 128; obsidian 128; oil platforms 192; rig beams 128. Larger bake sizes give crisper silhouettes on near-camera stamps at the cost of memory.
  • drawJaggedPolygon is legacy. It is kept only for debrisChunk — asteroid families switched to drawSmoothAsteroid for the soft oblong silhouette that matches the top-down projection of generateAsteroidMesh in draw-3d-terrain.ts.
  • Special-case families. oil_platforms and rig_beams exist for the Delphi lake surface view: oil platforms are top-down ringed platforms with pipe-arm stubs and deck structures; rig beams are vertical alpha-feathered beams with a top platform head and optional mast.
  • No DOM in non-browser paths. bakeVariant early-returns null when document is undefined, so server-side or worker-side imports of this module are safe.