background-schema.ts
PURPOSE
Schema + validator for tileable procedural background definitions. Mirrors the VFX workbench Layer stack shape but adds two contracts every tile-shader template must honor: (1) spatial tileability — uv ∈ [0,1] wraps cleanly with no derivative discontinuities at the seams; (2) temporal loop closure — frame N equals frame 0, with u_time arriving normalized in [0,1] and shaders animating via sin/cos at integer frequencies of 2π·u_time. Bake outputs are large (1024 default) because a single tile fills the screen at runtime.
OWNS
- Types:
BackgroundCategory,TileMode,BackgroundBakeSize,BackgroundBake,BackgroundDef,ValidationResult. - Exported constant:
BACKGROUND_BAKE_SIZES = [512, 1024, 2048] as const. - Internal constants:
CATEGORIES,TILE_MODES,LAYER_BLEND_MODES,FRAME_MIN=1,FRAME_MAX=128,PALETTE_STOP_COUNT=4,PALETTE_VEC3_LEN=3. - Internal validators:
validateLayer,validatePalette,validateBake. - Exported function:
validateBackgroundDef(def: unknown): ValidationResult.
READS FROM
../vfx-workbench/component-schema— importsLayerandLayerBlendModetypes (the layer stack shape is shared with VFX).
PUSHES TO
- Nothing at runtime. Pure types + a pure validator function. Consumers (workbench UI, bake pipeline, template registry) import the types and call
validateBackgroundDefbefore persisting or baking.
DOES NOT
- Does NOT enforce that
Layer.shaderreferences a tile-template id (vs a VFX template id). Comment onBackgroundDef.layersexplicitly defers this to “the template registry is the gate” — the compositor doesn’t check. - Does NOT validate the contents of
Layer.paramsbeyond requiring it be an object. - Does NOT bake, render, or compose anything. No GL state, no canvas, no DOM.
- Does NOT implement
wang-mode tiling.tileMode: 'wang'is accepted by the validator but flagged as “sub-project 3”; v1 ships'seamless'only. - Does NOT clamp or normalize values — invalid input returns
{ ok: false, reason }, never a coerced default.
Signals
ValidationResult is a discriminated union: { ok: true } | { ok: false; reason: string }. Every validator returns this shape. First failure short-circuits — validateBackgroundDef walks layers in order and returns on the first bad layer without checking palette/bake.
Reasons are human-readable error strings keyed by field path (e.g. "layers[2]: opacity must be 0..1", "palette[1] has non-finite number", "bake.frameCount must be 1..128").
Entry points
validateBackgroundDef(def: unknown)— top-level gate. Checks:idnon-empty string,namenon-empty string,categoryinCATEGORIES,tileModeinTILE_MODES,layersnon-empty array (each viavalidateLayer),palette(viavalidatePalette),bake(viavalidateBake).BACKGROUND_BAKE_SIZES— re-exported tuple[512, 1024, 2048]; consumers use it both as a runtime allowlist and as the source for theBackgroundBakeSizeliteral type.
Pattern notes
- Crash-on-bad-data at the boundary. Validator is the gate; downstream code can treat
BackgroundDefas trusted. No silent fallbacks inside the validator — every branch either returns{ok:true}or{ok:false,reason}. - No magic numbers. Frame range, palette shape, vec3 length all named constants at file top.
- Type assertion via runtime check. Validator takes
unknown, walks the shape, and assigns viaas Record<string, unknown>only after thetypeof === 'object'guard. Same pattern repeated for nested objects. - Layer validation is duplicated from VFX schema but doesn’t import the VFX validator — it inlines the field checks (
id,name,enabled,shader,params,blend,opacity). Likely an EXTRACT-CANDIDATE. - Palette is a fixed-shape vec3 grid (4 stops × 3 channels), not a flexible gradient — matches the cosine-palette convention shared with VFX.
- Bake config is independent of layer stack.
BackgroundBake(sizes, frameCount, fps, seed) is validated separately fromlayers; size enum is the runtime allowlistBACKGROUND_BAKE_SIZES.
EXTRACT-CANDIDATE
validateLayeris near-identical to VFX workbench’s layer validator. Both check the sameLayershape with the same blend-mode enum and opacity bounds. Candidate for a sharedvalidateLayer(l, idx, allowedBlendModes)helper invfx-workbench/component-schema(or a siblingshared-layer-validator.ts), parameterized only by the blend-mode allowlist. The current duplication means a newLayerBlendModevariant must be added in two places.ValidationResultdiscriminated union is reusable. Same shape appears in any boundary validator across the engine; could move to a sharedengine/validation.tsalongside small helpers likeisFiniteNumber(n)andisNonEmptyString(s).tileMode: 'wang'is type-allowed but not implemented. Either restrict the type to'seamless'until sub-project 3 lands, or add avalidateBackgroundDefrejection forwangso callers fail loudly instead of silently baking a non-wang surface.