palette.ts

PURPOSE

Iñigo Quílez-style cosine palette primitives for the VFX workbench. Provides the 4-vec3 palette type (Palette4), evaluation at parameter t, flatten-for-uniform helper, and hex>vec3 round-trip conversion. Pure functions, no state, no DOM.

OWNS

  • Vec3 type — [number, number, number], each channel 0..1.
  • Palette4 type — tuple of four Vec3 rows (a, b, c, d) matching the cosine-palette formula color(t) = a + b * cos(2π * (c*t + d)).
  • DEFAULT_PALETTE — Quílez canonical neutral grey-to-rainbow palette: a=(0.5, 0.5, 0.5), b=(0.5, 0.5, 0.5), c=(1, 1, 1), d=(0, 0.33, 0.67).

READS FROM

  • Nothing. Module is dependency-free.

PUSHES TO

  • Nothing. Consumers pull the helpers.

DOES NOT

  • Does not bind a WebGL uniform — flattenPalette only produces the 12-number array; the caller uploads it.
  • Does not validate Palette4 shape; trusts the type.
  • Does not clamp inputs to evalPalette (output may exceed 0..1 for extreme b).
  • Does not handle 3-digit shorthand hex, alpha channel, or named CSS colors — hexToVec3 requires exactly 6 hex digits after stripping # and throws otherwise.
  • Does not normalize parameter t (caller decides if it wraps, loops, or extrapolates).

Signals

  • Throws Error('invalid hex: <hex>') when hexToVec3 receives a string whose post-# length is not 6.
  • vec3ToHex silently clamps each channel to 0..1 before rounding to 0..255 — out-of-range input is folded, not flagged.

Entry points

  • flattenPalette(p: Palette4): number[] — spreads the 4 rows into a flat 12-number array for gl.uniform3fv / shader upload.
  • evalPalette(p: Palette4, t: number): Vec3 — sample the palette at scalar t; applies the cosine-palette formula per channel.
  • vec3ToHex(v: Vec3): string[0..1, 0..1, 0..1]#rrggbb (clamps, rounds, pads to 2 hex digits).
  • hexToVec3(hex: string): Vec3#rrggbb or rrggbb[0..1, 0..1, 0..1].
  • DEFAULT_PALETTE — exported constant, safe to use as a seed value.

Pattern notes

  • Cosine palette math: color(t) = a + b * cos(2π * (c*t + d)). a is base offset, b is amplitude, c is frequency, d is per-channel phase. The default values produce a smooth rainbow as t sweeps 0..1.
  • flattenPalette order is row-major (a.xyz, b.xyz, c.xyz, d.xyz) — matches the layout a shader expects when reading uniform vec3 palette[4].
  • vec3ToHex clamps before rounding so callers don’t have to pre-clamp; evalPalette does not, so the two are not symmetric round-trip unless inputs stay in range.
  • Hex round-trip is lossy: a vec3 holds floats, hex holds 8-bit channels. hexToVec3(vec3ToHex(v)) only equals v up to 1/255 per channel.

EXTRACT-CANDIDATE

  • Vec3 type is duplicated across many engine modules (rendering, vfx, particles). A shared engine/types/vec.ts would deduplicate, but the [number, number, number] alias is so trivial that local declaration is fine.
  • vec3ToHex / hexToVec3 are general-purpose color conversion utilities that could live in a shared engine/color/ module if any other system grows hex-conversion needs. Today they are only used by the workbench palette UI.
  • The cosine-palette formula (a + b * cos(2π * (c*t + d))) is the Iñigo Quílez canonical form — well-known enough that re-implementations elsewhere would also follow this shape and could share evalPalette.