PURPOSE

Places flat-shape silhouette stamps from a stamp family across an infinite tiled world for parallax backdrops. Deterministic per-tile placement keeps the same content in the same world position across scroll. Stamps paint into a per-layer offscreen buffer with destination-over compositing so overlapping stamps retain the first-painted alpha, then the merged buffer is blitted onto the main canvas at one configured opacity — the whole layer reads as a single cohesive silhouette at uniform alpha regardless of overlap density.

OWNS

  • SilhouetteStampLayerConfig interface (id, slot, parallax, depth, family, tileSize, density, tint, blurPx, opacity, sizeScale, rotate, composite, seed).
  • createSilhouetteStampLayer(config) factory that returns a ParallaxLayer.
  • Local resolveTint(tintOrSlot) helper that accepts either a PaletteSlot name or a #-prefixed hex literal.
  • Per-layer offscreen HTMLCanvasElement buffer and its CanvasRenderingContext2D, lazily allocated on first draw and resized to the viewport.
  • Per-layer seed multipliers SX0, SY0, SZ0 derived as seed * 1009, seed * 2003, seed * 3001.
  • The layer’s draw(frame) and dispose() implementations.

READS FROM

  • ./layer-typesParallaxLayer, ParallaxFrame, hash3i, worldToScreen, forVisibleTiles.
  • ./silhouette-stampsgetFamilyInfo, getStampCanvas, pickVariantIndex, StampFamilyId.
  • ../palette/palette-systemresolvePaletteSlot for slot-name tints (resolved per frame against the active palette).
  • ../palette/palette-typesPaletteSlot type.
  • The ParallaxFrame passed to draw: ctx, camX, camY, camZoom, viewW, viewH.
  • Global document (guarded by a typeof document === 'undefined' early return for non-DOM environments).

PUSHES TO

  • The per-layer offscreen buffer context (bctx): clearRect, globalAlpha = 1, globalCompositeOperation = 'destination-over', save/translate/rotate/scale/restore, and drawImage(stampCanvas, ...) for each placed stamp.
  • The main frame context (ctx): one save, globalAlpha = opacity, globalCompositeOperation = composite (default source-over), drawImage(buffer, 0, 0), restore.
  • Returns a ParallaxLayer (its id, slot, parallax, depth, draw, dispose) to whatever caller composes the parallax stack.

DOES NOT

  • Does not bake or cache stamp variants — that is owned by silhouette-stamps via getStampCanvas (the cache keys include tint and blur).
  • Does not resolve palette swaps over time — it calls resolvePaletteSlot once per draw for the layer’s tint.
  • Does not animate stamps; no per-frame motion, drift, twinkle, or rotation change. Positions and orientations are pure functions of (tileX, tileY, i, seed).
  • Does not stack alpha across overlapping stamps — destination-over in the buffer plus a single final blit prevents darkening.
  • Does not perform world simulation, spawn entities, or read game state outside the ParallaxFrame it is handed.
  • Does not own the parallax stack, tile visibility math, or camera transform — those live in layer-types.
  • Does not depend on WebGL; pure Canvas 2D.

Signals

None. The module exports no events, emitters, or callbacks. Communication is one-way: the renderer calls draw(frame) and dispose().

Entry points

  • createSilhouetteStampLayer(config: SilhouetteStampLayerConfig): ParallaxLayer — sole export besides the config type. Returns an object with id, slot, parallax, depth, draw(f), dispose().
  • ParallaxLayer.draw(f: ParallaxFrame) — invoked once per frame by the parallax renderer.
  • ParallaxLayer.dispose() — nulls out the buffer and its context.

Pattern notes

  • Coprime seed mixing: SX0 = seed*1009, SY0 = seed*2003, SZ0 = seed*3001. Mirrors the same pattern as starfield-layer.ts. Sibling layers that share a stamp family (e.g. back/mid/near buildings) pick different variants, rotations, scales, and flips at coincident tile coordinates, so the human eye does not pattern-match identical silhouettes across parallax slots.
  • Per-tile stamp count: Math.floor(family.baseDensity * layer.density + jitter) where jitter is hash3i(tx+SX0, ty+SY0, SZ0).
  • Per-stamp placement rolls all derive from hash3i with different third-argument offsets (i*3 + k + SZ0): ux, uy (position-within-tile), variant, sizeRoll, scaleRoll, rot, flipX, flipY.
  • World-to-screen via worldToScreen(worldX, worldY, parallax, camX, camY, camZoom, viewW, viewH); offscreen culling rejects stamps whose AABB lies fully outside the viewport.
  • worldSize = info.minSize + sizeRoll * (info.maxSize - info.minSize); final drawSize = worldSize * scale * camZoom where scale is drawn from sizeScale range [a, b].
  • Rotation branch: full 0..2π rotation plus independent mirror flips on both axes (flipX, flipY from a < 0.5 threshold). Rotation × flipX × flipY gives 4× finer orientation space than rotation alone. When rotate=false, stamps draw axis-aligned at the screen-space top-left of their AABB.
  • Tint resolution: a tint string starting with # (charcode 0x23) is treated as a hex literal; otherwise it is passed to resolvePaletteSlot as a PaletteSlot name and resolves to the active palette every draw, so palette transitions recolor the layer.
  • Blur is pre-baked into the stamp cache keyed on blurPx, so blur=4 and blur=1 share no cache. Typical values per the source comment: back=4, mid=2, near=1, fg=0.5.
  • Uniform alpha cohesion: the buffer is cleared, painted with destination-over, then blitted once with globalAlpha = opacity and globalCompositeOperation = composite. The opacity field is the only place layer alpha is applied — overlapping silhouettes never compound darker than a non-overlapping stamp.
  • Lazy + viewport-sized buffer: allocated on first draw via document.createElement('canvas'); resized to Math.max(1, Math.ceil(viewW)) × Math.max(1, Math.ceil(viewH)) whenever the viewport changes. Kept per-layer (not shared) so two silhouette layers in the same slot do not clobber each other.
  • dispose() simply nulls buffer and bufferCtx; the layer is intended to be cheap to drop and recreate at biome boundaries.
  • Default config values: opacity = 1, sizeScale = [1, 1], rotate = true, composite = 'source-over', seed = 0.