Skip to content

[css-color-4] Define epsilon (or range of valid epsilons) for determining when Lab/OKLab should be considered achromatic #11706

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

Closed
weinig opened this issue Feb 14, 2025 · 24 comments

Comments

@weinig
Copy link
Contributor

weinig commented Feb 14, 2025

The current CSS Color 4 spec doesn't define specific values for when a Lab/OKLab color should be considered achromatic, stating:

For extremely small values of a and b (near-zero Chroma), although the visual color does not change from being on the neutral axis, small changes to the values can result in the reported hue angle swinging about wildly and being essentially random. In CSS, this means the hue is powerless, and treated as missing when converted into LCH or OKLCh;

(https://drafts.csswg.org/css-color-4/#lab-to-lch)

The lack of specificity here (and therefore lack of testability) has lead to incompatibility between browsers (see https://bugs.webkit.org/show_bug.cgi?id=287637 for an example).

Is there some reason we cannot define what this "extremely small values of a and b (near-zero Chroma)" is? If defining specific constants is not ideal, perhaps some bounds?

@weinig weinig added the css-color-4 Current Work label Feb 14, 2025
@LeaVerou
Copy link
Member

Thanks for filing this Sam!

I think the balance here is that too small an epsilon and we get precision errors, too large and we get bugs like the one I spent nearly all of my workday on today 🫠

However, we can definitely do better. At the very least we should say that the exact epsilon should be color space specific, and consider the extent of each coordinate. And hopefully define some bounds, so that the behavior is testable.

cc @svgeesus

@svgeesus
Copy link
Contributor

I agree that some bounds could be given.

It also depends to some extent on the precision of internal representation.

The cumulative precision errors can become noticeable with much smaller epsilon than one JND so this can't really be specified as a ΔE.

@svgeesus
Copy link
Contributor

Would be handy to know what epsilon is used for (CIE) lch and for oklch in Blink, and in Gecko.

@lloydk
Copy link

lloydk commented Feb 14, 2025

I think bounds should be specified for the Jzazbz color space from CSS Color HDR as well unless Jzazbz is going to use a different method to determine if colors are achromatic. See color-js/color.js#629

@svgeesus
Copy link
Contributor

@svgeesus
Copy link
Contributor

unless Jzazbz is going to use a different method to determine if colors are achromatic. See color-js/color.js#629

It looks like it might need to be special-cased, yes. Trade off between capturing all the neutrals, and loosing some near-neutrals.

@svgeesus
Copy link
Contributor

So it is now clear that ε should be chosen to encompass the narrow range of values close to the central axis where the hue angle is ill-conditioned and replaced with none.

In many spaces (but not all) that is also where achromatic colors converted from some RGB space where R=G=B fall. For the spaces where that is not true, that is because of color appearance; the eye is not fully adapted and so neutrals gradually diverge from the central axis as the neutral gets brighter. But the hue angle is not ill-conditioned and the ε should not be made artificially large so as to encompass these colors.

Given that, I think we can firm up the distastefully wooly prose we currently have:

User agents may treat a component as [=powerless=]
if the color is "sufficiently close" to the precise conditions specified.
For example, a gray color converted into ''lch()'' may,
due to numerical errors,
have an extremely small chroma rather than precisely ''0%'';
this can, at the user agent's discretion,
still treat the hue component as [=powerless=].
It is intentionally unspecified exactly what "sufficiently close" means for this purpose.

In particular, dropping that last sentence and giving specific guidance on ε.

@svgeesus
Copy link
Contributor

It looks like it might need to be special-cased, yes. Trade off between capturing all the neutrals, and loosing some near-neutrals.

That is no longer my position.

@svgeesus
Copy link
Contributor

@weinig you wrote

the extent of the reference range divided by 100000. (so, (0.4 - -0.4) / 100000 for oklch and (125 - -125) / 100000 for lch).

That looks good, and for Jzazbz the extent of the reference range is (0.21 - -0.21) while for ICtCp it is (0.5 - -0.5).

@svgeesus
Copy link
Contributor

PROPOSED Resolution: The epsilon for converting chroma to none is the extent of the reference range divided by 100,000

@svgeesus svgeesus added Async Resolution: Proposed Candidate for auto-resolve with stated time limit and removed Needs Data Needs Design / Proposal labels Mar 19, 2025
@svgeesus
Copy link
Contributor

@astearns

@astearns
Copy link
Member

The CSSWG will automatically accept this resolution one week from now if no objections are raised here. Anyone can add an emoji to this comment to express support. If you do not support this resolution, please add a new comment.

Proposed Resolution: The epsilon for converting chroma to none is the extent of the reference range divided by 100,000

@astearns astearns added Async Resolution: Call For Consensus Resolution will be called after time limit expires and removed Async Resolution: Proposed Candidate for auto-resolve with stated time limit labels Mar 24, 2025
@svgeesus svgeesus added the css-color-hdr CSS HDR extension label Mar 25, 2025
@svgeesus
Copy link
Contributor

Tagging in css-color-hdr as the same resolution will apply there also

@facelessuser
Copy link

Proposed Resolution: The epsilon for converting chroma to none is the extent of the reference range divided by 100,000

I'm a little confused if there is a typo in this proposal. This is a proposal is for determining when hue is none, not chroma, right? I say this as it is stated as chroma becoming none, and I don't think that is the intention.

@svgeesus
Copy link
Contributor

You are right, let me clarify. The proposal is to standardize an ε for chroma "sufficiently close" to the central axis that hue will become none. So the chroma is compared to ε, and the result of that comparison affects the hue.

@facelessuser
Copy link

Does this apply to LCh as well? Is LCh using c / 100,000 and Lab using (a - b) / 100,000 or does LCh need to break chroma into a and b values so it can have a similar threshold to it's rectangular counter part?

I bring this up because chroma divided by 100,000 and the difference of a and b divided by 100,000 will produce different thresholds, but Lab and LCh are the same color spaces, just represented in different coordinate systems. I assume the desire is that the achromatic threshold would be the same for both rectangular and polar representations.

It seems you could provide interlock between the two coordinate systems by either selecting the difference of a and b divided by 100,000, converting polar chroma to a - b, or by specifying the reference range as chroma divided by 100,000 and requiring Lab-like representations to convert a and b to chroma when checking.

@svgeesus
Copy link
Contributor

It seems you could provide interlock between the two coordinate systems by either selecting the difference of a and b divided by 100,000, converting polar chroma to a - b, or by specifying the reference range as chroma divided by 100,000 and requiring Lab-like representations to convert a and b to chroma when checking.

This only applies to polar representations, right? So chroma range/100,000

@facelessuser
Copy link

Maybe it will help if I illustrate my confusion.

Consider Lab which has a reference range of 125 and -125. In Lab the threshold would be (125 - -125) / 100,000 = 0.0025.

I assume then the a and b component are compared to this threshold, so lab(50% 0.0025 0.0025) is on the edge of this threshold.

The reference range for LCh is 150 for chroma, 150 / 100,000 = 0.0015.

If we convert lab(50% 0.0025 0.0025) to LCh, we get lch(50% 0.00354 45) which exceeds the achromatic threshold in LCh. Is that desired? Do we care that LCh and Lab, which represent the same space, but in different coordinates, have a difference in what they consider achromatic?

@svgeesus
Copy link
Contributor

Consider Lab which has a reference range of 125 and -125. In Lab the threshold would be (125 - -125) / 100,000 = 0.0025.

I assume then the a and b component are compared to this threshold, so lab(50% 0.0025 0.0025) is on the edge of this threshold.

But Lab doesn't have a hue angle.

@facelessuser
Copy link

facelessuser commented Mar 27, 2025

But Lab doesn't have a hue angle.

I guess that's why I'm confused as well. We are using lab components when converting to the polar space to determine if the hue is none?

(0.4 - -0.4) / 100000 for oklch and (125 - -125) / 100000

You kind of agree to this without clarification:

PROPOSED Resolution: The epsilon for converting chroma to none is the extent of the reference range divided by 100,000

Did you mean to use the chroma reference range or the lab component range?

I personally find it odd to use the lab range, especially since it says this in interpolation:

converting them to a given color space which will be referred to as the interpolation color space below. If one or both colors are already in the interpolation colorspace, this conversion changes any powerless components to missing values

It seems using chroma would be the preferred approach as you would also need to convert hues to powerless while already in the polar space. Maybe this is what you meant? I probably didn't explain myself well from the start.

@facelessuser
Copy link

Looking at Color.js, which uses chromaRange / 100,000 to determine achromatic, if we can assume this as a model for the proposal, then it seems that resolves all my confusion. I had just thought the Lab reference range was being suggested which didn't make sense. If I'm wrong, I'm sure I'll be corrected :).

@svgeesus
Copy link
Contributor

svgeesus commented Apr 1, 2025

chromaRange / 100,000 to determine achromatic, if we can assume this as a model for the proposal, then it seems that resolves all my confusion

Yes, exactly

@svgeesus svgeesus added Closed Accepted by CSSWG Resolution and removed Async Resolution: Call For Consensus Resolution will be called after time limit expires labels Apr 1, 2025
@svgeesus
Copy link
Contributor

svgeesus commented Apr 1, 2025

@astearns informed me that the resolution is accepted, and deputized me to label it so.

RESOLVED: The epsilon for converting chroma to none is the extent of the reference range divided by 100,000

Following further discussion in this issue, the clarification is that, when converting colors to a polar form, the epsilon for a sufficiently small chroma to trigger missing hue is the reference range of the chroma, divided by 100,000

@svgeesus
Copy link
Contributor

svgeesus commented Apr 1, 2025

@weinig since you mentioned testability, if you make WPT for this, ping me for a review

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

6 participants