WebKit Bugzilla
New
Browse
Log In
×
Sign in with GitHub
or
Remember my login
Create Account
·
Forgot Password
Forgotten password account recovery
RESOLVED FIXED
287637
Color space conversion converts oklch hue to 0 for low chroma colors
https://bugs.webkit.org/show_bug.cgi?id=287637
Summary
Color space conversion converts oklch hue to 0 for low chroma colors
Lea Verou
Reported
2025-02-13 09:37:40 PST
Testcase:
https://dabblet.com/gist/752ccaec7435edfc36eb48a4c3317f1e
Related tweet:
https://x.com/LeaVerou/status/1890070546781806985
This seems to affect all code paths that convert between color spaces, including: - Relative color syntax in oklch() - color-mix(in oklch, ...) Using oklch() colors does not trigger the bug, since it skips the color space conversion code path (unless the color is first converted to another color space, e.g. `rgb(from oklch(80% 0.02 230) r g b)`). 3. It does not happen for every color, but only affects certain coordinate ranges. The following ranges refer to oklch coordinates (converted to rgb to trigger the bug, see 1). 3a. It never occurs for L <= 60% regardless of C and H. 3b. It never occurs for H=180 or H=270 (!) regardless of L and C. 3c. I never occurs for C >= 0.027. C ∈ (0.02, 0.027) triggers it for some hues, and I cannot figure out what they have in common. C <= 0.02 seems to trigger it reliably for every hue. I used this playground to find these ranges:
https://dabblet.com/gist/a518655f8eef4c074946b6ce5ba38243
4. It only seems to happen for oklch. Not lch, not lab, or any other color space I tested.
Attachments
Add attachment
proposed patch, testcase, etc.
Lea Verou
Comment 1
2025-02-13 13:12:35 PST
Some additional findings: - It is not correct that it never occurs for L <= 60. It seems to occur for *some* L values below that range, but not others. There are even certain values of L and C that reproduce it reliably for every hue. - When it occurs for an L < 60%, it seems to occur for *every* hue, even 180 or 270.
Sam Weinig
Comment 2
2025-02-13 18:41:15 PST
Taking just the case relative color case (since that is simpler) of `oklch(from #e5f6ff l c h)`, we have the following conversion chain: sRGB (8-bit) -> sRGB (float) sRGB (float) -> linear SRGB (float) linear SRGB (float) -> xyz-d50 (float) xyz-d50 (float) -> oklab (float) oklab (float) -> oklch (float) We can simplify things by starting with `oklab(0.963236391 -0.0137705645 -0.0167271886)` instead of #e5f6ff, and we can see the same issue, so we know its an issue in the polar form conversion. What seems to be going on here is that the code to detect acromatic colors is improperly getting triggered. It sees that both abs(`a`) and abs(`b`) are less than 0.02, our chosen epsilon, and sets the hue to `none`. I can't recall why 0.02 was chosen as the epsilon, but clearly it is too way too big. My guess is that we chose it lab -> lch, where values are much bigger, and we just need a separate epsilon for lab -> oklch here that is a bit smaller.
Sam Weinig
Comment 3
2025-02-13 19:13:54 PST
Pull request:
https://github.com/WebKit/WebKit/pull/40605
Sam Weinig
Comment 4
2025-02-13 19:21:11 PST
PR updates the epsilon value to match color-js, using the extent of the reference range divided by 100000. (so, (0.4 - -0.4) / 100000 for oklch and (125 - -125) / 100000 for lch).
Sam Weinig
Comment 5
2025-02-13 19:32:40 PST
I also filed
https://github.com/w3c/csswg-drafts/issues/11706
so that we can get this in the spec. The WebKit behavior is technically valid (the best kind of valid), since there is no defined value for "extremely small values of a and b (near-zero Chroma)", but clearly not compatible.
Lea Verou
Comment 6
2025-02-13 21:04:23 PST
Wow, that was fast! Thank you Sam! One note: I think it is actually converting to 0, not `none`. See this testcase:
https://dabblet.com/gist/af2e51f27f2d019806fd6aaf096c0aab
An actual `none` resolves to the other hue in color-mix(), but here we get an average.
Chris Lilley
Comment 7
2025-02-14 06:27:57 PST
Given that both sRGB and Oklab use a D65 whitepoint, why is there chromatic adaptation to D50 here? sRGB (8-bit) -> sRGB (float) sRGB (float) -> linear SRGB (float) linear SRGB (float) -> xyz-d50 (float) xyz-d50 (float) -> oklab (float) oklab (float) -> oklch (float) should be sRGB (8-bit) -> sRGB (float) sRGB (float) -> linear SRGB (float) linear SRGB (float) -> xyz-d65 (float) xyz-d65 (float) -> oklab (float) oklab (float) -> oklch (float)
Sam Weinig
Comment 8
2025-02-14 08:28:55 PST
It was just a typo on my part (I was doing the chain from memory). It is d65 of course both in spec and in the implementation.
Chris Lilley
Comment 9
2025-02-14 08:50:56 PST
Thanks. I was checking because an unwanted chromatic adaptation would tilt the neutral axis and could have explained lighter colors being more affected, and some hues (in direction of tilt) being more affected than others.
Sam Weinig
Comment 10
2025-02-14 09:03:30 PST
Lea, I think the color-mix() issue is actually separate, as you don't need any achromatic checking to get it. For instance, the following will show the same: `color-mix(in oklch, oklch(from oklch(0.96 0.02 none) l c h), oklch(none none 230.5));` This issue stems from the question of what the behavior of how `none` (and missing) are treated by RCS syntax, taking into account things like calc(). I believe their are at least two open issues on this (one from me and one from you :) ):
https://github.com/w3c/csswg-drafts/issues/10211
https://github.com/w3c/csswg-drafts/issues/10280
If this has been resolved, and I am just not aware of it, I would be happy to update WebKit.
Lea Verou
Comment 11
2025-02-14 10:44:40 PST
Ah, yes, that does seem to be a separate bug! I think the issues you linked to are about calculations with none though (e.g. calc(none + 10)) (and there is a WG resolution in the former, it just needs spec edits), the result of interpolation where one value is `none` and the other is not has been well defined for a while — that's what `none` was defined for!
Sam Weinig
Comment 12
2025-02-14 11:32:00 PST
In the second one I ask: ``` Should the none be carried forward for the bare identifier but not calc?: color(srgb none 0 0) Should the none be carried forward for the bare identifier and calc? If so, what is none * 2 evaluate to? ``` It wasn't clear to me there was resolution here. But again, if there is, happy to update the engine. And to be clear, for interpolation things work: ``` color-mix(in oklch, oklch(0.96 0.02 none), oklch(none none 230.5)); ``` Should work just fine (that just removes the RCS syntax from the example I gave in my last comment). The only issue is how does `none` work with placeholder identifiers.
EWS
Comment 13
2025-02-14 13:47:08 PST
Committed
290417@main
(500b1a7d41ac): <
https://commits.webkit.org/290417@main
> Reviewed commits have been landed. Closing PR #40605 and removing active labels.
Radar WebKit Bug Importer
Comment 14
2025-02-14 13:48:15 PST
<
rdar://problem/144884767
>
Note
You need to
log in
before you can comment on or make changes to this bug.
Top of Page
Format For Printing
XML
Clone This Bug