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 untilinvalidateGlowStamps). - Module-scoped
_tintCache: Map<string, HTMLCanvasElement>— color-tinted variants keyed by the string"r,g,b". - Constant
GLOW_SIZE = 128and derivedGLOW_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.0at0,0.85at0.15,0.5at0.35,0.2at0.6,0.05at0.8,0at1.0. These define the falloff curve and are the only place the visual shape of any glow is described. - The tinting recipe: draw base,
multiplya solidrgb(r,g,b)rect over it, thendestination-inre-stamp the base to restore the alpha thatmultiplywould otherwise destroy.
READS FROM
- Browser DOM —
document.createElement('canvas')andHTMLCanvasElement.getContext('2d')for the offscreen stamp surfaces. No DOM insertion; the canvases live only as in-memory image sources fordrawImage.
PUSHES TO
- The caller’s
CanvasRenderingContext2Dviactx.drawImage. Both draw functions temporarily setctx.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/barguments. Callers are expected to pass0..255integers; out-of-range values produce undefined CSS color behavior. - Does not clamp
radiusoralpha. The only guard isif (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
invalidateGlowStampsdrops 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 a2*radiussquare. Looks up (or bakes on miss) the tinted stamp for(r, g, b), setsctx.globalAlpha = alpha, callsdrawImage, restores alpha. Used byengine/rendering/draw.tsfor the outer color glow on every projectile body (radius scales with sprite size; thecannonballarchetype doubles again).drawWhiteGlow(ctx, sx, sy, radius, alpha)— Same asdrawGlowbut skips the tint lookup and draws the base white stamp directly. Marginally faster. Used byengine/rendering/draw.tsfor the inner white-hot core on every projectile body (radius is0.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/drawWhiteGlowcall.
Pattern notes
- Bake once, draw many. A single
createRadialGradient+fillRectper color, then a flatdrawImageper glow instance. Avoids the per-frame allocation and per-pixel gradient evaluation thatcreateRadialGradientinside the draw loop would impose. - Tinting via composite trick, not per-pixel.
multiplycolors the white pixels but stomps alpha to opaque; a follow-updestination-inwith 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.
Mappreserves insertion order, so_tintCache.keys().next().valueyields the oldest entry. Caller-side it behaves as a small LRU-ish bound on memory;MAX_TINT_CACHE = 16is plenty for the current weapon-color palette. - Lazy first-call init.
_ensureStampis the only path that builds the base canvas — bothdrawWhiteGlowand 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 whitedrawWhiteGlow(small, bright) to produce the “color halo + white-hot core” look. The pairing isn’t enforced here — it’s just the waydraw.tsuses them. - Distinct from
glow-stamps.ts(plural). A separate module atengine/rendering/glow-stamps.tsprovidesaddGlow/addCanvasGlowfor a different glow workflow consumed viaengine/bridge.ts. Singularglow-stamp.tsis the radial-gradient stamp described here.