-
Notifications
You must be signed in to change notification settings - Fork 660
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-4] [css-color-5] inconsistent mentions of powerless components in white
.
#8609
Comments
This is because the OkLCh algorithm does not resolve to a perfect zero (except maybe for black) when a color is achromatic. There are some color spaces that do even worse, but OkLCh is the worse offender in CSS, and it really isn't that bad. Compared to LCh though, OkLCh does a worse job and is actually more sensitive to the hue because chroma doesn't get as low as it does with LCh. The ideal hue for OkLCh is somewhere around 90˚ based on the CSS matrices, but using zero ends up being "close enough" and introduces only minimal error, but may slightly impact the round trip. >>> Color('white').convert('oklch').set('hue', 0).convert('srgb')[:]
[1.000000056645113, 0.9999999632010229, 1.0000001023697391, 1.0]
>>> Color('white').convert('oklch', undef=False).convert('srgb')[:]
[1.0000000000000049, 0.9999999999999981, 0.9999999999999999, 1.0]
>>> Color('white').convert('oklch', undef=False)[:]
[0.9999999935000001, 3.729999997759137e-08, 90.00000025580869, 1.0]
>>> Color('white').convert('oklch')[:]
[0.9999999935000001, 3.729999997759137e-08, nan, 1.0] The CSS spec does mention the powerless when "close to zero" for things like LCh and OkLCh, but yes, it gives no specific directions beyond that. It also doesn't mention this for HSL, so conversions from OkLCh to HSL, could leave you with a color close to achromatic, but not quite in HSL.
Calculating some perfect threshold for each cylindrical space that resolves to identify achromatic exactly the same in all spaces when you convert to them may prove difficult as each color space has different levels of accuracy for chroma/saturation equaling zero, and the scaling is a bit different. I'm not sure how much CSS cares about this, at least with the current included color spaces, if most achromatic colors are firmly in the achromatic pocket, it is likely you could pick a threshold tailored to each space that is close enough. You could eliminate a lot of these achromatic detection differences between spaces by identifying achromatic colors in a common space, like maybe taking the cross product of the color vector against the white point vector in the XYZ D65 color spaces (with some threshold for deviation away from zero). Then "closeness" for achromatic colors would be the same for each space. This would require an XYZ conversion when checking achromatic colors though. |
That is not really what is happening here. This should be true white mixed with blue in color-mix(in oklch, oklch(100% 0 0), blue); vs. color-mix(in oklch, oklch(100% none none), blue); Browsers just aren't handling powerless/missing components correctly. But the specification is also inconsistent when referring to these powerless/missing components:
This is not a math/floating point issue, it is a specification and implementation issue :) |
Ah, well the spec does mention it, just no real specifics :). If implementations do choose to rely on a hard 0, the previously mentioned issues may arise as well, but I get what you are saying. |
So, I've actually taken a closer look at this. If powerless means that interpolation should treat the components as missing, I think the current CSS spec is heading in a confusing direction. Let's take a look. Here we create two OkLCh colors But let's look at the entire interpolation: Oh no, now we have a huge shift in color. This defeats the whole purpose of using OkLCh! What happens if we use black? Just as bad. So, why do we want chroma as powerless treated as missing for black and white during interpolation? |
I guess I'd make the same argument for forcing |
Right, clearly for achromatic colors |
This part is just plain wrong:
Saying that missing components get treated as zero is fine (but assuming that always happens is not fine; in interpolation the missing component is treated as the analogous component in the other color). Saying that zero-valued components are always missing, on the other hand, is just wrong. |
@facelessuser Are you sure there aren't any bugs on your end? I don't understand why you would get a hue shift here. .foo {
background: linear-gradient(to right, rgb(255, 255, 255) 0%, color(display-p3 0.57766 0.85014 1.67992), color(display-p3 0.51024 0.77327 1.59733), color(display-p3 0.44387 0.69646 1.51535), color(display-p3 0.37854 0.61944 1.43399), color(display-p3 0.31424 0.54177 1.35327), color(display-p3 0.25084 0.4627 1.27319), color(display-p3 0.18808 0.38082 1.19377), color(display-p3 0.12528 0.29307 1.11502), color(display-p3 0.05998 0.19058 1.03695), rgb(0, 0, 255) 100%); /* generated fallback */
background: linear-gradient(to right in oklch, white 0%, blue 100%); /* original */
} Should look like : .foo {
background: linear-gradient(to right, rgb(255, 255, 255) 0%, color(display-p3 0.89543 0.92911 1.00686), color(display-p3 0.79238 0.85738 1.01077), color(display-p3 0.69089 0.78448 1.01198), color(display-p3 0.59093 0.70993 1.01068), color(display-p3 0.49247 0.63302 1.00706), color(display-p3 0.39536 0.55257 1.0013), color(display-p3 0.29925 0.46652 0.99353), color(display-p3 0.20328 0.37049 0.9839), color(display-p3 0.10457 0.25236 0.97254), rgb(0, 0, 255) 100%); /* generated fallback */
background: linear-gradient(to right in oklch, oklch(100% 0 none) 0%, blue 100%); /* original */
} Should look like : The second example with It is not unlikely that what I am doing here isn't a good way to verify this part of the specification. But using the tools I have, to try and illustrate how we've interpreted and implemented the specification. |
@romainmenke, I was still gamut mapping with LCh, so that's where the purple shift comes from 🤦🏻. This just affects the color that is displayed, not the computed values. But it still doesn't make sense to resolve chroma to missing. Again, let's take a look, this time doing things just like browsers. So, now using clip to keep in the gamut like browsers do, I don't get the purple shift in display (the computed values are still identical), but because we set chroma to missing, we we aren't interpolating from white to blue, it ends up as a super bright blue to normal blue as white adopts the blue chroma and hue. And before we say that there must be something wrong with what I am doing, let's look at Safari technical preview in Codepen. How are these results better? We lose the white and end up with an interpolation between bright blue and blue. That's not what the intent was. |
In short, what I'm saying is that the spec example yielding |
@facelessuser are you sure that what you are seeing aren't implementation bugs :) A gradient that has a starting color stop of white shouldn't be blue. https://codepen.io/romainmenke/pen/ZEMNYLO Chrome today : Safari today : Firefox nightly today (is missing needed features) : It is clear that no browser currently has a good/working implementation for "interpolate between white and blue in oklch". There are so many bugs here around color space conversion, handling of powerless components and out of gamut clipping that it is safe to say that all are just plain broken. generated fallbacks might still output display-3 which can express out of gamut colors. These generated fallbacks are sensitive to the same out of gamut clipping issues. |
Yes, I am certain. Mathematically, it makes no sense. Forget what it looks like. I use references only because visually can sometimes be easier to talk about, but in this case, let's just look at the math of the models. Let's say I have Let's convert it to LCh for instance, we get a color with negative lightness. >>> Color('oklch(0 0.31321 264.05)').convert('lch')
color(--lch -5.3831 104.75 315.62 / 1) The fact that the model allows you to convert it, doesn't mean it is a real, visible, and useful color. It's not only out gamut, it's out of the visible spectrum. It has a negative luminance. >>> Color('oklch(0 0.31321 264.05)').convert('xyz-d65')['y']
-0.004737023320937883 Whatever it happens to map to visually once you coerce it into the real visible spectrum is irrelevant. There is a reason that my implementation matches Safari, it's because what they are doing (in this specific interpolation case) makes sense. It's a nonsense color that they clip, and yes, you happen to get blue in OkLCh if visualized after forcing it to be visible via clipping, but whatever it looks like, it's a negative luminance color, it's a nonsense color. |
But the specification is perfectly clear on what to do in this case.
The end result is I don't understand why you would get |
Yes, CSS has conventions for undefined values when converting to other colors, but I'm talking about the color model. I'm trying to illustrate why what you are suggesting makes no sense. But. let's take one more approach and let's incorporate the conversion logic. When we are interpolating We get a small band of white at the beginning. At this point, I think I've illustrated my reservations in many different ways. The summary is that if you want to interpolate, only adjusting lightness, yes, At this point, I'll let others comment as I'm not sure I can rephrase my point in many other different ways 🙂 . |
@facelessuser I think I am starting to get what you are saying. Interpolation as specified can lead to achromatic colors with high chroma. As specified :
If color-mix(in oklch, oklch(1 none none) 99.99%, oklch(0.45201 0.31321 264.05)) That will indeed be a super saturated, near 100% lightness blue-isch. But I still fail to see where the issue is in having super saturated colors that are near 100% lightness? The visual examples are indeed adding confusion.
To clarify, I am not suggesting anything here :) I found an inconsistency between two specification. Aside from this inconsistency in the spec text, the implementations in browsers also happen to be bogus and wildly different. |
Yes, there is no other way to visualize it. Many of the super bright blues are probably out of gamut and forced back into the gamut with clipping.
This may be so, and I'm obviously not attempting to provide a solution there, I'll let the spec writers do that, but I wanted to mention that the example in the spec, even if the powerless part is confusing, seems to make the most sense. Forcing chroma to be
There is nothing wrong with interpolating super saturated colors, but my argument is white and black should not normalize by default, when converted to an LCh space, to |
Thank you for clarifying, yes I get what you mean now. I have no strong opinion about this and it was also not the focus of this issue :) Currently the specification requires powerless components to become missing when converting between color spaces and states that both |
Well, it's related to the fact that it would help steer the resolution on a consistent powerless approach. I feel others may come to a similar conclusion that you did, so being clear about what is ideal and not ideal is important when attempting to resolve the confusion and get everyone on board with the same convention. CSS may go against my personal opinions in this regard, but I think it is important to lay out the reasons.
Yep, and I think that is a mistake. I think "powerless" for LCh chroma or Lab a and b, if such a thing must be enforced, should mean zero. Hue is a bit special and makes sense to be missing for interpolation of achromatic colors. There are some color spaces where zero doesn't always make sense for chroma, JzCzhz actually has an exponential chroma response for achromatic colors as lightness increases, though it is still low enough that you can probably get away with clamping it at the loss of only a little accuracy. Google's new HCT color space gets really high (as far as chroma goes) because it's based on CAM16 without discounting, but I'm getting off-topic now 🙂. >>> Color('white').convert('jzczhz', norm=False)
color(--jzczhz 0.22207 0.0002 216.08 / 1)
>>> Color('white').convert('cam16-jmh', norm=False)
color(--cam16-jmh 100 2.2369 209.53 / 1) |
I agree the current specification is perfectly clear to do this; but as I also said earlier the current specification is completely incorrect to make Chroma powerless here. It is zero, not unknown or missing. Hue is missing, sure (although that then gives a discontinuity when L rises to, say, 0.001). |
Can we then check and fix these parts of css-color-4?
|
Done (for achromatic colors,
What problem do you see with the analogous components? |
Thank you 🙇 For analogous components it would be good to verify if As currently specified these do not and therefor there is no carry over when This seemed relevant since the powerlessness of a component and analogous components determine the outcome of interpolation. Verifying the correctness of both and having WPT tests of the expected outcomes should help to resolve the implementation bugs we are currently seeing. This change also needs to be reverted : #8631 |
I don't understand what you mean by this. The spec does not say that zero-valued components are missing. It says that certain components, when zero-valued, can cause other components to be powerless (and thus missing, after conversions). Your comment appears to be a non sequitur?
Chroma is powerless when lightness is 0% because any chroma value results in the exact same color (black). (And maybe the same is true of white, I forget the outcome of our conversation about this earlier.) Powerless is not missing or unknown. The component is zero, and also powerless (because of the lightness, not because of its own value). I sincerely don't understand what in the spec you're objecting to, here. |
Yes, the "powerless" concept has a sharp boundary. That's unfortunate, but there's not a great way to handle this gradually, like we can with premultiplied alpha. (Or at least doing so was more complicated than we thought worthwhile.) |
@svgeesus @tabatkins When there is time it would be good to revisit this. Currently the specification, WPT tests and implementations all differ. I don't mind spending time to submit patches if I know in which direction to go. |
Ah, good point. Lets see:
Once we got rid of the ill-fated attempt to deal with CIE Lightness as potentially going up to 400% then yes, the same is true of white. And we have WPT for that (which didn't get tagged with Interop2022 because it had already started, and which all browsers fail):
Then, this is what the spec currently says, and it should say
Lets get consensus on the spec for this aspect, then pull WPT into alignment with that. |
Are these tools you mention up to date with the latest specification? Can you provide reproducible examples? |
As far as the topic of demonstrating proper conversion and interpolation, both libraries support the features to demonstrate correct results if configured correctly. They may not demonstrate browser "automatic" behavior unless you configure the statements correctly. So don't let your assumptions about browsers fool you into thinking they are not correct, they just don't automatically do what browsers do by default. I don't maintain Color.js and only use Color.js casually, so I cannot speak to whether there are bugs, but as far as the current topic is concerned, it appears to be correct.
Let me clear something up. My library will not assume hue is powerless like CSS does when an explicit hue is provided (conversion may cause a hue to go powerless). So I am not demonstrating how a browser should handle chroma. What I am demonstrating is the claim that your results are treating hue has powerless is not correct. If it was treating hue as powerless you would have had something approximately close to That is my statement. I demonstrate this by showing an example with an explicit hue that is not being treated as powerless and one where it is.
Yes, put this into https://colorjs.io/notebook/. And you will get your RGB results, but with an OkLCh hue of 30. The second example will give you a powerless example. let color1 = new Color("oklch(75% 0% 60deg)");
let color2 = new Color("oklch(75% 50% 0deg)");
let notPowerless = color1.mix(color2, {space: 'oklch'});
notPowerless.to('srgb').toGamut({method: 'oklch.chroma'});
color1.set('hue', NaN);
let powerless = color1.mix(color2, {space: 'oklch'});
powerless.to('srgb').toGamut({method: 'oklch.chroma'}); You will have to convert the percent values to non-percent, but you will see it matches my results.
It depends on what part of the specification you are talking about. Color.js behaves in a similar way as my library, so explicit, user defined hues will not be treated as undefined unless a conversion causes them to be undefined. So don't assume browser behavior in this sense, but the logic of the statements is correct. Color.js does not support While my goal is not to mirror the browser behavior, I am able to demonstrate it when required. |
@romainmenke Even in your own tool, the only way to get It is possible that you made a typo? |
Isn't it confusing to go beyond the scope of CSS and expected browser behavior? To me this feels like comparing apples and oranges and it keeps throwing me off balance. I will gladly admit that I am often incorrect here and that I make many mistakes both in statements here and in implementations. So I welcome anything that challenges my assumptions. But I think it would be best to keep everything focussed on CSS and expected browser behavior. I also try to avoid comparing with tools that go beyond the scope of CSS because it is too easy to get lost in what is essentially user error.
That is always possible As far as I know these things are true :
So when applying this by hand :
Ignore any tool, library, browser implementation. Are these steps correct for the current latest versions of all relevant specifications? |
Correct. We don't currently support |
I think I am often confused because I don't know exactly which values you are challenging. In this case I thought you were questioning the validity of the I think I know what we are seeing. In the current version of the specification Setting So the This unfortunately means that users still can not mix a random color with some other color in the same color space where one of them has no chroma/saturation. In those cases there will be an unexpected hue shift. Users would need to :
I think this is problematic because the input colors might not be known to the user. They might be the result of relative color syntax or https://drafts.csswg.org/css-color/#powerless
|
I apologize as I guess I'm not always as clear as I should be. I often don't use browsers to demonstrate as they are a bit all over the place in implementations, though they are getting better. But that means I have to demonstrate with tools, and people obviously are not familiar with how those tools behave. I often think what I'm showing is clear, but that is because I know how what I'm showing works. |
I appreciate that @facelessuser 🙇 To circle back to the question:
No, this was recently edited so that only
current specification :
I am fairly certain that this is correct for the current latest specification, but as stated previously, I might be wrong about stuff. Please let me know if I missed something else that also triggers the conversion from |
@romainmenke our results match 🙂. Live example here The only thing that could change this is if CSS requires an OkLCh (or any cylindrical space) hue to become missing during interpolation even if an explicit hue is defined and chroma is zero. I don't recall exactly what CSS specifies here, and isn't something I would automatically do in a generic color library by default, but if browsers need to do this, or need to in the future, obviously some results would be different after normalizing those hues to "missing". |
@facelessuser wrote:
It does require it. The spec says:
and also says, for Oklch:
and, for CIE LCH:
and says, for HSL:
HWB is polar but doesn't have a Chroma or Saturation analog so the HWB doesn't have powerless hues. Which is odd, but I don't see an obvious way around that. It also says, regarding hues (my emphasis):
Lastly, and allowing some implementation-defined wiggle room it says:
|
@svgeesus But And the specification currently also says that
|
From my understanding I was also aware that carryforward needed to be handled before powerless handling. I had a vague recollection that during interpolation CSS may ignore explicit hues if chroma is zero (or near zero). It's also not something I've implemented in my color library on purpose as I want users to have explicit hues respected. If conversions occur, all bets are off. But I understand why CSS would do this: to make it so people don't have to think about things when they interpolate with achromatic colors. My goals are a bit different than CSS. I imagine I could always add a CSS normalization flag if needed. Does an undefined chroma cause hue to be powerless as well? I know zero chroma does, and I know when undefined is normalized it becomes zero, but technically |
Has no effect on the rendered color, as it says right above your quoted text:
But yes, I see your point: if one or both of the colors to be interpolated is already in the interpolation colorspace, it will have a powerless component not a missing component. The current text assumes that all colors are converted into the interpolation colorspace, and the part about classifying missing components doesn't quite capture that. |
I guess that answered my question as well. If all colors are assumed to be converted, then |
Oh, I see now. This conversion assumption (all colors are converted for interpolation) makes carryforward mandatory or it would wipe out user-defined |
Also see : #9220 |
Yeah, getting all of this clear is important. Unfortunately, if colors already in the color space have hue normalized, and if chroma is |
I think I brought this issue up with @tabatkins a while ago, and the conclusion was that the text (source)
was interpreted to mean that conversion always happened for interpolation, even when it was to the same color space, and so powerless components became missing. Tab may have a better source for that than my memory. |
I've updated the example to use CSS's "powerless" hues and carryforward (though carryforward doesn't apply to any of the previous cases). Only the second case is different:
Assuming I haven't made a misstep in interpreting how powerless hues act, this should be correct. I guess this is subject to change if I'm way off 🙃. Live example here. |
This however isn't correct for the current specification, which literally states that conversion only happens when required. So maybe the wording needs to change? https://drafts.csswg.org/css-color/#interpolation emphasis mine :
I've updated our implementation so that conversion always happens, even when the input and output color space are the same. This effectively removes the difference between As far as I can tell this gives better results when interpolating between any color and white/black/... when both are described in the same cylindrical polar color model. You can play around with this here : https://preset-env.cssdb.org/blog/css-color-parser-v1.0.0/ I've also added an output in @facelessuser For the second case I now have :
I think this matches your results, right? |
@romainmenke Yep, as far as I can see, it looks like an exact match. |
Color.js now accepts |
…powerless components become missing #8609
Agreed, and now required with 1b3a4bf |
https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch
So
white
should be equivalent tooklch(100% none none)
.https://drafts.csswg.org/css-color-5/#ex-mix-blue-white
When mixing in
oklch
:color-mix(in oklch, white, blue);
Chrome :
oklch(0.725987 0.15663 323.92)
Safari :
oklch(0.72600687 0.15660721 177.02602)
Specification :
oklch(72.601% 0.15661 264.052)
What I think it should actually be :
oklch(72.601% 0.31321 264.052)
I think we are seeing the same implementation issues as in :
none
and interpolation color spaces. #8563But this might require different specification and test edits to resolve.
The text was updated successfully, but these errors were encountered: