player-glow.ts

PURPOSE

Renders a colored radial glow halo underneath the player ship. Idle state is a light-blue tinted ring at fixed opacity; gameplay events flash the glow to a new color for ~0.3 seconds and fade back to default. Most recent flash wins (newer events stomp older). Performance is structured so the idle path (the vast majority of frames) costs a single drawImage with no composite mode change, while the multiply-tint path runs only during the brief flash window.

OWNS

  • Module-level flash state: _flashR, _flashG, _flashB, _flashTimer.
  • Two pre-baked offscreen radial-gradient canvases: _defaultStamp (blue) and _whiteStamp (white).
  • Constants: DEFAULT_R/G/B (100, 180, 255), DEFAULT_ALPHA (0.50), FLASH_DECAY (0.3 s), FLASH_ALPHA (0.80), STAMP_SZ (128).
  • The PlayerGlow export object exposing flash, update, draw, reset.
  • The internal radial stamp baking routine _bakeRadialStamp (gradient stops at 0, 0.3, 0.7, 1.0 with alphas 1.0, 0.6, 0.2, 0).

READS FROM

  • Camera.toS from ../rendering/camera to convert world coordinates to screen coordinates.
  • camera from ../core for camera.zoom, used to scale the glow radius into screen space.
  • The worldRadius argument passed into draw (ship collision radius), multiplied by 2.5 to size the halo wider than the hull silhouette.
  • The dt argument passed into update to decay the flash timer.

PUSHES TO

  • The supplied CanvasRenderingContext2D in draw: mutates globalAlpha, optionally globalCompositeOperation (set to multiply only on the flash path) and fillStyle, then issues drawImage, arc, and fill calls. All mutations are bracketed by ctx.save() / ctx.restore().
  • Constructs DOM canvases lazily via document.createElement('canvas') inside _bakeRadialStamp on first draw (cached for the lifetime of the page).

DOES NOT

  • Does not own or read ship position, velocity, or facing — caller passes world coordinates and radius.
  • Does not own the rendering frame loop, the camera, or composite ordering — caller decides when to invoke draw.
  • Does not classify events. Color selection (shield/hull/XP/weapon) happens in the call sites; this module only consumes RGB triples.
  • Does not blend or queue multiple concurrent flashes — each flash call overwrites the prior color and resets the timer to full.
  • Does not emit audio, telemetry, or particles.
  • Does not allocate per frame on the idle path: no createRadialGradient, no temporary objects beyond the screen-space coord returned by Camera.toS.
  • Does not draw anything when glowR is zero or the ship is offscreen — there is no culling here; the caller decides whether to invoke draw.

Signals

  • PlayerGlow.flash(r, g, b) — set the current flash color to the given RGB (0-255) and reset the flash timer to FLASH_DECAY. Idempotent; the most recent call wins.
  • PlayerGlow.update(dt) — decay the flash timer by dt, clamped at zero. No effect if no flash is active.
  • PlayerGlow.draw(ctx, wx, wy, worldRadius) — render the glow. Computes t = _flashTimer / FLASH_DECAY and ease = t * t. If ease < 0.01, takes the idle path (single drawImage of the blue stamp at DEFAULT_ALPHA). Otherwise, takes the flash path: draws the white stamp at interpolated alpha between DEFAULT_ALPHA and FLASH_ALPHA, then overlays a multiply-blended filled arc tinted to the interpolated RGB between default-blue and flash color.
  • PlayerGlow.reset() — clear _flashTimer to zero (called on run reset).

Entry points

  • engine/bridge.ts calls PlayerGlow.reset() on run reset and PlayerGlow.update(rawDt) once per frame in the engine tick. The corresponding PlayerGlow.draw call inside the world render pass is currently commented out with a DISABLED — FPS profiling note.
  • engine/combat/damage.ts calls PlayerGlow.flash(255, 255, 255) on shield hit and PlayerGlow.flash(255, 40, 40) on hull hit.
  • engine/effects/actions.ts calls PlayerGlow.flash(r, g, b) from generic effect actions (XP collect and other color-driven cues).
  • engine/weapons/weapons.ts calls PlayerGlow.flash(r, g, b) on weapon fire using rarity color.

Pattern notes

  • Pre-baked stamps avoid per-frame gradient construction; the radial gradient is computed once and cached on the first draw via _ensureStamps.
  • The two-path design (idle vs. flash) is a deliberate fast-path optimization: the idle path skips the multiply composite operation entirely because the stamp is already pre-tinted blue, while the flash path uses a neutral white stamp plus multiply tint so any RGB color is reachable from a single baked asset.
  • Easing is ease = t * t (quadratic ease-out from peak), giving a fast initial fade and a slower settle.
  • Glow radius is worldRadius * 2.5 * camera.zoom — the 2.5 multiplier ensures the halo bleeds visibly beyond the ship hull rather than being clipped to the silhouette.
  • The multiply tint on the flash path is clipped to a circular arc rather than a fillRect to avoid showing a visible square box around the glow at flash peak.
  • Module state is file-scoped let bindings, not a class instance — there is exactly one player glow per page lifetime, matching the single-player game model.
  • No null checks on getContext('2d') result (uses !); the stamp bake assumes a standards-compliant 2D canvas context is always available.