Skip to content
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

[css-color-5] Gamut mapping in HSL/HWB #7107

Closed
devongovett opened this issue Mar 6, 2022 · 13 comments
Closed

[css-color-5] Gamut mapping in HSL/HWB #7107

devongovett opened this issue Mar 6, 2022 · 13 comments
Assignees

Comments

@devongovett
Copy link
Contributor

I am implementing support for color-mix in Parcel CSS, and following the tests in WPT. A few tests related to gamut mapping failed in my implementation and I am wondering whether the test is wrong or my understanding of the spec is wrong.

Example test:

test_computed_value(`color`, `color-mix(in hsl, color(display-p3 0 1 0) 100%, rgb(0, 0, 0) 0%)`, `rgb(0, 249, 66)`); // Naive clip based mapping would give rgb(0, 255, 0).

The test expects rgb(0, 249, 66) as the result, but I am getting rgb(0, 247, 78). This seems to be the result of gamut mapping occurring in the sRGB space rather than the HSL space. However, the color-mix in the test states that the color mixing should occur in the HSL space. If I convert the HSL color to sRGB before gamut mapping, and then back to HSL afterward for interpolation the test passes. However, I don't see where in the spec that is required. It says:

For intermediate color calculations, these out of gamut values are preserved.

In this case, we're converting from OKLCH to HSL, and then comparing the clipped result via deltaE, so I would consider sRGB to be an intermediate space.

So my question: does gamut mapping happen in the sRGB space for HSL and HWB colors, or in the HSL/HWB space?

Hopefully that made sense...

@devongovett
Copy link
Contributor Author

As another point of confusion, Example 7 in the spec shows this example:

color-mix(in hsl, color(display-p3 0 1 0) 80%, yellow);

It says that the P3 color is equivalent to hsl(120 100% 49.898%), but that doesn't match either of the above results, which are either approximately hsl(136 100%, 49%) or hsl(139 100% 48%). What is the correct answer here?

@devongovett devongovett changed the title [css-color-5] Gamut mapping in HSL/HSB [css-color-5] Gamut mapping in HSL/HWB Mar 7, 2022
@facelessuser
Copy link

facelessuser commented Mar 7, 2022

Part of the issue is the examples were most likely generated with https://colorjs.io/. They haven't been updated since the spec suggested using Oklch to gamut map. I believe the CSS examples still reflect what color.js is using.

  1. Color.js is using Lch, not Oklch, so if you implement gamut mapping to the spec, it'll be different.
  2. The algorithm in general is also slightly different. I think the CSS algorithm is tuned to be a bit faster as it limits the ∆E calls. This results in chroma sometimes being a bit more aggressively reduced than it is in Color.js (at least that is what I have observed).

Anyways, I'm pretty sure the examples need to be updated, assuming Oklch remains the best choice for gamut mapping: #7071.

EDIT: Ignore this

@facelessuser
Copy link

facelessuser commented Mar 7, 2022

I'm going to walk back what I said a little. I'm positive I've run into some cases in the CSS spec that have not been updated, but this doesn't appear to be the case here.

Unless I'm missing something, it seems the test is mixing in display-p3, even though is says in hsl. At least when I do that, it matches the test:

Screen Shot 2022-03-07 at 6 44 00 AM

@svgeesus
Copy link
Contributor

svgeesus commented Mar 7, 2022

@devongovett thanks for raising this. Also, I wasn't aware of parcel-css and it looks like an interesting tool for authoring in more modern CSS while still covering older browsers.And if you are implementing CCSS gamut mapping in Rust, I would be interested to know whether you found the specification sufficiently clear.

Specification wise:

  1. CSS Color 5 Calculating the result of color-mix looks to be correct but converted to the specified needs a link from converted to a general term on color conversion in CSS Color 4.
  2. CS Color 4 needs the general convert between color spaces concept which can then point off to the varous existing, more specialized conversion sections such as Converting between predefined RGB color spaces. I have several times considered adding a diagram about overall colorspace conversion, and that would be a good place for it.
  3. For this particular case of convert any random color into HSL, in CSS Color 4, Converting sRGB colors to HSL colors I should add some clarifying statements that colors in non-sRGB colorspaces are first converted to sRGB; then they are CSS gamut mapped into the sRGB gamut, and then the conversion to HSL is done as per the sample code. This is implicit, but needs to be explicit.

Implementation-wise, @facelessuser is right that I need to finish off and then land the CSS Gamut mapping code in color.js, which is currently off in a branch.

Then the individual stages in this sub-test can be evaluated and the expected result checked. @weinig you wrote this test, could you share the working for this sub-test? And does it do extended sRGB to in-gamut sRGB gamut mapping in oklch or was it written at a time when it was specified to use CIE LCH? The test itself just mentions that it is testing that naive clipping is not used.

@svgeesus
Copy link
Contributor

svgeesus commented Mar 7, 2022

This seems to be the result of gamut mapping occurring in the sRGB space rather than the HSL space. However, the color-mix in the test states that the color mixing should occur in the HSL space. If I convert the HSL color to sRGB before gamut mapping, and then back to HSL afterward for interpolation the test passes. However, I don't see where in the spec that is required.

In Calculating the result of color-mix, which says (my emphasis):

Both colors are converted to the specified <color-space>. If the specified color space cannot express the color (for example, the hsl and hwb spaces cannot express colors outside the sRGB gamut), gamut mapping will occur.

It says:

For intermediate color calculations, these out of gamut values are preserved.

They are, for all the predefined RGB spaces which use the extended range (component values not not clamped to 0.0 .. 1.0). However, HSL and HWB take, as input, an in-gamut sRGB color and thus, gamut mapping will occur at that step. This sRGB to sRGB gamut mapping is computed in OKLCH

In this case, we're converting from OKLCH to HSL, and then comparing the clipped result via deltaE, so I would consider sRGB to be an intermediate space.

Yes, sRGB is an intermediate space; but sRGB to HSL requires the input values to be in gamut because HSL and HWB) cannot represent out of sRGB-gamut colors.

@svgeesus
Copy link
Contributor

svgeesus commented Mar 7, 2022

So my question: does gamut mapping happen in the sRGB space for HSL and HWB colors, or in the HSL/HWB space?

And thus, the answer to that question is "neither". All CSS gamut mapping occurs in OKLCH. The gamut mapping is triggered by the need to convert an sRGB color to HSL/HWB, which requires an in-gamut color.

Ah, I just spotted

This CSS gamut mapping algorithm
applies to individual, Standard Dynamic Range (SDR) CSS colors which are out of gamut of an RGB display

needs to be extended to also cover conversion to HSL/HWB.

@devongovett
Copy link
Contributor Author

Thanks, that makes sense. I think it would be good to call out that gamut mapping should occur before converting to HSL/HSB, as this does not occur with other intermediate color spaces.

All CSS gamut mapping occurs in OKLCH.

Yeah, sorry, I meant the part that converts from OKLCH to the destination color space in order to check it at each step of the binary search. I was confused whether this destination space was meant to be HSL or sRGB.

I would be interested to know whether you found the specification sufficiently clear.

I would say the individual parts were clear, but what was missing for me was a higher level overview of the whole conversion/interpolation algorithm. I found myself jumping around a fair bit trying to figure out in what order to apply each of the parts, e.g. when and when not to apply gamut mapping, when to replace missing components with 0 (before conversion), when to substitute converted powerless and explicit missing components, etc. I eventually figured this out, but a diagram or list of high level steps would have been nice.

@devongovett
Copy link
Contributor Author

Oh, another thing specifically about the gamut mapping algorithm that I found by looking at WebKit's implementation was how to deal with colors with OKLCH lightness values that are 0% or >= 100%. There were some tests that didn't pass without handling those edge cases specifically.

https://github.com/WebKit/WebKit/blob/0d33053bc9c52320e9aa6ce13529b7a0a27d1f9e/Source/WebCore/platform/graphics/ColorConversion.h#L195-L198

Example test:

test_computed_value(`color`, `color-mix(in hsl, oklab(100% 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 92, 255).

However, without the extra conditions that WebKit added for whites and blacks, I get rgb(255, 249, 255) by the binary search algorithm. Should those conditions be added to the spec or is the test incorrect?

@svgeesus
Copy link
Contributor

svgeesus commented Mar 7, 2022

Oh, another thing specifically about the gamut mapping algorithm that I found by looking at WebKit's implementation was how to deal with colors with OKLCH lightness values that are 0% or >= 100%.

@weinig reported that too, and so the spec was fairly recently clarified:

  1. if the Lightness of origin_OKLCH is greater than or equal to 100%, return { 1 1 1 origin.alpha } in destination
  2. if the Lightness of origin_OKLCH is less than than or equal to 0%, return { 0 0 0 origin.alpha } in destination

@svgeesus
Copy link
Contributor

svgeesus commented Mar 7, 2022

I would say the individual parts were clear, but what was missing for me was a higher level overview of the whole conversion/interpolation algorithm. I found myself jumping around a fair bit trying to figure out in what order to apply each of the parts, e.g. when and when not to apply gamut mapping, when to replace missing components with 0 (before conversion), when to substitute converted powerless and explicit missing components, etc. I eventually figured this out, but a diagram or list of high level steps would have been nice.

Fair comment. Really appreciate getting implementer comments! I will ping again in this issue when I have addressed your concerns and there is updated spec text to review.

@svgeesus
Copy link
Contributor

@devongovett ping.

I now have a new section in CSS Color 4, Converting colors which lists the steps to go from color1 in some color space src to color2 in some colo space dest.

It includes converting missing values to none, and explicitly lists the two cases (displays, and HSL/HWB) where gamut mapping is needed before the next step in the conversion. It also covers chromatic adaptation, if there is a different white point.

Please take a look, if it seems ok then I will add examples.

Is the list of steps sufficient or would a diagram in addition make it clearer?

@devongovett
Copy link
Contributor Author

Thanks for the ping @svgeesus! Sorry for the slow response. This is very helpful. 👍

I really liked the diagram in @LeaVerou's recent blog post btw. Something like that might be useful as well. https://lea.verou.me/2022/06/releasing-colorjs/

@svgeesus
Copy link
Contributor

svgeesus commented Jul 5, 2022

Yes, a subset of that would be a useful addition.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants