PURPOSE

Pure deterministic function that turns three numeric parameters (temperature, brightness, saturation) plus an optional hue_bias and an id string into a fully resolved 7-slot Palette. All math runs in OkLCh perceptual color space so equal lightness steps read as equal brightness across hues, then converts to sRGB hex at the end. Same inputs always produce the same 7 hexes — no randomness, no state.

OWNS

  • generatePalette(id, params) — the sole exported function; returns a Palette with the input params echoed plus seven hex strings.
  • SLOT_RECIPE — the fixed internal shape of every palette: per-slot lOff (lightness offset from brightness-driven center), hueRot (degrees rotated from base hue), and cMul (chroma multiplier against the saturation parameter). Defines the contract that all three background slots rotate +180° to the complementary hue while terrain and the shadow/midtone bridge stay at the core hue.
  • MAX_CHROMA = 0.18 — the OkLCh chroma ceiling at saturation=1, chosen because higher chroma starts falling out of sRGB gamut and clips.
  • HUE_STOPS = [220, 270, 320, 20] — the cyan→violet→magenta→red-orange arc that temperature walks. The desaturated green band is intentionally skipped.
  • hueCenter(temperature, hueBias) — resolves the base hue. If hueBias is set, normalizes it to 0..360 and returns it directly. Otherwise interpolates along HUE_STOPS using shortest-arc lerp so 320→20 crosses through red, not backward through cyan.
  • oklchToHex(L, C, hDeg) — full OkLCh → OkLab → LMS → linear sRGB → gamma-encoded sRGB hex pipeline using Björn Ottosson’s 2020 constants.
  • linearToSrgb(x) — clamped sRGB gamma encode (piecewise: linear below 0.0031308, power 1/2.4 above).
  • clamp(x, lo, hi) — local utility.

READS FROM

  • PaletteParams and Palette type definitions from ./palette-types.
  • The four input fields on PaletteParams: temperature, brightness, saturation, hue_bias.
  • The id string passed alongside the params.

No global state, no config files, no other modules. Fully pure.

PUSHES TO

Nothing. Returns a Palette value to its caller and exits. Does not mutate inputs, does not register anywhere, does not log.

DOES NOT

  • Does not apply preset overrides — that is the caller’s job (the palette system layers PalettePreset.overrides on top of generator output).
  • Does not pick which palette is active or which preset to use.
  • Does not cache. Every call recomputes from scratch; callers that want memoization wrap it themselves.
  • Does not validate id uniqueness, does not enforce any naming convention on it.
  • Does not clamp temperature or saturation inputs hard — only brightness (clamped -1..1 inside) and saturation (clamped 0..1 inside) and the per-slot L (clamped 0.04..0.96) get clamped. Out-of-range temperature walks off the end of the hue arc.
  • Does not handle the green hue band — by design, the cool→violet→magenta→red arc routes around it; reaching green requires hue_bias.
  • Does not gamut-map. Slots that would exceed sRGB get clamped channel-wise in linearToSrgb, which can flatten color near gamut edges.

Signals

None. No event emission, no telemetry, no console output, no callback registration.

Entry points

  • generatePalette(id: string, params: PaletteParams): Palette — the only export. Called by the palette system when resolving a preset’s parameters into final slot hexes.

Pattern notes

  • Pure-function module: zero imports beyond a type-only import, zero side effects, no class wrapper. Trivial to test by comparing returned hex strings.
  • Color math kept in OkLCh deliberately — comment block at top justifies the choice against HSL (HSL equal-L hues are not equal-brightness, which would wreck contrast budgets between slots).
  • The 7-slot SLOT_RECIPE is declared as const and treated as the immutable shape of every palette. The slot loop builds each output by feeding { lOff, hueRot, cMul } through a closure that reads lCenter, c, and h from the outer scope.
  • Contrast contract is enforced via the recipe, not via runtime checks: terrain slots stay at the core hue, all three bg_* slots rotate +180°, and the L offsets guarantee a ≥0.18 L gap between terrain_base and bg_haze. shadow and midtone sit at the core hue at low chroma to act as a tonal bridge between the terrain and its complementary backdrop.
  • Brightness maps linearly into a ±0.25 window around L=0.5, so the brightness parameter shifts the whole L window rather than stretching it — slot-to-slot contrast stays constant regardless of overall brightness.
  • The hue_bias escape hatch lets presets pin a specific hue (e.g. green, yellow) outside the arc; when used, temperature is documented as biasing complementary accents, but in the current implementation hue_bias overrides hue entirely and temperature has no further effect on hue.
  • Hex output is lowercased and zero-padded via padStart(2, '0'); round-trip stable.