bake-background.ts

PURPOSE

Multi-resolution flipbook bake for a BackgroundDef. For each size in def.bake.sizes, renders frameCount frames at that resolution and composites them into a single PNG grid (cols x rows = ceil(sqrt(N)) x ceil(N/cols)). Returns one BackgroundBakeResult per size; caller writes PNGs to disk via /__dev/atlas-write or data: URLs.

OWNS

  • BackgroundBakeResult interface: { size, pngBytes, gridW, gridH, cols, rows, frameCount, fps }.
  • bakeBackgroundOneSize(def, size) — bakes a single resolution; returns one result.
  • bakeBackgroundAllSizes(def) — iterates def.bake.sizes sequentially, returns all results.
  • Internal helpers canvasToPngBytes() and blitFramePixels().

READS FROM

  • ../vfx-workbench/compositorLayerCompositor (size-x-size FBO stack with wrap: 'repeat').
  • ./shader-templates/registrygetTileTemplate (custom template resolver for tile-authored shaders).
  • ./background-schema — types BackgroundBakeSize, BackgroundDef (bake.frameCount, bake.fps, bake.sizes, layers, palette).

PUSHES TO

  • Returns BackgroundBakeResult[] to caller (no direct disk/network writes from this module).
  • Allocates a transient HTMLCanvasElement per size, drawn into via 2D context.

DOES NOT

  • Does not write PNGs to disk or POST to /__dev/atlas-write — caller does.
  • Does not parallelize across sizes — sizes are baked sequentially (one compositor lifecycle per size).
  • Does not validate BackgroundDef; trusts schema upstream.
  • Does not handle the i / (N-1) (open-loop) case — always uses closed-loop tNorm = i / frameCount.

Signals

Tileability invariants

  • LayerCompositor is constructed with wrap: 'repeat' so accumulator FBOs sample seamlessly across edges.
  • Shader templates come from TILE_TEMPLATES registry (getTileTemplate), authored to wrap in uv-space and close in u_time.
  • Frame i uses tNorm = i / frameCount (NOT i / (N-1)), so frame N would equal frame 0 — guarantees clean playback loop.

Grid layout

  • cols = ceil(sqrt(frameCount)), rows = ceil(frameCount / cols).
  • gridW = cols * size, gridH = rows * size.
  • Cell (col, row) for frame i: col = i % cols, row = floor(i / cols).

Pixel orientation

  • blitFramePixels flips Y: srcY = tileSize - 1 - y per row, because WebGL readPixels returns bottom-up but 2D canvas putImageData expects top-down.

Entry points

  • bakeBackgroundOneSize(def: BackgroundDef, size: BackgroundBakeSize): Promise<BackgroundBakeResult> — single-size bake.
  • bakeBackgroundAllSizes(def: BackgroundDef): Promise<BackgroundBakeResult[]> — full multi-resolution bake.

Pattern notes

  • Try/finally compositor lifecycle. Each size creates its own LayerCompositor and disposes via compositor.destroy() in finally. WebGL resources never leak even if readPixels or canvasToPngBytes throws.
  • PNG encoding fallback. canvasToPngBytes prefers canvas.toBlob (async, no string allocation); falls back to toDataURL + base64 decode (atob in browser, Buffer.from in Node) when toBlob is unavailable.
  • No tests in module. Pure side-effectful render pipeline; verification is visual via the workbench UI.
  • Sequential awaits. bakeBackgroundAllSizes uses a for...of with await — intentional to avoid concurrent GL contexts.
  • Caller contract. Caller must run inside a DOM with document.createElement('canvas') and a usable 2D context — not Node-headless without jsdom.

EXTRACT-CANDIDATE

  • canvasToPngBytes and blitFramePixels are generic 2D-canvas utilities — duplicated patterns likely exist in other bake/atlas writers (vfx-workbench, sprite atlasers). Consider extracting to engine/util/canvas-png.ts once a second caller appears.
  • Grid math (cols, rows, gridW, gridH, cell coordinate from index) is the same shape used by any flipbook atlas — candidate for engine/util/flipbook-grid.ts if another flipbook baker lands.
  • Y-flip blit logic is WebGL-readPixels-specific and may belong next to the compositor that produces those pixels.