Colour Palettes

When code needs more than one colour (chart series, icon plates, avatars, heat maps, status ramps), it asks CuspPalette in MeshWeaver.Layout.Palettes for them. Don't hand-pick hex lists, and don't compute HSL. The engine is pure and deterministic, has no state and needs no hub, so the same call gives the same colours on every process and every deploy.

Why not HSL or a hand-picked list

For each hue, the colours a screen can show form a rough triangle: black, white, and the cusp, the most saturated colour of that hue. The cusp sits in very different places: yellow's is almost white (OKLab L 0.97), blue's is dark (L 0.45). HSL ignores this. At the same HSL "lightness", yellow is about six times brighter than blue, and a fixed saturation leaves some hues washed out and pushes others out of gamut. Hand-picked lists inherit the same unevenness from whoever picked them.

CuspPalette works in OKLCH (Björn Ottosson's OKLab in polar form), where equal lightness steps look equal. It draws each colour from a smooth curve inside that hue's triangle. The method is Wijffelaars et al., Generating Color Palettes using Intuitive Parameters (EuroVis 2008), as reformulated for OKLCH by David Aerne's CuspHanger (MIT, © David Aerne). The curve construction and the default parameters follow CuspHanger.

The guarantees, each pinned by CuspPaletteTest:

The API

using MeshWeaver.Layout.Palettes;

// The primitive: the colour on a hue's curve at a given lightness.
OklchColor c = CuspPalette.ColorAt(hue: 255, lightness: 0.6, saturation: 0.8);

// Sequential: one hue, dark to light. Brightness and contrast place the lightness range.
var ramp = CuspPalette.Sequential(7, hue: 255);
var ranged = CuspPalette.SequentialRange(5, hue: 145, minLightness: 0.3, maxLightness: 0.95);

// Diverging: two arms meeting at a light centre (odd counts get a mixed centre colour).
var div = CuspPalette.Diverging(7, hueStart: 25, hueEnd: 255);

// Categorical: distinct hues at ONE lightness; optionally inside an arc of the wheel.
var series = CuspPalette.Categorical(8);                        // L 0.62, starts at blue
var earth = CuspPalette.Categorical(6, 0.52, 0.45, startHue: 40, hueSpan: 110);

// Contrast: walk a colour's lightness until text of another colour reaches a WCAG ratio.
var plate = CuspPalette.EnsureContrast(c, OklchColor.FromHex("#ffffff"), 4.5);

string hex = c.ToHex();   // "#rrggbb", clipped to sRGB
string css = c.ToCss();   // "oklch(60% 0.18 255)", culture-invariant
string[] fills = series.ToHex();

The parameters:

Parameter Range Meaning
saturation 0–1 0 is a grey ramp, 1 runs the curve through the cusp itself. Default 0.6.
brightness 0–1 Where a ramp's lightness range sits. Default 0.75.
contrast 0–1 How wide a ramp's lightness range is. The default grows with the step count: min(0.88, 0.34 + 0.06·n).
lightness 0–1 OKLab L. 0.62 suits series marks on both themes; about 0.5 or below carries white text.

OklabGamut.Cusp(hue) and OklabGamut.MaxChroma(lightness, hue) expose the gamut itself. OklchColor converts to and from hex and linear sRGB, and reports RelativeLuminance and ContrastRatio as WCAG defines them.

Where the platform uses it

When you add a new surface that needs several colours, call the engine. Don't copy its output into a literal list: a copied list stops following the engine, and the next agent tunes one copy and not the other.

One numeric detail

OKLab's sRGB boundary is not perfectly straight near blue. Along the edge from black to #0000ff, the red channel dips to about −0.0007 (linear). A strict gamut test therefore cuts a sliver off blue's cusp, reporting a chroma of 0.264 instead of 0.313. OklchColor.GamutTolerance (0.001, linear) accepts that sliver. It is far below one 8-bit step, and ToHex clips it away.