Skip to content

Fix hcl/lch/oklch interpolation not reaching pure black/white exactly - #401

Merged
gka merged 2 commits into
gka:mainfrom
raphyabak:fix/hcl-oklch-interpolation-reach-black
Sep 14, 2026
Merged

gka merged 2 commits into
gka:mainfrom
raphyabak:fix/hcl-oklch-interpolation-reach-black

Conversation

@raphyabak

Copy link
Copy Markdown
Contributor

Summary

Fixes #310.

chroma.interpolate('#f00', '#000', 1, 'hcl').hex(); // '#5b0000', should be '#000000'

interpolator/_hsx.js intentionally freezes saturation/chroma at the colored endpoint's value while interpolating toward (or from) an achromatic color, so shade()/tint() keep colors looking vivid instead of fading to gray at intermediate steps. That freeze is correct and tested behavior for f strictly between 0 and 1.

The bug is that the freeze also applied exactly at the boundary (f === 1 interpolating toward black/white, or f === 0 interpolating away from it), where the result must equal the achromatic endpoint exactly. Unlike hsl/hsv/hcg/hsi (where saturation has no visual effect at extreme lightness/value), chroma in hcl/lch/oklch is still meaningful at L=0, so freezing it at a large nonzero value there produces a slightly-off-black/white color once converted to RGB.

Fix

Stop the freeze exactly at that boundary (f < 1 / f > 0 guards) so it falls through to the existing linear interpolation, which already resolves correctly to the target's own chroma (0 for a fully achromatic target). Every other case is untouched.

Test plan

  • Added tests in test/mix.test.js:
    • the exact cases from the issue (hcl/lch/oklch reaching pure black from a saturated color),
    • a near-black target to confirm non-degenerate targets still resolve exactly,
    • red.shade(0.5, 'lch') to lock in that the intermediate-step "stay vivid" behavior is unchanged.
  • Verified the regression: reverting the source change turns the two new boundary tests from passing into failing with '#5b0000' / '#100000' (the exact wrong values from the issue), while the shade test still passes on the reverted code — confirming the test isolates the actual bug rather than the intentional shading behavior.
  • Full suite passes locally: npx vitest run — 2522 passing (2519 existing + 3 new).
  • npm run lint (prettier + eslint) passes, and the repo's pre-commit hook (lint + full test run) passed on commit.

Fixes gka#310.

When interpolating toward (or from) an achromatic color in hcl/lch/oklch,
saturation/chroma is intentionally frozen at the colored endpoint's value
for intermediate steps, so shade()/tint() keep colors looking vivid
instead of fading to gray. But the freeze also applied at f === 1 (or
f === 0), where the result must equal the achromatic endpoint exactly.
Since chroma is still meaningful at L=0 in these color spaces (unlike
saturation at L=0/1 in hsl/hsv/hcg/hsi), keeping it frozen at a nonzero
value there produced a slightly-off-black/white result, e.g.
interpolate('#f00', '#000', 1, 'hcl') returning '#5b0000' instead of
'#000000'.

Stop the freeze exactly at that boundary so it falls through to the
normal linear interpolation, which already resolves correctly to the
target's own chroma. Intermediate steps (f between 0 and 1) are
unaffected.
@raphyabak
raphyabak requested a review from gka as a code owner August 25, 2026 08:08
@changeset-bot

changeset-bot Bot commented Aug 25, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c36e3d5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
chroma-js Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@gka gka left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense, thanks

@gka
gka merged commit 4b5d959 into gka:main Sep 14, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorret HCL interpolation between saturated colors and black

2 participants