PURPOSE

Pre-baked radial gradient stamp for soft additive light effects (projectile glows, XP orb halos, weapon cores). Bakes a single 128px white radial gradient canvas once, then draws it at any size, position, and color tint via a single drawImage per glow. Replaces hard-edged ctx.arc() circles and per-frame createRadialGradient calls with smooth gradual falloff at a flat cost per instance.

OWNS

  • Module-scoped _stamp: HTMLCanvasElement | null — the lazily-built white base stamp. Created once on first use, reused forever (or until invalidateGlowStamps).
  • Module-scoped _tintCache: Map<string, HTMLCanvasElement> — color-tinted variants keyed by the string "r,g,b".
  • Constant GLOW_SIZE = 128 and derived GLOW_HALF = 64 — the dimensions of every baked stamp canvas.
  • Constant MAX_TINT_CACHE = 16 — the soft cap on tinted variants. Oldest entry is evicted (Map insertion order) when a new tint is requested past the cap.
  • The hardcoded gradient stops on the base stamp: 1.0 at 0, 0.85 at 0.15, 0.5 at 0.35, 0.2 at 0.6, 0.05 at 0.8, 0 at 1.0. These define the falloff curve and are the only place the visual shape of any glow is described.
  • The tinting recipe: draw base, multiply a solid rgb(r,g,b) rect over it, then destination-in re-stamp the base to restore the alpha that multiply would otherwise destroy.

READS FROM

  • Browser DOM — document.createElement('canvas') and HTMLCanvasElement.getContext('2d') for the offscreen stamp surfaces. No DOM insertion; the canvases live only as in-memory image sources for drawImage.

PUSHES TO

  • The caller’s CanvasRenderingContext2D via ctx.drawImage. Both draw functions temporarily set ctx.globalAlpha, then restore the previous value before returning. No other ctx state is touched (composite operation, transforms, fillStyle, etc. are left to the caller).

DOES NOT

  • Does not set or restore globalCompositeOperation. The caller is expected to set 'lighter' (additive) before calling for additive glow looks — without that, the stamp draws as a plain alpha-blended white blob.
  • Does not handle transforms (translate/rotate/scale). Position and size are passed directly to drawImage; rotation is irrelevant because the stamp is radially symmetric.
  • Does not clamp r/g/b arguments. Callers are expected to pass 0..255 integers; out-of-range values produce undefined CSS color behavior.
  • Does not clamp radius or alpha. The only guard is if (radius <= 0 || alpha <= 0) return — negative or zero values are silently skipped, positive values pass straight through.
  • Does not pre-warm any tints. Every new color pays a one-time bake cost on first use.
  • Does not own the cosmetic “linger” or post-FX systems — purely the gradient stamp source.
  • Does not invalidate on window resize, DPI change, or palette change. Only invalidateGlowStamps drops the cache, and nothing calls it automatically.

Signals

None. No events, no observers, no listeners — pure draw helpers plus a manual invalidation hook.

Entry points

  • drawGlow(ctx, sx, sy, radius, r, g, b, alpha) — Draws a color-tinted soft glow centered at (sx, sy) covering a 2*radius square. Looks up (or bakes on miss) the tinted stamp for (r, g, b), sets ctx.globalAlpha = alpha, calls drawImage, restores alpha. Used by engine/rendering/draw.ts for the outer color glow on every projectile body (radius scales with sprite size; the cannonball archetype doubles again).
  • drawWhiteGlow(ctx, sx, sy, radius, alpha) — Same as drawGlow but skips the tint lookup and draws the base white stamp directly. Marginally faster. Used by engine/rendering/draw.ts for the inner white-hot core on every projectile body (radius is 0.25× the outer glow radius).
  • invalidateGlowStamps() — Clears the base stamp and the entire tint cache. Intended for canvas context loss recovery or a major state reset. Not called from anywhere in the current codebase.
  • Module load — no side effects; nothing is baked until the first drawGlow/drawWhiteGlow call.

Pattern notes

  • Bake once, draw many. A single createRadialGradient + fillRect per color, then a flat drawImage per glow instance. Avoids the per-frame allocation and per-pixel gradient evaluation that createRadialGradient inside the draw loop would impose.
  • Tinting via composite trick, not per-pixel. multiply colors the white pixels but stomps alpha to opaque; a follow-up destination-in with the base stamp restores the original alpha mask. Two composite passes total per bake, then never re-touched.
  • String-keyed tint cache with insertion-order eviction. Map preserves insertion order, so _tintCache.keys().next().value yields the oldest entry. Caller-side it behaves as a small LRU-ish bound on memory; MAX_TINT_CACHE = 16 is plenty for the current weapon-color palette.
  • Lazy first-call init. _ensureStamp is the only path that builds the base canvas — both drawWhiteGlow and the tint baker go through it. No module-load DOM work.
  • Composite mode is the caller’s job. Additive glow looks come from the caller setting globalCompositeOperation = 'lighter' before drawing. This file intentionally stays unopinionated about blend mode so the same stamp can serve additive glows, alpha-blended halos, or destination-out masks if needed.
  • Outer + inner pair is a calling convention. The projectile pipeline pairs a tinted drawGlow (color, large, dim) with a white drawWhiteGlow (small, bright) to produce the “color halo + white-hot core” look. The pairing isn’t enforced here — it’s just the way draw.ts uses them.
  • Distinct from glow-stamps.ts (plural). A separate module at engine/rendering/glow-stamps.ts provides addGlow/addCanvasGlow for a different glow workflow consumed via engine/bridge.ts. Singular glow-stamp.ts is the radial-gradient stamp described here.