PURPOSE

Render adapter that bridges the per-frame 2D draw pass into the live-shader runtime for archetype === 'cone_beam' bullets. A single shared LiveShaderRuntime hosts the fragment shader; each active cone bullet renders the shader once with its own uniforms and composites the result back onto the main 2D canvas via drawImage. The cone’s hitbox is computed elsewhere from the same world-space inputs sent here, keeping visual and collision locked together.

OWNS

  • Module-scoped singleton _runtime: LiveShaderRuntime | null for the shared cone-beam shader instance.
  • Module-scoped flag _unavailable: boolean that latches when WebGL2 or shader compilation fails, suppressing further init attempts.
  • Lazy construction of the runtime on first getConeBeamRuntime() call using DYNAMIC_CONE_FIRE_ID and DYNAMIC_CONE_FIRE_GLSL with 'additive' blend mode.
  • Public ConeBeamDrawParams interface defining the per-draw input shape (world endpoints, half-angle, intensity, heat, camera, zoom, time).

READS FROM

  • ./live-shader-runtime for the LiveShaderRuntime class (init, resize, setUniforms, render, isReady, getCanvas, destroy).
  • ../../data/vfx/live-shaders/dynamic_cone_fire.live for the shader id and GLSL source constants.
  • The caller-supplied CanvasRenderingContext2D (its canvas.width / canvas.height for the composite destination size).
  • Caller-supplied ConeBeamDrawParams (world coordinates, beam geometry, visual modulation, camera transform, time).

PUSHES TO

  • The shared LiveShaderRuntime via setUniforms (uFrom, uTo, uHalfAngle, uCoreIntensity, uHeat, uCamera, uZoom) and render(timeSeconds).
  • The caller’s CanvasRenderingContext2D via ctx.drawImage(glCanvas, 0, 0, ctx.canvas.width, ctx.canvas.height), upsampling the half-res shader canvas to full viewport.

DOES NOT

  • Does not set ctx.globalCompositeOperation — caller must already have additive ('lighter') mode active, matching the bullet pass in the bridge layer.
  • Does not compute the cone hitbox — that lives in bullets.ts and consumes the same world-space inputs independently.
  • Does not iterate bullets or filter by archetype — caller decides when to invoke drawConeBeam.
  • Does not retry runtime init after a failure; once _unavailable latches, only disposeConeBeamRuntime clears it.
  • Does not own the destination canvas, the camera transform, or bullet lifecycle.

Signals

  • getConeBeamRuntime returns null to signal WebGL2 unavailable or shader compile failure (latched via _unavailable).
  • drawConeBeam is a silent no-op when the runtime is missing or rt.isReady() is false.
  • resizeConeBeamRuntime is idempotent and is a no-op when the runtime is unavailable.

Entry points

  • getConeBeamRuntime(): LiveShaderRuntime | null — lazy accessor for the shared runtime singleton.
  • resizeConeBeamRuntime(viewW: number, viewH: number): void — call when the main canvas resizes.
  • drawConeBeam(ctx: CanvasRenderingContext2D, params: ConeBeamDrawParams): void — render one cone and composite onto ctx.
  • disposeConeBeamRuntime(): void — destroy the runtime and clear both the singleton and the _unavailable flag; exposed for shutdown / test teardown.

Pattern notes

  • Lazy singleton with negative-result caching: _unavailable prevents repeated failed init attempts each frame.
  • Init failure path constructs the runtime, calls init(), then latches _unavailable and returns null without retaining the runtime reference.
  • Half-resolution offscreen render upsampled by drawImage (bilinear) into the full-res context; the source comment notes this is acceptable for a soft fire effect.
  • Stateless per-draw API: the caller passes the full uniform set on every call; no diffing or persistence between frames.
  • Shared runtime is reused across all simultaneous cone bullets — one shader compilation, one GL canvas, one render + drawImage pair per cone per frame.
  • Visual/collision coupling is enforced by convention (shared inputs), not by code in this module.