explosion-fx.ts
PURPOSE
Layered explosion VFX for enemy deaths, bullet impacts, and AoE detonations. Exports two singletons: ExplosionFX (pre-baked stamp explosions with weapon-colored tinting plus expanding halo damage rings) and AoeExplosion (vector-shape explosion for missile/mortar/cannon detonations combining sonar pulse, boundary ring, and 4-layer fireball core). All rendering uses additive blending for natural bloom stacking. Stamps and tint canvas are baked lazily on first draw and cached forever.
OWNS
entities[]— pool ofExplosionEntity(capMAX_EXPLOSIONS = 40) for stamp-based explosions._haloRings[]— pool ofHaloRingEntity(capMAX_HALO_RINGS = 30) for expanding damage rings._aoeEntities[]— pool ofAoeExplosionEntity(capMAX_AOE = 20) for vector AoE explosions.stampCache—Record<string, HTMLCanvasElement>holding the four baked 128×128 stamps (core,rays,ring,debris)._tintCv/_tintCx— single shared 128×128 offscreen canvas reused for source-atop color multiply on white stamps.- Layer-spec presets
DEATH_LAYERS,IMPACT_LAYERS,BIG_LAYERSdefining stamp, delay, duration, fade ramps, scale curve, peak alpha, and rotation per layer. - Fireball palette
FIREBALL_LAYERS(4 layers: 255/100/30 → 255/136/0 → 255/204/0 → white) withradiusFrac,alphaMax, and per-layerstagger. - Timing constants:
FB_EXPAND_END,FB_HOLD_END,FB_TOTAL,FB_SHRINK_TO,SONAR_DURATION,SONAR_RADIUS_MULT,BOUNDARY_DURATION,BOUNDARY_RAMP_FRAC, and derivedAOE_MAX_TIMER. - Stamp size constants
STAMP_SZ = 128andSTAMP_HALF.
READS FROM
../core—camera(forcamera.zoom),W,H(screen dimensions for frustum culling).../rendering/camera—Camera.toS(x, y)to project world coordinates into screen space.../core/config—PERF_FLAGSimported (not currently referenced in any branch).document.createElement('canvas')and 2D contexts for stamp baking and tint scratch surface.Math.random()for per-explosionrotationOffsetso explosions do not look identical.
PUSHES TO
- The
CanvasRenderingContext2Dpassed intoExplosionFX.draw(ctx)/AoeExplosion.draw(ctx)— setsglobalCompositeOperation = 'lighter',globalAlpha,fillStyle,strokeStyle,lineWidth, plussave/translate/rotate/restoreanddrawImage/arc/fill/strokecalls. Both draw methods restoreglobalAlpha = 1andglobalCompositeOperation = 'source-over'before exiting. - The internal
_tintCvcanvas — each_tintStampcall clears it, draws the white stamp, then overlays solid RGB viasource-atop. The returned canvas reference is shared, so the caller must draw it before the next tint call overwrites it.
DOES NOT
- Does not deal damage.
haloRingis a visual-only effect — actual AoE damage is applied by the caller (collision-resolver). Despite the source comment mentioning “hits enemies once as it passes through”, this file only renders the ring. - Does not own world simulation state — explosions are purely cosmetic timers, removed when
timer >= maxTimer. - Does not respond to pause directly — it relies on the caller passing
dt = 0while paused. - Does not allocate per-frame: stamps are baked once, the tint canvas is reused, entity arrays use swap-remove for O(1) deletion.
- Does not handle off-screen entities efficiently beyond frustum culling at draw time (update still ticks all entries).
- Does not consult
PERF_FLAGSdespite importing it; thermal scaling offireballLayersis set up structurally (the field exists onAoeExplosionEntity) but is always assignedFIREBALL_LAYERS.lengthat spawn. - Does not bake stamps until the first
draw()call —_ensureStampsand_ensureTintCanvasare lazy. - Does not validate
colorHexstrictly —hexToRgbfalls back to128for any channel that fails to parse.
Signals
ExplosionFX.death(wx, wy, radius, colorHex)— death explosion (3 layers: core, rays, ring; ~0.22s).ExplosionFX.impact(wx, wy, radius, colorHex)— bullet impact (2 layers: core, ring; ~0.14s).ExplosionFX.big(wx, wy, radius, colorHex)— large/explosive (3 layers: core, rays, ring; ~0.33s).ExplosionFX.haloRing(wx, wy, maxRadius, colorHex, durationSec=0.3, thickness=15)— expanding ring visual.ExplosionFX.update(dt)— advances all stamp explosion and halo ring timers, removes expired.ExplosionFX.draw(ctx)— renders all stamp explosions then halo rings.ExplosionFX.clear()— emptiesentitiesand_haloRings.ExplosionFX.count— getter returningentities.length + _haloRings.length.AoeExplosion.spawn(wx, wy, blastR, colorHex)— vector AoE explosion (sonar + boundary + fireball).AoeExplosion.update(dt)— advances AoE timers, swap-removes expired.AoeExplosion.draw(ctx)— renders fireball, boundary ring, sonar pulse for each entity.AoeExplosion.clear()— empties_aoeEntities.AoeExplosion.count— getter returning_aoeEntities.length.
Entry points
damage.tsspawns death explosions on enemy death.collision-resolverspawns impact explosions on bullet hit andAoeExplosion.spawnwhen explosive or homing bullets detonate.bridge.tscallsExplosionFX.update(dt)andExplosionFX.draw(ctx)each frame; the same bridge drivesAoeExplosion.update/AoeExplosion.draw.- Run reset paths call
ExplosionFX.clear()andAoeExplosion.clear().
Pattern notes
- Stamp baking mirrors
SmokeFX._bakeStamp. Four shapes:core(radial gradient white→amber→transparent),rays(12 tapered triangles clipped from a radial gradient),ring(hollow ring via stacked gradient stops),debris(24 dots scattered using golden-angle spacing of137.508°). - Tint via shared offscreen canvas +
source-atop._tintStampclears the scratch canvas, draws the white stamp, thenfillRects the target RGB withsource-atopso the color only affects existing pixels. Returns the shared canvas — must be drawn before the next call. - Three-phase per-layer alpha curve. Within each layer,
frac = layerT / duration; alpha ramps linearly from 0 toalphaMaxoverfadeIn, holds atalphaMax, then ramps back to 0 overfadeOut. Alpha below0.01short-circuits the draw. - Scale interpolation is linear between
scaleStartandscaleEnd;drawRadius = radius * scale * camera.zoom, skipped if< 1screen pixel. - Frustum culling at draw time uses
radius * 3 * camera.zoomfor stamp explosions andblastR * SONAR_RADIUS_MULT * camera.zoomfor AoE (since the sonar pulse is the widest layer at1.5× blastR). - Rotation is optional. If
rot === 0the draw uses the cheapdrawImage(x, y, w, h)path; otherwise it wraps insave/translate/rotate/restore. - Swap-remove pattern in both
updatemethods: iterate backwards, on expiry overwrite slotiwith the last element and shrinklengthby 1 — O(1) deletion without preserving order. - Cap-and-drop spawn policy. New explosions are silently dropped when the entity array hits its cap; no overwrite of oldest.
- AoE fireball uses sqrt easing during expansion (
radius = maxR * sqrt(frac),alpha = alphaMax * sqrt(frac)) for fast initial bloom that decelerates, then a hold phase, then linear shrink toFB_SHRINK_TO = 0.60of max while alpha fades linearly. - AoE boundary ring uses cubic ease-out (
1 - (1-frac)^3) so radius reaches exactblastRsmoothly; alpha ramps over the firstBOUNDARY_RAMP_FRAC = 0.15then fades linearly. Stroke width scales with zoom but is clamped to>= 1pixel. - Sonar pulse uses quartic ease-out (
1 - (1-frac)^4) for near-instant expansion to1.5× blastR, with quadratic alpha falloff0.7 * (1-frac)^2. Fixed white color andlineWidth = 2.5(not zoom-scaled). - Layer 0 of the fireball uses the weapon’s identity color; layers 1-3 use the fixed orange/yellow/white palette regardless of the weapon.
- Random rotation offset per explosion breaks visual repetition without per-frame randomness.
- Minimum sizes enforced at spawn —
radiusclamped to>= 8for stamp explosions,blastRclamped to>= 20for AoE,maxRadiusclamped to>= 10for halo rings. - Halo rings draw after stamp explosions in
ExplosionFX.drawso the ring stroke sits on top of the stamp bloom.