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 — imports Layer and LayerBlendMode types (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 validateBackgroundDef before persisting or baking.

DOES NOT

  • Does NOT enforce that Layer.shader references a tile-template id (vs a VFX template id). Comment on BackgroundDef.layers explicitly defers this to “the template registry is the gate” — the compositor doesn’t check.
  • Does NOT validate the contents of Layer.params beyond 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: id non-empty string, name non-empty string, category in CATEGORIES, tileMode in TILE_MODES, layers non-empty array (each via validateLayer), palette (via validatePalette), bake (via validateBake).
  • BACKGROUND_BAKE_SIZES — re-exported tuple [512, 1024, 2048]; consumers use it both as a runtime allowlist and as the source for the BackgroundBakeSize literal type.

Pattern notes

  • Crash-on-bad-data at the boundary. Validator is the gate; downstream code can treat BackgroundDef as 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 via as Record<string, unknown> only after the typeof === '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 from layers; size enum is the runtime allowlist BACKGROUND_BAKE_SIZES.

EXTRACT-CANDIDATE

  • validateLayer is near-identical to VFX workbench’s layer validator. Both check the same Layer shape with the same blend-mode enum and opacity bounds. Candidate for a shared validateLayer(l, idx, allowedBlendModes) helper in vfx-workbench/component-schema (or a sibling shared-layer-validator.ts), parameterized only by the blend-mode allowlist. The current duplication means a new LayerBlendMode variant must be added in two places.
  • ValidationResult discriminated union is reusable. Same shape appears in any boundary validator across the engine; could move to a shared engine/validation.ts alongside small helpers like isFiniteNumber(n) and isNonEmptyString(s).
  • tileMode: 'wang' is type-allowed but not implemented. Either restrict the type to 'seamless' until sub-project 3 lands, or add a validateBackgroundDef rejection for wang so callers fail loudly instead of silently baking a non-wang surface.