Repository navigation
feat: versioned colour models, v2 (Tailwind v4 fit) as the new default - #8
Merged
Merged
Conversation
This was referenced Oct 27, 2026
`fitted` varies lightness, chroma and a small hue drift smoothly with hue, fitted by least squares to the Tailwind CSS v4 palette. Around yellow it targets a share of the sRGB gamut instead, so light shades do not turn neon. Chroma is fitted as a share of sRGB and then refitted against the exact gamut, which keeps cyan / sky 400–500 vivid. `chromaticColorFrom`, `chromaticPaletteFrom`, `themeFrom`, `colorBy` and `chromaticFrom` accept `model`; `classic` keeps the previous colours. The fit and the Tailwind v4 data live in `scripts/fitted/`; coefficients are generated with `pnpm generate:fitted` (`--check` to verify them). BREAKING CHANGE: the default model is now `fitted`. Pass `model: 'classic'` to keep the previous colours. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With `model: 'fitted'` (now the default) the preflight emits, per colour, the Fourier basis of its hue and the lightness / chroma / hue drift of each shade as custom properties; baked colours use the values of `@proj-airi/chromatic`, which the preset now depends on. A test evaluates the generated CSS and compares it with `fittedOklch`. BREAKING CHANGE: the default model is now `fitted`, which does not emit `--chromatic-chroma` / `--chromatic-chroma-<shade>`. Pass `model: 'classic'` to keep the previous colours and variables. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nekomeowww
force-pushed
the
feat/fitted-model
branch
from
October 27, 2026 21:06
e07ba75 to
9846823
Compare
✅ Deploy Preview for proj-airi-chromatic ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
- `model: 'classic' | 'fitted'` becomes `'v1' | 'v2'` (default `v2`).
Released versions are frozen; a different fit ships as a new version,
so colours never change under an existing one. `FITTED_MODEL` becomes
`MODEL_V2` in `model-v2.ts`.
- The fit uses culori's `clampChroma` and `differenceEuclidean('oklab')`
instead of hand-written OKLab maths (culori is now a root dev
dependency for `scripts/`).
- The second chroma stage samples a uniform 1° grid, where the least
squares Fourier fit is the truncated DFT, so it no longer solves a
linear system. ΔE against Tailwind is unchanged (RMS 1.63, CV 2.11).
BREAKING CHANGE: `model` takes `'v1'` (colours of chromatic 1.x) or
`'v2'` (default).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Blue / cyan shades looked heavy and dull. The cause was in how shades are derived: each shade has a fixed OKLCH lightness, chroma follows
0.18 + 0.04 · cos(hue), and anything past the sRGB gamut is channel-clipped. Cyan / sky only reach high chroma at high lightness, so ~49% of hues at shade 500 were out of sRGB and clipped into darker colours.This PR introduces versioned colour models and makes
v2the default:v1is the model of chromatic 1.x.model: 'v1'keeps those colours.v2varies lightness, chroma and a small hue drift smoothly with hue. They are fitted by least squares to the Tailwind CSS v4 palette.v3, so colours never change under an existing version.How
v2is fitted:share × exact sRGB maxrefitted per shade with 12 harmonics. On the uniform 1° grid of the second stage, the least-squares solution is the truncated DFT.#23c8ecto#00caf0(Tailwind ≈#00c7fb).v1is at 5.80.Changes
@proj-airi/chromatic:fittedOklch,MODEL_V2(inmodel-v2.ts), and amodel: 'v1' | 'v2'option onchromaticColorFrom/chromaticPaletteFrom/themeFrom/colorBy/chromaticFrom. The runtime still depends only on culori.@proj-airi/unocss-preset-chromatic: amodeloption. Withv2, the preflight emits per colour a sharedcos(k·h)/sin(k·h)basis and per-shade lightness / chroma / hue-drift custom properties. Baked colours use the core values. Modifiers (~,*) keep working. The preset now depends on@proj-airi/chromatic.scripts/fitted/: the Tailwind v4 data, the fit, and the generator.clampChroma,differenceEuclidean('oklab')). culori is added as a root dev dependency for the scripts.pnpm generate:fittedwritesmodel-v2.ts;--checkverifies it is up to date.Breaking changes
v2, so every colour changes slightly. Shades 50 / 100 are no longer mixed with white.v2does not emit--chromatic-chroma/--chromatic-chroma-<shade>. Code that reads them directly should switch to utilities with~/*modifiers, or tochromaticColorFrom(hue, { shade, saturation, brightness }). It can also passmodel: 'v1'.Notes
With
v2, lightness and chroma are computed from--chromatic-hueon:root. A subtree override of--chromatic-huerotates the hue but keeps the root's lightness and chroma, which is whatv1already did for chroma.CSS size per colour: ~17 KB raw / 2.4 KB gzip.
Style recalc per hue change with 5000 elements (median of 5):
Simulators run on the host CPU. Real devices are not measured yet.
Registering the variables with
@propertyhelped on WebKit but not on Blink, so it is not used.Longest substituted utility colour is 2.3 KB, ~900× below Chrome's ~2 MB
var()substitution limit.Test plan
pnpm install --frozen-lockfile,pnpm lint,pnpm typecheck,pnpm buildpnpm test:run: 26 tests pass.v1tests are unchanged and pinned tomodel: 'v1'.fittedOklchfor 3 colours × 5 hues × 11 shades.pnpm generate:fitted --check🤖 Generated with Claude Code