relativeLuminance() and contrastAPCA() measured colors through toRgb(),
i.e. 8-bit rounded, clipped sRGB: oklch(0.6 0.1 200) vs
oklch(0.95 0.02 90) gave WCAG 3.2520 where colorjs.io gives 3.2635, and
the APCA field was stepped (5 distinct values over 200 samples spanning
dL = 0.004). Contrast regions meanwhile used unclamped luminance for
WCAG, so points just inside an AA region could still fail
contrastRatio() >= 4.5.
- Contrast metrics now measure the unrounded sRGB channels clipped to
the display gamut. Clipping (not gamut mapping) was chosen because it
is what toRgb()/toHex() render, it is exact and equal to colorjs.io for
every in-gamut color, it is O(1) with no dependence on a mapping
method, and WCAG/APCA are defined on displayable [0, 1] channels.
- New optional ContrastOptions on relativeLuminance, contrastRatio,
contrastAPCA, meetsAA and meetsAAA: gamut ('srgb' default, or
'display-p3' to clip in P3 and measure the extended sRGB result) and
precision ('exact' default, or '8bit' to quantize like hex-based
tools; reproduces color-kit 0.1 results bit for bit).
- Contrast region fields (hybrid and legacy, WCAG and APCA) evaluate the
public contrastRatio/contrastAPCA with the region's gamut, so region
boundaries sit on the check's own boundary. The internal
relativeLuminanceUnclamped/contrastRatioUnclamped are removed.
- toHsl/toHsv/toHct no longer round to 8-bit first (new
conversion/unrounded.ts; toHct uses Material's CAM16 on the unrounded
clipped color). rgbToHsl/rgbToHsv treat channel spreads <= 1e-10 as
gray so float noise does not produce an arbitrary hue; 8-bit inputs
are unaffected. This touches three one-line bodies in
conversion/index.ts.
- 8-bit inputs: default results match the old values to float precision
and pass/fail decisions are unchanged; '8bit' mode is bit-identical.
- Tests: colorjs.io reference for unrounded/out-of-gamut OKLCH pairs
(vs the clipped color), fast-check property that contour vertices
nudged 0.0005 L either side split across the public check (about half
failed with 8-bit checks), APCA/WCAG continuity, P3 gamut option, and
HCT/HSL parity with the 8-bit results.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
relativeLuminance() and contrastAPCA() measured colors through toRgb(),
i.e. 8-bit rounded, clipped sRGB: oklch(0.6 0.1 200) vs
oklch(0.95 0.02 90) gave WCAG 3.2520 where colorjs.io gives 3.2635, and
the APCA field was stepped (5 distinct values over 200 samples spanning
dL = 0.004). Contrast regions meanwhile used unclamped luminance for
WCAG, so points just inside an AA region could still fail
contrastRatio() >= 4.5.
the display gamut. Clipping (not gamut mapping) was chosen because it
is what toRgb()/toHex() render, it is exact and equal to colorjs.io for
every in-gamut color, it is O(1) with no dependence on a mapping
method, and WCAG/APCA are defined on displayable [0, 1] channels.
contrastAPCA, meetsAA and meetsAAA: gamut ('srgb' default, or
'display-p3' to clip in P3 and measure the extended sRGB result) and
precision ('exact' default, or '8bit' to quantize like hex-based
tools; reproduces color-kit 0.1 results bit for bit).
public contrastRatio/contrastAPCA with the region's gamut, so region
boundaries sit on the check's own boundary. The internal
relativeLuminanceUnclamped/contrastRatioUnclamped are removed.
conversion/unrounded.ts; toHct uses Material's CAM16 on the unrounded
clipped color). rgbToHsl/rgbToHsv treat channel spreads <= 1e-10 as
gray so float noise does not produce an arbitrary hue; 8-bit inputs
are unaffected. This touches three one-line bodies in
conversion/index.ts.
and pass/fail decisions are unchanged; '8bit' mode is bit-identical.
(vs the clipped color), fast-check property that contour vertices
nudged 0.0005 L either side split across the public check (about half
failed with 8-bit checks), APCA/WCAG continuity, P3 gamut option, and
HCT/HSL parity with the 8-bit results.
Part of a stacked set of PRs (#204 → #221) from the color-kit assessment. Validated per branch with
pnpm buildandpnpm test; lint, format, typecheck, size, artifact, preprod and agents checks pass at the top of the stack.🤖 Generated with Claude Code