-
Notifications
You must be signed in to change notification settings - Fork 708
[css-color-4] Channel clipping breaks author expectations, especially when using 'perceptually uniform' spaces #9449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
An important aspect is that there is no feature detection for gamut mapping. |
It's probably important to note that there likely isn't a perfect gamut mapping approach, each will probably have some quirks and can be useful if their limits are understood, but anything is better than clipping, which currently is what all browsers do. OkLCh generally does well for gamut mapping when colors are within the model's ideal range. OkLCh seems to do decent up through rec2020 as the color space maintains a reasonable geometric shape. It also avoids the purple shift that occurs when gamut mapping with LCh.
But we can see that the geometry of the OkLCh space becomes quite distorted for a space like ProPhoto RGB which extends past the visible gamut. This distortion helps contribute to issues like #7071.
Gamut mapping with LCh has its own issues, purple shift in the blue range as an example, but the space does hold its shape much better with extreme gamuts allowing for more consistent mapping, but still, some corner cases exists, like with bright yellows due to the geometry in that hue region. Clipping is still probably worse than either of these options: ![]() |
The same perceptual lightness shift is also present in (ok)lab. Here's a Codepen demo showing (ok)lab and (ok)lch with consistent lightness values, and changing only the Ok(lab) bug reports-
Both these browser bug reports and in the issue description itself also talk about the separate but connected issue around powerless components. No browsers have implemented this portion of the spec (which is also present on LCH, Oklab, and Oklch specs).
These all should be black and white- There is also related conversation here- #8794 |
I agree. After all the work that went into finding a good Gamut Mapping Algorithm that was hue-preserving, lightness-preserving, and thus allowed the closest approximation to a specified color that was out of gamut of the display device, we end up with naive clip shipping in browsers which gives massive hue shifts and even bigger lightness shifts. And this was done out of a misguided attempt to make 2D Canvas (which is drawing millions of pixels) align with displayed images (which will be using a perceptual gamut mapping, to preserve overall look and image detail) and with CSS (where you have maybe a hundred or so colors in all the stylesheets on a page). Trading off authoring complexity and frustration for minimal gains in computing efficiency of the implementation, Here is an example: the CSS Color 4 GMA with Oklch on the left, the (old) CSS Color 4 GMA with CIE LCH and DeltaE2000 on the right, and in the middle naive clip which, as cal clearly be seen, for these light colors gives a much darker result quite unlike the requested color. Its a screen shot from this demo with OK lightness set to 0.95. Which is why we see preprocessor plugings like this which take your CSS and auto-generate sRGB fallbacks (using the CSS Color 4 GMA)
|
Here is that PostCSS GMA plugin btw |
That portion of the spec has changed because of it now says:
which is more correct - the specified color does have chroma, but because of the lightness it will be out of gamut of any SDR display (where the brightest color that can be displayed is media white). |
Sorry- I was looking at an outdated version of the spec. I see that
|
No problem, we should update the official TR version more often (I keep meaning to but then there is always more to do). But the Editor's Draft is the right place to look for the latest version. |
@jamesnw wrote:
So that demo has Because Chrome and Firefox do naive clipping, that becomes A CSS gamut mapped version of On a P3 screen, we start from @mirisuzanne wrote:
Similarly this has [ TLDR; clip is a terrible gamut mapping replacement (unless the colors to be clipped are barely out of gamut) |
Thanks for the walkthrough of the issue here. I made a Codepen that compares the CSS Algorithm outputs for sRGB and display-p3 with a naive clip (and a comparison with the browser's adjustment, so we can compare when that is fixed). |
I agree that the However, I do believe that the CSS gamut mapping algorithm can be inappropriate to apply to other things like I think that the best way forward would be to "bake" CSS gamut mapping in to the definitions of The difficulty is to define exactly what "baking" to do. Mapping to the display's gamut might be okay, but on sRGB-ish devices, it might do surprising things. When drawing to a canvas, the mapping cannot depend on the device's color space (ignoring fingerprinting, we just wouldn't want the non-determinism), and I don't think that mapping to the canvas' space would be that good (sRGB is the default and is very narrow). One scheme would be something where we bake a well-known gamut into A better scheme could be to define a standard polyhedron to always do gamut mapping to, then I think that would be a really good way forward. This polyhedron should be big -- maybe as big as the spectral colors. And it could be made to be convex. (And maybe we could define it as being smooth). I've been using this tool to visualize some of these options. |
No, it really isn't. It doesn't care whether the out of gamut color came from
is entirely unjustified. |
The linked ('red/redder') example demonstrates to me is that rgb clipping works well when there is only one rgb channel in use. That seems like the extreme special case to me. Maybe there could be special handing of single-channel rgb in a gamut mapping algorithm? But as soon as you start combining channels in any color space, channel-clipping will cause hue-shift. That's the 99% case, and the case that gamut mapping is designed to solve. (and while it may be better to have the 'redder' red in that case, even a slightly desaturated red is a much closer to user-intent than we get from the channel-clipping failure cases. At least it's still red!) |
I think the red/redder example is a sidetrack because it starts from an incorrect assumption. It makes the assumption that
Gamut mapping from
These both happen to have the maximum value in a single channel and zero values in the others in their respective color space. Connecting these two values and assuming that one gamut maps to the other is incorrect. |
With respect to the "red redder", it is not obvious to me that this (the gamut mapped result) is a desirable representation of this (the original, needs a P3 monitor). I don't think that projecting along constant luminance is the right thing to do in the general case, but we can drop that for now. I would really like to apply gamut mapping to The difficulty with the ![]() This is what the gradient looks like, with CSS gamut mapping, going from ![]() But there is a problem here! There is a real mapping from points in this 3D space to chromaticity values, and this gradient does not accurately represent those colors. At the top, at At the bottom, at ![]() The problem with
The simplest solution that I see to this problem is to bake gamut mapping into the definitions of |
@ccameron-chromium I am sorry, but I am not really following. To me it seems that there are some misconceptions leading to incorrect conclusions about the new color notations, interpolation and gamut mapping. I am not sure if this particular issue is the best place to answer these questions. The focus of this issue is that browsers shipped color notations that can express wide gamut colors without implementing gamut mapping. New issues for your specific questions would be easier to resolve without causing noise in this issue :) |
@romainmenke I think what is being suggested is just to map the points more 3 dimensionally instead of just mapping by reducing chroma in one dimension. It's mapping the geometry of a wider gamut surface down to a smaller gamut surface. This sacrifices preserving lightness, chroma, and hue to gain something the suggester thinks would be more intuitive. What is better can be subjective based on what your intent is. CSS has chosen to preserve as much of the original color intent as possible by only reducing chroma (though some minor hue and lightness are sacrificed with the clipping via MINDE). It is like what is expressed in this Oklab gamut mapping article. Do you just project along the chroma dimension? Or do you project in the lightness dimension as well, and if so, then by what degree do you project in the lightness dimension, or how much original lightness are you willing to sacrifice to get what you think is a "better" color? The suggestion being made is to do this more in 3 dimensions, most likely sacrificing more hue and lightness than CSS currently does. EDIT: I do realize I am oversimplifying the 3-dimensional transform being suggested.
I agree, this probably deserves a separate topic if a different algorithm wants to be discussed. |
This is factually incorrect. Highly out of gamut colors can be produced in pretty much any colorspace, including sRGB. |
I think that the difference is whether or not it is obvious to the content author when they are entering the danger zone. If specifying colors in an RGB color space, there's the clear signal that "if the parameters are outside of the [0,1] interval, then I'm playing with fire". True, one can specify Meanwhile, in something like It turns out that For this reason, I think that we should provide content authors with a space that is perceptually uniform but does not suffer from this problem. A cylindrical space something like okhsl would be nice. (That particular formulation is very tightly tied to the sRGB gamut, so it won't do as a verbatim drop-in). The CSS gamut mapping algorithm has the effect of cylinder-ifying the |
Sure, there might be an even more perfect color space down the road. It's totally theoretical, but the theory is great. And if that perfect space ever ships in CSS, colors will continue to go out-of-gamut. And authors should get better behavior than random clipping when that happens. We can't put this on authors, and expect them to just keep all their colors in the gamut. Because the web (by design) is an unreliable context. A cylindrical space with integer boundaries won't change that. We can't expect authors to carefully manage their colors across a web for everyone, on everything. Even the most carefully crafted rec2020 colors will continue to go outside some remaining sRGB monitors. When that happens, CSS should try and help provide a 'close match' for the majority of use-cases, rather than throwing up our hands. When clipping is anywhere close to author intent, it's pure luck. That's not a solution, it's a stopped clock. We need an approach to out-of-gamut colors that attempts to maintain author intent. Now that browsers have shipped The default behavior should help get 90% of use-cases close-enough. And then we can provide additional tools for authors that need additional precision around the edges. |
I agree with this statement. It shouldn't be expected that an author ensure that all colors be in the gamut of the target device. What concerns me is something slightly different: Should an author specify a color when they do not (or physically cannot) know what that color actually looks like? Should an author specify colors that are very far outside of the gamut of any existing display (including the one that the author is using)? Should an author specify colors that do not physically exist? |
"How do we help authors make sensible choices?" is a good question, but it's not the question that's being posed here, which is "how do we best adapt arbitrary content for users on varied hardware"? And doing worse things for users – especially users on less-capable devices – is not a good answer to the "how do we help authors" question. |
If the concern is about color formats that give easy access to a very wide gamut, rather than a concern with adapting those colors for display, then browsers shipped the wrong half of the spec. Now authors are encouraged to use the fully interop/supported wide gamut formats, and there's absolutely no safety net in place to ensure those formats work for people on the other end. This is what has me confused.
In that case, a solution that is specific to (ok)l** formats is not a solution to this issue. We can't 'bake gamut mapping' into a few wide-gamut formats and be done with it. We would still need gamut mapping for other wide-gamut formats, which may still render on narrower-gamut displays. If we want to also change how a few formats work, that should be a separate issue for discussion. Trying to move gamut mapping forward, I see a few options on the table.
Did I miss something? Can we narrow in on a path forward? |
Pushing some more on this assertion that in-gamut colors in images never change, here are a few useful diagrams from Ján Morovič "Color Gamut Mapping". These are all using typical RGB to CMYK gamuts so absolutely no "unreasonable" colors. The first shows a variety of input-to-output transfer functions used in gamut mapping; only one (here, labelled clipping) has a straight line where output=input for in-gamut colors. The others use a soft knee, or a sigmoidal function, to ease the transition and thus, some or all in-gamut colors change. Next, a division of gamut volume into core colors which will not change, destination colors (the intersection of source and destination, minus core) where gamut compression is applied, and source (outside destination; all colors will change): And lastly, a constant hue and lightness-compressing GMA which uses that core/destination division to perform gamut mapping into the destination: |
Wasn't clear from the proposal or the minutes, but is gamut mapping still the last step? Current steps around interpolation, color space conversions, relative colors,... are quite complex and depend on gamut mapping happening at a later stage, right before display. Would this still be true? Or would it happen earlier? Has the prototype in Chrome advanced enough to be able to test color mixing, relative colors, gradients, ...? Simple color declarations aren't the best way to validate the proposed solution, more advanced values allows us to stress test this a bit more. |
Clipping Rec.2020 colors to, e.g., sRGB sometimes produces significant lightness shifts vs doing what's in the spec now (which tries to preserve lightness above all else). Here's a ~worst-case, real-world example of how different results can be, even post-mapping-to-Rec.2020: https://codepen.io/eeeps/pen/zYQPqKM
I agree; "match the image processing pipeline" should be an option, but I would prefer if "preserve lightness" were the default – not only for the proposed first stage, where wildly out-of-gamut OkLCH colors are mapped to Rec.2020, but also when doing the proposed second stage: mapping Rec.2020 colors to physical display gamuts. |
An alternative could be that we map to either rec2020 or p3, choosing the smallest that has a gamut larger than the user's monitor, and then clip from there. Here's a fork of your codepen, with the addition of an example where we first map to p3, then clip to sRGB. https://codepen.io/jamessw/pen/abrVLvQ I added the Lightness Delta in |
There is a port of the Chromium algorithm in the Color.js Gamut Mapping apps at https://apps.colorjs.io/gamut-mapping/ and https://apps.colorjs.io/gamut-mapping/gradients which may help with some exploration. |
The goal of the two-step process (as I understand) was to "bake step 1 into the format" so it doesn't change over time. So I don't think a contextual first step (either rec2020 or p3) meets the requirement. |
What I am mostly interested in is how colors behave depending on the notation. With relative color syntax you can change the notation. https://codepen.io/romainmenke/pen/GRbgMPj ![]() I would still prefer that gamut mapping was applied to all CSS color values and that it doesn't depend on how a value is declared. With another way to opt-in/out of clipping/gamut mapping for all CSS colors. Also concerned about mapping to rec2020 and then clipping. Is there any clipping or gamut mapping that happens before any other interpolation? Clipping literally throws away information and in a way that does not preserve the balance between channels. i.e. these gradients should render differently as they should render the hue circle in opposite directions. #a {
background: linear-gradient(to right in oklch longer hue, oklch(from color(srgb 1 -0.01 0) l c h), red 100%);
}
#b {
background: linear-gradient(to right in oklch longer hue, oklch(from color(srgb 1 0 -0.01) l c h), red 100%);
} Chrome (with the flag) shows pure red, but maybe not by design given that the implementation is experimental? |
I've opened #10579 on this topic (with a request for algorithms to consider). |
Are we losing the window where we can still make changes? Given that there is pretty good interop for most of It also seems that the experimental implementation was removed from Chrome? |
Chatting with @jyasskin today, it looks like there has been a miscommunication: @ccameron-chromium has been expecting a gamut mapping algorithm from the CSS WG, and the CSS WG has been waiting for him to review existing algorithms per action item in #9449 (comment) . We need to resolve this ASAP and move forwards before the window for changes closes. @astearns could we slot this in the TPAC agenda somewhere? 🙏🏼 |
@astearns not in a comment, but here: https://apps.colorjs.io/gamut-mapping/ |
@LeaVerou are all the options in https://apps.colorjs.io/gamut-mapping/ still in play, or could the list be reduced further? A comment in 10579 instead of this one would be useful. |
here's a newer one that may not be tracked https://github.com/texel-org/color |
IIRC, that is just implementing Björn's gamut mapping algorithm with constant lightness as described here: https://bottosson.github.io/posts/gamutclipping/. You can see it compared against all the others here: https://deploy-preview-7--color-apps.netlify.app/gamut-mapping/gradients?from=oklch%2890%25+.4+250%29&to=oklch%2840%25+.1+20%29. I've had a PR to merge it open for a while, but I don't have the power to merge it officially, so the preview is the best way to view it currently. |
Slightly off topic but
Not seeing it? |
If we give the comparison a bit more work to do then Edge Seeker is the fastest at 2ms, (same speed as clip!) while CSS_Rec2020 is slowest at 10ms (in Firefox on my machine). Times in Chrome are similar, Edge Seeker at 0.6ms while CSS_Rec2020 is 3.3ms. |
Merged now, sorry for the delay! |
What's the status of this, and is there any chance it will move forward? |
This is not an issue in the css-color-4 spec, but in all the implementations. While issues have been filed on the individual bug trackers, I wanted to raise the issue with the CSSWG since it seems like this was an intentional decision agreed to by the browser vendors.
Here are the individual bug reports:
And, as I understand, the decision was made in these CSSWG issues:
I'm opening a separate issue because I don't have strong feelings about all the details of a gamut mapping algorithm, but I'm pretty frustrated about the state of what browsers shipped here, and I think we need to do something to fix it asap. From an authoring perspective it's entirely unusable, and it breaks the fundamental promise of the format: providing perceptually-uniform lightness.
This is the format that authors were most excited about, and it doesn't do what we told them it does. I really wish this feature hadn't shipped at all, since it clearly wasn't ready to ship. Adding agenda+ because I think this deserves more eyes on it, and more urgency in fixing it.
The text was updated successfully, but these errors were encountered: