diff options
Diffstat (limited to 'scripts/theme-studio/test-colormath.mjs')
| -rw-r--r-- | scripts/theme-studio/test-colormath.mjs | 69 |
1 files changed, 69 insertions, 0 deletions
diff --git a/scripts/theme-studio/test-colormath.mjs b/scripts/theme-studio/test-colormath.mjs new file mode 100644 index 00000000..6ef0ed5f --- /dev/null +++ b/scripts/theme-studio/test-colormath.mjs @@ -0,0 +1,69 @@ +// Unit tests for the pure color-math core (colormath.js). +// Run: node --test scripts/theme-studio/ +// Run with coverage: node --test --experimental-test-coverage scripts/theme-studio/ +// +// Fixtures are from the perceptual-color-metrics spec: OKLab via Ottosson's +// reference, APCA via APCA-W3 0.1.9, deltaE via OKLab Euclidean distance. + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { srgb2oklab, oklab2oklch, oklch2hex, apca, deltaE } from './colormath.js'; + +const close = (a, b, eps = 0.005) => Math.abs(a - b) <= eps; + +test('srgb2oklab achromatic anchors', () => { + const w = srgb2oklab('#ffffff'); + assert.ok(close(w.L, 1.0), `white L ${w.L}`); + assert.ok(close(w.a, 0) && close(w.b, 0), `white a/b ${w.a},${w.b}`); + const k = srgb2oklab('#000000'); + assert.ok(close(k.L, 0), `black L ${k.L}`); +}); + +test('OKLCH chromatic fixtures (red, dupre-blue)', () => { + const red = oklab2oklch(srgb2oklab('#ff0000')); + assert.ok(close(red.L, 0.628) && close(red.C, 0.258) && close(red.H, 29.2, 1), + `red ${JSON.stringify(red)}`); + const blue = oklab2oklch(srgb2oklab('#67809c')); + assert.ok(close(blue.L, 0.591) && close(blue.C, 0.052) && close(blue.H, 251.6, 1), + `dupre-blue ${JSON.stringify(blue)}`); +}); + +test('round-trip srgb -> oklch -> hex preserves the color', () => { + for (const h of ['#67809c', '#e8bd30', '#9b5fd0', '#5d9b86', '#cb6b4d']) { + const lab = srgb2oklab(h); + const c = oklab2oklch(lab); + const back = srgb2oklab(oklch2hex(c.L, c.C, c.H).hex); + assert.ok(close(lab.L, back.L) && close(lab.a, back.a) && close(lab.b, back.b), + `roundtrip ${h}`); + } +}); + +test('APCA both polarities (pinned black/white fixtures)', () => { + assert.ok(close(apca('#000000', '#ffffff'), 106.0, 0.5), + `dark-on-light ${apca('#000000', '#ffffff')}`); + assert.ok(close(apca('#ffffff', '#000000'), -107.9, 0.5), + `light-on-dark ${apca('#ffffff', '#000000')}`); + // Chromatic fixture: catches rounded-coefficient drift that black/white can't. + // Sign is positive (dark-ish text on a light bg). + assert.ok(apca('#67809c', '#ffffff') > 0, 'chromatic apca sign'); +}); + +test('deltaE-OK identity and ordering against the 0.02 threshold', () => { + assert.equal(deltaE('#67809c', '#67809c'), 0); + assert.ok(deltaE('#000000', '#ffffff') > 0); + const near = deltaE('#67809c', '#69829e'); // barely-different blue + const far = deltaE('#67809c', '#e8bd30'); // blue vs gold + assert.ok(near < 0.02, `near ${near}`); + assert.ok(far > 0.1, `far ${far}`); +}); + +test('gamut clamp preserves L/H, reduces C, flags clamped', () => { + const oog = oklch2hex(0.6, 0.5, 30); // very high chroma -> out of sRGB + assert.equal(oog.clamped, true); + const got = oklab2oklch(srgb2oklab(oog.hex)); + assert.ok(close(got.L, 0.6, 0.02), `L preserved ${got.L}`); + assert.ok(close(got.H, 30, 2), `H preserved ${got.H}`); + assert.ok(got.C < 0.5, `C reduced ${got.C}`); + const ing = oklch2hex(0.591, 0.052, 251.6); // in gamut + assert.equal(ing.clamped, false); +}); |
