culori: using `clampChroma` within a conversion sequence results in invalid values
Issue
Using clampChroma within a conversion sequence results in a color object with invalid values.
Expected
Post-clampChroma conversions result in color objects with valid values as defined by culorijs.org/color-spaces.
Example
import {rgb, oklch, clampChroma} from "culori"
const max = {l:1, c:0.322, h:360}
rgb(
clampChroma(
oklch({
l: (max.l * 0.5),
c: (max.c * 0.5),
h: (max.h * 0.5),
})
)
)
Result
oklch > clampChroma > rgb
r has a negative value, which is outside of the rgb color-space’s documented {min:0, max:1} range.
{mode:'rgb', r:-0.00015269415291914584, g:0.4633768089712706, b:0.40462053745496246}
Other Values
oklch
{mode:'oklch', l:0.5, c:0.161, h:180}
oklch > clampChroma
{mode:'oklch', l:0.5074222643221025, c:0.09214221649333439, h:179.92987163513519}
oklch > rgb
{mode:'rgb', r:-1.1275082865428603, g:0.4943967336505179, b:0.40272542671134454}
About this issue
- Original URL
- State: open
- Created 3 years ago
- Comments: 18
Commits related to this issue
- Re: #129 / clampChroma(): when C:0 is not displayable, return rgb-clamped achromatic — committed to Evercoder/culori by danburzo 3 years ago
I’m currently working through the data you provided. The color table you provided with
chromaClamp(oklch()):This converts the colors to
lch, then tries to find the closest chroma that’s displayable while maintaining thelchhue constant, which doesn’t necessarily result in theoklchhue to remain constant. The fact thatoklchhas its own Chroma and Hue dimensions is incidental — nonetheless it may become confusing.When doing
clampChromain the color’s original color space,clampChroma(oklch(...), 'oklch')we get rid of the hue drift, and the Lightness increases monotonically:Now, a remaining problem in both tables is the row corresponding to the OKLCh Lightness = 1. This is due to a quirk/side-effect in OKLab, in that sRGB white has
L: 0.9999882345920056, notL: 1. So any OKLCh color withL: 1will be too bright to be displayable in sRGB on any Chroma (even 0), soclampChromafalls back toclampRgb, and the row appears out of order with the others. I should probably update the OKLab / OKLCh Lightness range on the Color Spaces to be more accurate. (Also, there are other ways to clamp out-of-gamut colors that may be better for this particular situation)@danburzo Hey I think I might have found some more issues with the chroma clamping.
Video Demonstration:
I wrote a small wrapper for culori that uses unit intervals [0-1] for [
r,g,b,h,c,l,a, etc.], but I do have some tests implemented that ensure accurate conversions. I’ll do a bit more debugging tomorrow to double check that the issues aren’t being caused by anything on my end & let you know how it goes.If I do narrow the issue down to something within culori, I’d be open to setting up a screen sharing session with you if you think it might lead to any insights or want to check out any of the generated data.
This is the function I’m calling in the demo:
The
count,min,max,hue, &chromavariables are being passed to a Svelte component via Storybook’s controls.handcremain static for each generated range, andlvalues are being generated frommintomaxvia the easing library, after which all of the resulting color objects are passed through my wrapper which converts the unit interval values to culori values and then runs them through a similar color conversion process as initially detailed in this issue, finally applying the generated hex values to the HTML elements.So, the
clampChroma()method made the following assumption: there is a value forcfor which we know the color is displayable, we’re going to iterate towards it until the search interval becomes very small. At this point, for a small enough interval, the color should be displayable. But it didn’t guarantee you landed on a displayable color on the last iteration. With the fix above we use the last color in the interation we know to be displayable (sacrificing a tiny bit of chroma in the process).