Files
Gabriel Fontes c9b492a141 feat(lib): add TonalPalette + palette sets (brick 3)
Port material-colors 0.4.2 palettes: tonalPalette hue chroma -> tone(t) via the
HCT solver (with the yellow tone-99 average quirk), corePalette (matugen's
palettes field: primary max(48,chroma), sec 16, tert hue+60/24, neutral 4,
neutral-variant 8, error 25/84), and rainbowPalettes (scheme palettes for the
role layer: neutral/neutral-variant chroma 0).

Validated: corePalette reproduces matugen's palettes JSON for #2B3975
bit-exact across all 6 palettes x 18 tones (108/108).

Assisted-by: pi (opus-4.8)
2026-07-04 17:35:24 -03:00

67 lines
2.0 KiB
Nix

# TonalPalette + the palette sets for a source color, ported from
# material-colors 0.4.2 (the version matugen 4.0 pins).
# - corePalette: matches matugen's `palettes` JSON output (CorePalette.of).
# - rainbowPalettes: the rainbow *scheme* palettes, consumed by the role layer.
{lib}: let
h = import ./hct.nix {inherit lib;};
m = import ./math.nix {inherit lib;};
inherit (builtins) floor;
isYellow = hue: hue >= 105.0 && hue < 125.0;
sanitizeDeg = d: let r = m.fmod d 360.0; in
if r < 0.0
then r + 360.0
else r;
max = a: b:
if a > b
then a
else b;
# average two "#rrggbb" per channel (used for the yellow tone-99 quirk)
averageHex = h1: h2: let
a = h.hexToRgb h1;
b = h.hexToRgb h2;
avg = x: y: floor ((x + y) / 2.0 + 0.5);
in
h.rgbToHex {
r = avg a.r b.r;
g = avg a.g b.g;
b = avg a.b b.b;
};
tonalPalette = hue: chroma: let
toneOf = t:
if t == 99 && isYellow hue
then averageHex (toneOf 98) (toneOf 100)
else h.hexFromHct {inherit hue chroma; tone = 1.0 * t;};
in {
inherit hue chroma;
tone = toneOf;
};
in rec {
inherit tonalPalette;
# CorePalette.of(source) — matugen's `palettes` field
corePalette = {hue, chroma}: {
primary = tonalPalette hue (max 48.0 chroma);
secondary = tonalPalette hue 16.0;
tertiary = tonalPalette (sanitizeDeg (hue + 60.0)) 24.0;
neutral = tonalPalette hue 4.0;
neutral_variant = tonalPalette hue 8.0;
error = tonalPalette 25.0 84.0;
};
# SchemeRainbow palettes — consumed by the DynamicColor role layer (brick 4)
rainbowPalettes = {hue, chroma}: {
primary = tonalPalette hue 48.0;
secondary = tonalPalette hue 16.0;
tertiary = tonalPalette (sanitizeDeg (hue + 60.0)) 24.0;
neutral = tonalPalette hue 0.0;
neutral_variant = tonalPalette hue 0.0;
error = tonalPalette 25.0 84.0;
};
# convenience: dump a palette as { "<tone>" = "#hex"; ... } for given tones
paletteTones = tones: pal: lib.listToAttrs (map (t: lib.nameValuePair (toString t) (pal.tone t)) tones);
}