Replies: 2 comments 5 replies
-
This actually has nothing to do with gamut mapping. It has everything to do with the conversion algorithm for Okhsl. Okhsl and Okhsv are a rough approximation of the sRGB gamut. Additionally, it is not a color space designed to be expandable to other gamuts, though Okhsv is more stable than Okhsl as you expand outside the gamut. Note below what happens to Okhsl vs Okhsv when we expand the colors being represented in the colors spaces from the gamut sRGB to Display P3. As you can see, the conversion completely breaks down in Okhsl in Display P3 and Okhsv is still fairly stable, at least in P3. So why is gamut mapping breaking down? Because the conversion from Okhsl to Oklch with such high saturation is unstable. Let's convert the Okhsl values to OkLCh. >>> frames = [
... [133.7, 1.216, 0.590],
... [135.4, 1.219, 0.595],
... [137.1, 1.223, 0.599],
... [138.9, 1.226, 0.604],
... [140.6, 1.230, 0.608],
... [142.3, 1.233, 0.613],
... [144.0, 1.237, 0.618],
... [145.7, 1.240, 0.622],
... [147.4, 1.244, 0.627],
... [149.1, 1.247, 0.632],
... [150.9, 1.251, 0.636],
... [152.6, 1.255, 0.641]
... ]
>>> [Color('okhsl', f).convert('oklch') for f in frames]
[color(--oklch 0.64694 0.27759 133.7 / 1), color(--oklch 0.65127 0.34935 135.4 / 1), color(--oklch 0.65473 0.54876 137.1 / 1), color(--oklch 0.65906 4.6251 138.9 / 1), color(--oklch 0.66252 0.42249 320.6 / 1), color(--oklch 0.66684 0.13998 322.3 / 1), color(--oklch 0.67117 0.27316 324 / 1), color(--oklch 0.67463 1.0549 325.7 / 1), color(--oklch 0.67895 1.374 147.4 / 1), color(--oklch 0.68327 0.53155 149.1 / 1), color(--oklch 0.68673 0.36602 150.9 / 1), color(--oklch 0.69105 0.29836 152.6 / 1)] You can notice that these extreme saturations cause the Okhsl conversion to OkLCh to cause the hues to jump from the normal 133-150 range for the hue to ~320. The conversion just isn't stable with with extreme saturation. |
Beta Was this translation helpful? Give feedback.
-
Yes, not long after posting this I changed the title here and added some additional text to broaden the possibilities. Thanks for the explanation. I didn't assume there was an issue in Color.js, and my code is a pass-through for this part of things. It's jarring to see it glitch like this in an otherwise smooth animation, but it is what it is. I do like to confirm that it's the expected behavior though, so thanks for that. And the Color.js gradient app? Seems to me that something is amiss there as I can't find a way to reproduce this within those confines. |
Beta Was this translation helpful? Give feedback.
-
EDIT: @facelessuser answered my first question, but the second question about the gradient app is still unanswered:
I stumbled on this while fiddling with linear interpolations between two colors. This codepen illustrates the range of values that are not mapping correctly. The result is a set of magenta-ish colors in the middle of a green range. I have reproduced this in Chrome and Firefox on Windows 11 and in Safari on Mac OS Sonoma. Color.js defaults to
lab
as the display space in all cases.Is this an issue, or just par for the course with gamut mapping? Or is it an issue with Color.js
okhsl
tolab
conversion? Or is it an issue with browserlab()
rendering? Or none of the above?The codepen is a static illustration of a simulated 2-second animation at 60fps, 121 frames numbered 0-120. (the 0th frame is the value prior to the start of animation). It's a linear interpolation of values, as you can see by coordinates in the
okhsl()
text on the left side of each frame's<h1>
. These two colors are the defaults for my test page: "darkred" and "color(--lchuv 86.8 42.9 230)".The Color.js gradient app avoids this glitch in spite of checking
Allow gamut mapping
. I set it to 120 steps, output spacelab
, interpolation spaceokhsl
. I have not touched thedelta E
because I'm not sure what it is. The Okhsl values onscreen appear to be generated byColor.prototype.toString()
, which clamps the saturation at 100%. The coordinates that are used to generate the gradient appear to be similarly clamped, in spite of allowing gamut mapping. So while my saturation end value is out of gamut at1.42
, the gradient app keeps every step at1
. The codepen gets its values from:new Color("color(--lchuv 86.8 42.9 230)").to("okhsl").coords
I see no difference in the gradient app output when I check or uncheck Allow gamut mapping. Is that an issue?
Beta Was this translation helpful? Give feedback.
All reactions