PURPOSE

WebGL2 procedural fire renderer. Draws animated fire effects using FBM (fractal Brownian motion) noise on an offscreen canvas, sized at half viewport resolution. The offscreen canvas is composited onto the main Canvas 2D surface via drawImage with additive blend. Mirrors the architecture of nebula-engine.ts: own GL context, own shaders, batched instanced draw.

OWNS

  • Module-private offscreen HTMLCanvasElement (_canvas) sized at FIRE_SCALE (0.5) of the viewport
  • A WebGL2RenderingContext (_gl) created with alpha: true, premultipliedAlpha: true, antialias: false
  • The compiled WebGLProgram (_program) using the inlined FIRE_VS vertex shader and FIRE_FS fragment shader
  • A WebGLVertexArrayObject (_vao) binding a static 6-corner vertex buffer plus a dynamic per-instance buffer
  • A dynamic instance buffer (_instanceBuffer) and CPU-side Float32Array (_instanceData) sized for MAX_INSTANCES (64) instances of FLOATS_PER_INSTANCE (5) floats each: sx, sy, sw, sh, alpha
  • Cached uniform locations: _uViewport, _uTime, _uColor
  • Tint state: _colorR, _colorG, _colorB (default orange 1.0, 0.4, 0.0)
  • Cached camera state for world-to-screen conversion: _camX, _camY, _camZoom, _halfW, _halfH
  • The per-frame instance queue (_queue: FireInstance[])
  • Viewport dimensions (_viewW, _viewH)

READS FROM

  • Caller-supplied world or screen coordinates passed to add() / addScreen()
  • Caller-supplied camera state via setCamera(x, y, zoom, viewW, viewH) (must be called once per frame before add())
  • Caller-supplied time value passed to flush() (used as the u_time uniform driving shader animation)
  • Tint colour set via setColor(r, g, b)
  • The DOM (document.createElement('canvas')) and the WebGL2 context returned by the canvas

PUSHES TO

  • The offscreen WebGL canvas (_canvas), exposed via getCanvas() so the main 2D renderer can composite it with ctx.drawImage
  • The GPU instance buffer via gl.bufferSubData (each flush() call uploads count * FLOATS_PER_INSTANCE floats)
  • console.error on shader compile failure or program link failure (prefixed [FireEngine])

DOES NOT

  • Does not composite itself onto the main canvas — callers must invoke getCanvas() and drawImage it
  • Does not own any world-space simulation state (no fire lifetimes, no fire entities) — every fire instance must be re-queued each frame
  • Does not read the camera or time globals — both are passed in explicitly
  • Does not allocate beyond MAX_INSTANCES — extra add() / addScreen() calls past 64 are silently dropped via early return
  • Does not clear the queue on resize or on missing GL context — flush() clears it after upload, or after a failed init
  • Does not support multiple tint colours per frame — _colorR/G/B is a single uniform applied to all instances in a flush
  • Does not depend on any other engine module — pure self-contained WebGL primitive

Signals

None. The module is a procedural API; it does not emit events.

Entry points

Exported as the FireEngine singleton object with these methods:

  • add(worldX, worldY, width, height, intensity = 0.3) — queue a world-space fire; converted to screen space at queue time using cached camera; fire is anchored so it extends upward from (worldX, worldY)
  • addScreen(screenX, screenY, width, height, intensity = 0.3) — queue a screen-space fire (for HUD); height is internally multiplied by 1.6 and y-offset by -height * 0.3 so the fire extends above the target element
  • setColor(r, g, b) — set the tint colour applied to all subsequent flushes
  • setCamera(x, y, zoom, viewW, viewH) — update cached camera; must be called once per frame before any world-space add()
  • flush(time) — pack the queue into the instance buffer, upload, render one instanced draw call, then clear the queue; lazy-inits the GL context on first call using cached or fallback viewport (800x600)
  • getCanvas() — return the offscreen HTMLCanvasElement (or null if not initialized) for compositing
  • resize(w, h) — update viewport dimensions and resize the offscreen canvas to w * FIRE_SCALE by h * FIRE_SCALE
  • isReady() — returns true once the GL context is initialized

Pattern notes

  • Module-level singleton. All state is held in module-private let bindings; the exported FireEngine object is a thin facade.
  • Lazy init. _init() runs on the first flush() call rather than at module load, so the module is safe to import in non-browser contexts up to that point. Init returns false and the queue is cleared if WebGL2 is unavailable or the viewport is degenerate.
  • Half-resolution offscreen. All queued coordinates are pre-multiplied by FIRE_SCALE (0.5) at queue time; the offscreen canvas is sized at half viewport. The 2D compositor is expected to stretch it back to full size via drawImage.
  • One instanced draw per flush. Uses gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, count) with a 6-vertex unit quad and per-instance attributes a_rect (vec4) and a_alpha (float), each with vertexAttribDivisor = 1.
  • Additive blending. gl.blendFunc(gl.ONE, gl.ONE) so multiple overlapping fires brighten rather than alpha-blend.
  • Premultiplied alpha context. The GL context is created with premultipliedAlpha: true; the fragment shader outputs vec4(col * fire, fire * v_alpha) to match.
  • Hard cap, silent drop. MAX_INSTANCES is enforced via early return in both add() and addScreen(); the 65th-and-beyond fire of a frame is simply discarded.
  • Fire shape baked into fragment shader. The vertical falloff (fade = (1 - v_uv.y)^2), horizontal taper, FBM noise (3 octaves), downward UV scroll (u_time * 0.8), and white-core mix into the tint are all in FIRE_FS. No CPU-side animation state.
  • World-to-screen at queue time, not flush time. add() immediately converts using the cached camera, so out-of-order setCamera() calls within a frame will yield stale coordinates for already-queued fires.
  • Fire emanates downward in UV space, upward in world space. The shader scrolls UV downward so the noise pattern moves with the source; add() offsets sy by -height so the bottom of the rect anchors at worldY.
  • No GL resource cleanup. Once initialized, the GL context, program, VAO, and buffers persist for the lifetime of the page; resize() only adjusts canvas dimensions.