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
BackgroundBakeResultinterface:{ size, pngBytes, gridW, gridH, cols, rows, frameCount, fps }.bakeBackgroundOneSize(def, size)— bakes a single resolution; returns one result.bakeBackgroundAllSizes(def)— iteratesdef.bake.sizessequentially, returns all results.- Internal helpers
canvasToPngBytes()andblitFramePixels().
READS FROM
../vfx-workbench/compositor—LayerCompositor(size-x-size FBO stack withwrap: 'repeat')../shader-templates/registry—getTileTemplate(custom template resolver for tile-authored shaders)../background-schema— typesBackgroundBakeSize,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
HTMLCanvasElementper 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-looptNorm = i / frameCount.
Signals
Tileability invariants
LayerCompositoris constructed withwrap: 'repeat'so accumulator FBOs sample seamlessly across edges.- Shader templates come from
TILE_TEMPLATESregistry (getTileTemplate), authored to wrap in uv-space and close inu_time. - Frame
iusestNorm = i / frameCount(NOTi / (N-1)), so frameNwould equal frame0— guarantees clean playback loop.
Grid layout
cols = ceil(sqrt(frameCount)),rows = ceil(frameCount / cols).gridW = cols * size,gridH = rows * size.- Cell
(col, row)for framei:col = i % cols,row = floor(i / cols).
Pixel orientation
blitFramePixelsflips Y:srcY = tileSize - 1 - yper row, because WebGLreadPixelsreturns bottom-up but 2D canvasputImageDataexpects 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
LayerCompositorand disposes viacompositor.destroy()infinally. WebGL resources never leak even ifreadPixelsorcanvasToPngBytesthrows. - PNG encoding fallback.
canvasToPngBytespreferscanvas.toBlob(async, no string allocation); falls back totoDataURL+ base64 decode (atobin browser,Buffer.fromin Node) whentoBlobis unavailable. - No tests in module. Pure side-effectful render pipeline; verification is visual via the workbench UI.
- Sequential awaits.
bakeBackgroundAllSizesuses afor...ofwithawait— 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
canvasToPngBytesandblitFramePixelsare generic 2D-canvas utilities — duplicated patterns likely exist in other bake/atlas writers (vfx-workbench, sprite atlasers). Consider extracting toengine/util/canvas-png.tsonce a second caller appears.- Grid math (
cols,rows,gridW,gridH, cell coordinate from index) is the same shape used by any flipbook atlas — candidate forengine/util/flipbook-grid.tsif another flipbook baker lands. - Y-flip blit logic is WebGL-readPixels-specific and may belong next to the compositor that produces those pixels.