bake.ts
PURPOSE
Bakes a ComponentDef (kind 'baked') into a frame-grid PNG atlas plus a manifest entry. Renders each animation frame off-screen via either the multi-layer LayerCompositor (when baked.layers.length > 1) or the legacy single-shader PreviewSurface fast path, packs frames into a cols × rows grid sized by ceil(sqrt(frameCount)), encodes to PNG, and returns a BakeResult consumable by the atlas pipeline.
OWNS
bakeComponent(def: ComponentDef): Promise<BakeResult>— sole public export (plus type re-export ofComponentDef).- Module-private
canvasToPngBytes(canvas)—toBlob-preferred PNG encoder withtoDataURL+ chunked-decode fallback for jsdom. - Module-private
blitFramePixels(ctx, px, tileSize, dstX, dstY)— Y-flipping RGBA blit from WebGL bottom-up pixels into the 2D grid canvas at tile coordinates. - Re-export of
ComponentDef(back-compat shim; new code imports fromcomponent-schema.ts).
READS FROM
./preview-surface→PreviewSurface(single-shader path)../compositor→LayerCompositor(multi-layer path)../component-schema→migrateBakedToV2,ComponentDef(forces v2 baked schema)../shader-templates/registry→TemplateId(type only)../atlas-format→BakeResult(return type).def.kind,def.baked,def.id,def.name,def.category,def.anchorsfrom the input def.- After migration:
baked.bake.{frameCount, fps, tileSize, loopMode},baked.layers[],baked.palette,baked.postChain, plus legacybaked.shader/baked.params.
PUSHES TO
Returns a BakeResult:
pngBytes: Uint8Array— PNG-encoded grid atlas.gridW,gridH— atlas pixel dimensions (cols * tileSize,rows * tileSize).manifestEntry—{ componentId, name, category, kind: 'baked', frameCount, fps, tileSize, gridCols, gridRows, loopMode, pivot: { x: tileSize/2, y: tileSize/2 }, anchors, palette, shaderTemplate, shaderParams, atlasPath: "${category}/${id}.png" }.
No filesystem writes, no global state mutation, no network I/O. Caller is responsible for persisting pngBytes at atlasPath.
DOES NOT
- Does not write files or upload to any store.
- Does not validate that the def is well-formed beyond the
kind === 'baked' && bakedguard and the shader-presence check on the single-shader path. - Does not handle
kind !== 'baked'components — throws. - Does not retry on transient WebGL/canvas failures; surface/compositor errors propagate.
- Does not deduplicate atlas paths or check for collisions.
- Does not honor any pivot override — pivot is hard-coded to tile center.
Signals
frameCount,fps,tileSize,loopModedrive grid sizing and thetNormschedule.loopMode === 'loop' || 'parameterized'→tNorm = i / frameCount(open interval, last frame ≠ first).- Otherwise (e.g.
'once') →tNorm = i / max(frameCount - 1, 1)(closed interval, last frame att = 1). useCompositor = Array.isArray(baked.layers) && baked.layers.length > 1selects the path.- Single-layer v2 stacks intentionally fall through to the
PreviewSurfacefast path to skip compose overhead.
Entry points
bakeComponent(def)— called by the bake pipeline (CLI / build script / workbench preview “bake all”).
Pattern notes
- Canvas creation order is load-bearing. Tests mock
document.createElement('canvas')with a call-index switch that expects (render-surface first, gridCanvas second). The two branches preserve this order — do not reorder. - PNG encoding avoids
String.fromCharCode(...spread)because spreading a >~1MB byte buffer blows the call stack. Preferscanvas.toBlob→arrayBuffer(); jsdom fallback decodes base64 byte-by-byte in a loop. - Y-flip in
blitFramePixelscompensates for WebGLreadPixelsreturning bottom-up rows; the destination 2D canvas is top-down. - Migration on entry.
migrateBakedToV2(def.baked)runs unconditionally — downstream code only sees v2 shape. - Manifest back-compat.
shaderTemplateandshaderParamsmirror the bottom layer (baked.layers[0]) for v2 stacks, falling back to legacybaked.shader/baked.params. Consumers expecting the legacy single-shader fields keep working. - Try/finally around surface/compositor. Both paths wrap rendering in
try { ... } finally { surface.destroy() / compositor.destroy() }to release GL resources even on throw. gridCanvasis allocated inside each branch, not hoisted, to keep the createElement call order documented above.- Single-shader path tolerates array uniforms (vec2/3/4) because
PreviewSurface.setUniformsdispatches by.length— even though stack-originated params can include arrays, the legacy path accepts them.
EXTRACT-CANDIDATE
canvasToPngBytes— generic enough that other off-screen-canvas → PNG callers (e.g. screenshot, atlas debug dump) would benefit. Candidate forengine/vfx-workbench/canvas-png.tsor a broaderengine/util/.blitFramePixels— Y-flipping WebGL→2D blit is reusable wherever WebGL readback meets a 2D atlas; same extraction target.tNormframe-schedule formula (loop/parameterizedvs. closed interval) is duplicated between the two branches — candidate for a helperframeTime(i, frameCount, loopMode).