Skip to content

Commit 1274dec

Browse files
Site navigation: sticky nav across all pages, recipes on-site, npm badge
- Shared sticky nav (demos / accuracy / recipes / npm / github) with current-page highlighting on index, accuracy, and recipes pages - docs/recipes.html generated from RECIPES.md by a committed minimal markdown pipeline (tools/build-recipes-page.js) -- visitors stay on the site instead of bouncing to a GitHub blob - README: npm version badge (live, now that it resolves), recipes link points at the site page - Verified in preview: nav renders on all three pages with correct highlighting, demos still drawing, recipes markdown conversion clean Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 1a63909 commit 1274dec

5 files changed

Lines changed: 306 additions & 5 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
> **A research-grade color laboratory for the web.**
44
> **Every color space, every illuminant — digit-identical in JS, GLSL, and WGSL.**
55
6+
[![npm](https://img.shields.io/npm/v/whitepoint)](https://www.npmjs.com/package/whitepoint)
67
[![CI](https://github.com/johnbxx/whitepoint/actions/workflows/ci.yml/badge.svg)](https://github.com/johnbxx/whitepoint/actions/workflows/ci.yml)
78
&nbsp;[**live demos**](https://johnbxx.github.io/whitepoint/)
89
· [**measured accuracy**](https://johnbxx.github.io/whitepoint/accuracy.html)
9-
· [**recipes**](./RECIPES.md)
10+
· [**recipes**](https://johnbxx.github.io/whitepoint/recipes.html)
1011
· [north star](./NORTHSTAR.md)
1112

1213
Every conversion is generated from one table of cited constants. The matrix that

docs/accuracy.html

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,26 @@
2727
.legend i { display: inline-block; width: 18px; height: 3px; vertical-align: middle; margin-right: 6px; border-radius: 2px; }
2828
code { font-family: var(--mono); font-size: 0.85em; background: var(--panel); padding: 0.1em 0.4em; border-radius: 5px; }
2929
.note { color: var(--dim); font-size: 0.85rem; margin-top: 0.75rem; }
30+
nav { position: sticky; top: 0; background: rgba(12,13,16,0.85); backdrop-filter: blur(8px); border-bottom: 1px solid var(--edge); z-index: 10; }
31+
.navin { max-width: 880px; margin: 0 auto; display: flex; justify-content: space-between; align-items: baseline; padding: 0.65rem 1.25rem; }
32+
.brand { font-weight: 650; color: var(--text); text-decoration: none; }
33+
.navlinks a { color: var(--dim); margin-left: 1.1rem; font-size: 0.9rem; text-decoration: none; }
34+
.navlinks a:hover, .navlinks a.here { color: var(--text); }
3035
</style>
3136
</head>
3237
<body>
38+
<nav><div class="navin">
39+
<a class="brand" href="./index.html">whitepoint</a>
40+
<div class="navlinks">
41+
<a href="./index.html">demos</a>
42+
<a class="here" href="./accuracy.html">accuracy</a>
43+
<a href="./recipes.html">recipes</a>
44+
<a href="https://www.npmjs.com/package/whitepoint">npm</a>
45+
<a href="https://github.com/johnbxx/whitepoint">github</a>
46+
</div>
47+
</div></nav>
3348
<main>
3449
<header>
35-
<p class="crumb"><a href="./index.html">← whitepoint</a></p>
3650
<h1>Measured accuracy</h1>
3751
<p class="sub">Every chart on this page is generated from the repository's own
3852
measurement scripts (<code>tools/cusp-accuracy.js</code>,

docs/index.html

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,31 @@
5757
a { color: var(--accent); text-decoration: none; }
5858
a:hover { text-decoration: underline; }
5959
footer { margin-top: 5rem; color: var(--dim); font-size: 0.85rem; }
60+
nav { position: sticky; top: 0; background: rgba(12,13,16,0.85); backdrop-filter: blur(8px); border-bottom: 1px solid var(--edge); z-index: 10; }
61+
.navin { max-width: 880px; margin: 0 auto; display: flex; justify-content: space-between; align-items: baseline; padding: 0.65rem 1.25rem; }
62+
.brand { font-weight: 650; color: var(--text); text-decoration: none; }
63+
.navlinks a { color: var(--dim); margin-left: 1.1rem; font-size: 0.9rem; text-decoration: none; }
64+
.navlinks a:hover, .navlinks a.here { color: var(--text); }
6065
</style>
6166
</head>
6267
<body>
68+
<nav><div class="navin">
69+
<a class="brand" href="./index.html">whitepoint</a>
70+
<div class="navlinks">
71+
<a class="here" href="./index.html">demos</a>
72+
<a href="./accuracy.html">accuracy</a>
73+
<a href="./recipes.html">recipes</a>
74+
<a href="https://www.npmjs.com/package/whitepoint">npm</a>
75+
<a href="https://github.com/johnbxx/whitepoint">github</a>
76+
</div>
77+
</div></nav>
6378
<main>
6479
<header>
6580
<h1><span class="wp">whitepoint</span><br>a research-grade color laboratory for the web</h1>
6681
<p class="tagline">Every color space, every illuminant — digit-identical in JS, GLSL, and WGSL.
6782
Every constant cited, every matrix derived, every claim measured. Every color on this page
6883
is computed by the library, right now, in your browser.</p>
6984
<pre>npm install whitepoint</pre>
70-
<p style="margin-top:0.6rem; font-size:0.9rem"><a href="./accuracy.html">measured accuracy →</a>
71-
&nbsp;·&nbsp; <a href="https://github.com/johnbxx/whitepoint/blob/main/RECIPES.md">recipes →</a>
72-
&nbsp;·&nbsp; <a href="https://github.com/johnbxx/whitepoint">source →</a></p>
7385
<div class="numbers">
7486
<div><b>42</b><span>color spaces</span></div>
7587
<div><b>10⁻¹⁵</b><span>round-trip error</span></div>

docs/recipes.html

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>whitepoint — recipes</title>
7+
<meta name="description" content="Job-to-be-done recipes for the whitepoint color library: gradients, gamut mapping, illuminants, HCT palettes, shaders, spectra, compositing.">
8+
<style>
9+
:root {
10+
--bg: #0c0d10; --panel: #14161b; --edge: #23262e;
11+
--text: #e8eaf0; --dim: #9aa0ae; --accent: #4ba3f7;
12+
--mono: ui-monospace, SFMono-Regular, Menlo, monospace;
13+
}
14+
* { box-sizing: border-box; margin: 0; }
15+
body { background: var(--bg); color: var(--text); font: 16px/1.6 system-ui, -apple-system, sans-serif; padding-bottom: 6rem; }
16+
main { max-width: 880px; margin: 0 auto; padding: 0 1.25rem; }
17+
nav { position: sticky; top: 0; background: rgba(12,13,16,0.85); backdrop-filter: blur(8px); border-bottom: 1px solid var(--edge); z-index: 10; }
18+
.navin { max-width: 880px; margin: 0 auto; display: flex; justify-content: space-between; align-items: baseline; padding: 0.65rem 1.25rem; }
19+
.brand { font-weight: 650; color: var(--text); text-decoration: none; }
20+
.navlinks a { color: var(--dim); margin-left: 1.1rem; font-size: 0.9rem; text-decoration: none; }
21+
.navlinks a:hover, .navlinks a.here { color: var(--text); }
22+
h1 { font-size: 2rem; letter-spacing: -0.02em; padding: 3rem 0 0.25rem; }
23+
h2 { font-size: 1.2rem; margin: 2.4rem 0 0.6rem; letter-spacing: -0.01em; }
24+
p { color: var(--dim); max-width: 46rem; margin-bottom: 0.6rem; }
25+
p strong { color: var(--text); }
26+
ul { color: var(--dim); margin: 0.4rem 0 0.8rem 1.2rem; max-width: 46rem; }
27+
li { margin-bottom: 0.35rem; }
28+
li strong { color: var(--text); }
29+
pre, code { font-family: var(--mono); font-size: 0.85rem; }
30+
pre { background: var(--panel); border: 1px solid var(--edge); border-radius: 10px; padding: 1rem 1.25rem; overflow-x: auto; line-height: 1.5; margin: 0.6rem 0 1rem; }
31+
code.inline { background: var(--panel); padding: 0.1em 0.4em; border-radius: 5px; }
32+
a { color: var(--accent); }
33+
</style>
34+
</head>
35+
<body>
36+
<nav><div class="navin">
37+
<a class="brand" href="./index.html">whitepoint</a>
38+
<div class="navlinks">
39+
<a href="./index.html">demos</a>
40+
<a href="./accuracy.html">accuracy</a>
41+
<a class="here" href="./recipes.html">recipes</a>
42+
<a href="https://www.npmjs.com/package/whitepoint">npm</a>
43+
<a href="https://github.com/johnbxx/whitepoint">github</a>
44+
</div>
45+
</div></nav>
46+
<main>
47+
<h1>Recipes</h1>
48+
<p>Job-to-be-done snippets. Conventions throughout: colors are plain arrays, channels are 0–1 floats (hue in degrees; CIE Lab/Luv L runs 0–100), every function takes an optional <code class="inline">out</code> array for zero-allocation loops.</p>
49+
<h2>Hex in, any space out</h2>
50+
<pre>import { fromHex, toHex, convert } from 'whitepoint';
51+
52+
const oklch = convert(fromHex('#4ba3f7'), 'srgb', 'oklch');
53+
// → [0.6976, 0.1334, 250.4]
54+
toHex(convert([0.6976, 0.1334, 250.4], 'oklch', 'srgb')); // '#4ba3f7'</pre>
55+
<h2>A perceptual gradient (CSS output)</h2>
56+
<pre>import { mix, convert, serialize, toGamut } from 'whitepoint';
57+
58+
const a = convert(fromHex('#0b3d91'), 'srgb', 'oklch');
59+
const b = convert(fromHex('#f7b32b'), 'srgb', 'oklch');
60+
const stops = Array.from({ length: 9 }, (_, i) =&gt; {
61+
const c = mix(a, b, i / 8, 'oklch'); // CSS Color 4 §12 hue arcs
62+
return serialize(toGamut(c, 'oklch'), 'oklch'); // gamut-safe css strings
63+
});</pre>
64+
<h2>The same gradient, in your fragment shader</h2>
65+
<pre>import { glslMix, glsl } from 'whitepoint/codegen';
66+
67+
const shaderSrc = `
68+
${glsl('oklch', 'srgb')} // vec3 wp_oklch_to_srgb(vec3 c)
69+
${glslMix('oklch')} // vec3 wp_mix_oklch_shorter(vec3 a, vec3 b, float t)
70+
// in main(): color = wp_oklch_to_srgb(wp_mix_oklch_shorter(A, B, t));
71+
`;
72+
// constants are digit-identical to the JS above — same tables, parity-tested</pre>
73+
<h2>Display a wide-gamut color on an sRGB screen</h2>
74+
<pre>import { toGamut, convert } from 'whitepoint';
75+
76+
const p3Color = [0, 1, 0.2]; // out of sRGB
77+
const safe = toGamut(convert(p3Color, 'display-p3', 'oklch'), 'oklch',
78+
{ gamut: 'srgb', method: 'css' }); // spec algorithm
79+
// method: 'cusp' = faster, hue-exact; 'clip' = the blunt baseline</pre>
80+
<h2>Check (and fix) text contrast</h2>
81+
<pre>import { contrastWCAG2, mix, convert } from 'whitepoint';
82+
83+
let fg = convert(fromHex('#888888'), 'srgb', 'oklch');
84+
const bg = [1, 1, 1];
85+
while (contrastWCAG2(convert(fg, 'oklch', 'srgb'), bg) &lt; 4.5) {
86+
fg[0] -= 0.01; // darken in OKLCH: hue-stable
87+
}</pre>
88+
<h2>Re-light a brand color (the illuminant lab)</h2>
89+
<pre>import { adapt, convert, illuminantFromCCT } from 'whitepoint';
90+
91+
const xyz = convert(brandSrgb, 'srgb', 'xyz-d65');
92+
const underStoreLight = adapt(xyz, 'F2', 'D65'); // fluorescent → display
93+
const underTungsten = adapt(xyz, 'A', 'D65', undefined, { cat: 'cat16' });
94+
const at5000K = adapt(xyz, illuminantFromCCT(5000), 'D65');</pre>
95+
<h2>A Material-style tonal palette via HCT</h2>
96+
<pre>import { convert } from 'whitepoint';
97+
98+
const [h, c] = convert(seedSrgb, 'srgb', 'hct');
99+
const tones = [10, 20, 30, 40, 50, 60, 70, 80, 90, 95].map((t) =&gt;
100+
toHex(toGamut(convert([h, c, t], 'hct', 'srgb'), 'srgb', { method: 'cusp' })));</pre>
101+
<h2>Sort by perceptual similarity</h2>
102+
<pre>import { deltaE2000, deltaEOK, convert } from 'whitepoint';
103+
104+
const target = convert(pick, 'srgb', 'lab');
105+
swatches.sort((p, q) =&gt;
106+
deltaE2000(convert(p, 'srgb', 'lab'), target) -
107+
deltaE2000(convert(q, 'srgb', 'lab'), target));
108+
// deltaEOK (fast) and deltaECAM16 (appearance-grade) take the same shape</pre>
109+
<h2>HDR: tone operations in ICtCp, on the GPU</h2>
110+
<pre>import { glsl } from 'whitepoint/codegen';
111+
112+
const src = `
113+
${glsl('rec2100-pq', 'ictcp')} // decode PQ signal into ICtCp
114+
${glsl('ictcp', 'rec2100-pq')} // and back after your tone curve on I
115+
`;</pre>
116+
<h2>VFX handoff: sRGB ↔ ACEScg</h2>
117+
<pre>import { convert } from 'whitepoint';
118+
119+
const acescg = convert(srgb, 'srgb', 'acescg'); // scene-linear AP1
120+
const log = convert(acescg, 'acescg', 'acescc'); // colorist log
121+
// ~D60 white point handled by the same CAT machinery as everything else</pre>
122+
<h2>Spectral: a measured sample under two lights</h2>
123+
<pre>import { reflectanceToXyz, illuminantASPD, D65_SPD } from 'whitepoint/spectral';
124+
import { convert, adapt } from 'whitepoint';
125+
126+
const refl = { start: 380, step: 10, values: measuredReflectance }; // 0–1
127+
const day = reflectanceToXyz(refl); // D65 by default
128+
const night = reflectanceToXyz(refl, { illuminant: illuminantASPD() });
129+
// compare appearance: adapt 'night' to D65 first, then deltaE in oklab</pre>
130+
<h2>Composite layers without losing precision</h2>
131+
<pre>import { premultiply, overStack, unpremultiply, blend } from 'whitepoint';
132+
133+
const layers = sprites.map((s) =&gt; premultiply(s)); // [r,g,b,a] straight → premul
134+
const flat = unpremultiply(overStack(layers, 'srgb-linear')); // divide ONCE, at the end
135+
const moody = blend(top, bottom, 'soft-light'); // W3C blend modes, straight alpha</pre>
136+
<h2>YCbCr from a JPEG decoder</h2>
137+
<pre>import { convert, makeYCbCr } from 'whitepoint';
138+
139+
convert([y, cb, cr].map((v) =&gt; v / 255), 'ycbcr-601-full', 'srgb'); // JFIF
140+
const video = makeYCbCr({ matrix: '709', range: 'limited' }); // both REQUIRED
141+
// there is deliberately no bare 'ycbcr' — the name underdetermines the math</pre>
142+
<h2>The zero-allocation hot loop</h2>
143+
<pre>import { convert, OKLCH, sRGB } from 'whitepoint';
144+
145+
const out = [0, 0, 0];
146+
for (const px of pixels) {
147+
convert(px, OKLCH, sRGB, out); // space OBJECTS + out array: 47 ns, 0 B
148+
write(out);
149+
}</pre>
150+
<h2>CSS string parsing (interop, on purpose)</h2>
151+
<pre>// whitepoint refuses input parsing by design (grammar churn, `none`
152+
// semantics). The one-liner interop:
153+
import { parse } from 'culori';
154+
const { r, g, b } = parse('oklch(70% 0.15 250deg)') /* culori converts */;
155+
// …or keep coords in whitepoint arrays from the start.</pre>
156+
<h2>Pitfalls worth knowing</h2>
157+
<ul>
158+
<li><strong>Lab/Luv L is 0–100; OKLab L is 0–1</strong> — both per spec; don't mix them up.</li>
159+
<li><strong>HDR/log spaces clamp negative linear light</strong> (<code class="inline">ictcp</code>, <code class="inline">jzazbz</code>, <code class="inline">rec2100-*</code>, <code class="inline">acescc/t</code>): out-of-container colors don't round-trip, by design. <strong>OKHSL is only invertible inside sRGB</strong> (its s&gt;0.8 mapping has a pole at the boundary).</li>
160+
<li><strong>Alpha is not a coordinate.</strong> 3-channel everywhere; alpha-aware entry points (<code class="inline">mixAlpha</code>, <code class="inline">composite</code>, <code class="inline">serialize</code>'s option) are explicit.</li>
161+
<li><strong>Compositing on hue-polar spaces throws</strong> — there's no defensible meaning.</li>
162+
<li>The default <code class="inline">mix()</code> hue arc is <code class="inline">'shorter'</code>; CSS's other three (<code class="inline">longer</code>/<code class="inline">increasing</code>/<code class="inline">decreasing</code>) are options.</li>
163+
</ul>
164+
165+
</main>
166+
</body>
167+
</html>

tools/build-recipes-page.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Generates docs/recipes.html from RECIPES.md — a minimal converter for the
2+
// markdown subset the recipes use (headings, fenced code, paragraphs, lists,
3+
// inline code/bold/links). Keeps the site self-contained instead of bouncing
4+
// visitors to GitHub. Usage: node tools/build-recipes-page.js
5+
6+
import { readFileSync, writeFileSync } from 'node:fs';
7+
8+
const md = readFileSync(new URL('../RECIPES.md', import.meta.url), 'utf8');
9+
10+
const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
11+
const inline = (s) => esc(s)
12+
.replace(/`([^`]+)`/g, '<code class="inline">$1</code>')
13+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
14+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
15+
16+
const lines = md.split('\n');
17+
let html = '';
18+
let i = 0;
19+
let inList = false;
20+
const closeList = () => { if (inList) { html += '</ul>\n'; inList = false; } };
21+
22+
while (i < lines.length) {
23+
const line = lines[i];
24+
if (line.startsWith('```')) {
25+
closeList();
26+
let code = '';
27+
i++;
28+
while (i < lines.length && !lines[i].startsWith('```')) code += lines[i++] + '\n';
29+
html += `<pre>${esc(code.trimEnd())}</pre>\n`;
30+
} else if (line.startsWith('## ')) {
31+
closeList();
32+
html += `<h2>${inline(line.slice(3))}</h2>\n`;
33+
} else if (line.startsWith('# ')) {
34+
// page supplies its own h1
35+
} else if (line.startsWith('- ')) {
36+
if (!inList) { html += '<ul>\n'; inList = true; }
37+
// gather continuation lines (indented)
38+
let item = line.slice(2);
39+
while (i + 1 < lines.length && /^ {2,}\S/.test(lines[i + 1])) item += ' ' + lines[++i].trim();
40+
html += `<li>${inline(item)}</li>\n`;
41+
} else if (line.trim() === '') {
42+
closeList();
43+
} else {
44+
closeList();
45+
let para = line;
46+
while (i + 1 < lines.length && lines[i + 1].trim() !== '' && !/^(#|```|- )/.test(lines[i + 1])) para += ' ' + lines[++i];
47+
html += `<p>${inline(para)}</p>\n`;
48+
}
49+
i++;
50+
}
51+
closeList();
52+
53+
const page = `<!doctype html>
54+
<html lang="en">
55+
<head>
56+
<meta charset="utf-8">
57+
<meta name="viewport" content="width=device-width, initial-scale=1">
58+
<title>whitepoint — recipes</title>
59+
<meta name="description" content="Job-to-be-done recipes for the whitepoint color library: gradients, gamut mapping, illuminants, HCT palettes, shaders, spectra, compositing.">
60+
<style>
61+
:root {
62+
--bg: #0c0d10; --panel: #14161b; --edge: #23262e;
63+
--text: #e8eaf0; --dim: #9aa0ae; --accent: #4ba3f7;
64+
--mono: ui-monospace, SFMono-Regular, Menlo, monospace;
65+
}
66+
* { box-sizing: border-box; margin: 0; }
67+
body { background: var(--bg); color: var(--text); font: 16px/1.6 system-ui, -apple-system, sans-serif; padding-bottom: 6rem; }
68+
main { max-width: 880px; margin: 0 auto; padding: 0 1.25rem; }
69+
nav { position: sticky; top: 0; background: rgba(12,13,16,0.85); backdrop-filter: blur(8px); border-bottom: 1px solid var(--edge); z-index: 10; }
70+
.navin { max-width: 880px; margin: 0 auto; display: flex; justify-content: space-between; align-items: baseline; padding: 0.65rem 1.25rem; }
71+
.brand { font-weight: 650; color: var(--text); text-decoration: none; }
72+
.navlinks a { color: var(--dim); margin-left: 1.1rem; font-size: 0.9rem; text-decoration: none; }
73+
.navlinks a:hover, .navlinks a.here { color: var(--text); }
74+
h1 { font-size: 2rem; letter-spacing: -0.02em; padding: 3rem 0 0.25rem; }
75+
h2 { font-size: 1.2rem; margin: 2.4rem 0 0.6rem; letter-spacing: -0.01em; }
76+
p { color: var(--dim); max-width: 46rem; margin-bottom: 0.6rem; }
77+
p strong { color: var(--text); }
78+
ul { color: var(--dim); margin: 0.4rem 0 0.8rem 1.2rem; max-width: 46rem; }
79+
li { margin-bottom: 0.35rem; }
80+
li strong { color: var(--text); }
81+
pre, code { font-family: var(--mono); font-size: 0.85rem; }
82+
pre { background: var(--panel); border: 1px solid var(--edge); border-radius: 10px; padding: 1rem 1.25rem; overflow-x: auto; line-height: 1.5; margin: 0.6rem 0 1rem; }
83+
code.inline { background: var(--panel); padding: 0.1em 0.4em; border-radius: 5px; }
84+
a { color: var(--accent); }
85+
</style>
86+
</head>
87+
<body>
88+
<nav><div class="navin">
89+
<a class="brand" href="./index.html">whitepoint</a>
90+
<div class="navlinks">
91+
<a href="./index.html">demos</a>
92+
<a href="./accuracy.html">accuracy</a>
93+
<a class="here" href="./recipes.html">recipes</a>
94+
<a href="https://www.npmjs.com/package/whitepoint">npm</a>
95+
<a href="https://github.com/johnbxx/whitepoint">github</a>
96+
</div>
97+
</div></nav>
98+
<main>
99+
<h1>Recipes</h1>
100+
${html}
101+
</main>
102+
</body>
103+
</html>
104+
`;
105+
106+
writeFileSync(new URL('../docs/recipes.html', import.meta.url), page);
107+
console.log('docs/recipes.html written');

0 commit comments

Comments
 (0)