Open romainmenke opened 1 year ago
Chrome and Safari seem to agree that lab(50% calc(Infinity) 0) serializes as lab(50% Infinity 0), but that no longer roundtrips because the required calc was removed.
Yes, that's clearly wrong. The arguments aren't calculations, they're either keywords (channel names) or math functions. We should get a WPT enforcing this.
Rendering is just a consequence of the "clamp it to your actual allowed range" behavior that V&U specifies for infinite (and other arbitrarily-large finite) values. I'm not sure what the actual behavior of a ginormous a
value should be, but whatever it is, that's the correct rendering. (I suspect white? But I have no actual knowledge here.)
I'm not sure what the actual behavior of a ginormous a value should be, but whatever it is, that's the correct rendering. (I suspect white? But I have no actual knowledge here.)
Also unsure. Consistency would be nice, but I can imagine that this is very sensitive to implementation details.
In our implementation for the a
/b
component :
white
black
color.js
seems to produce white and blue after gamut mapping. But I am never sure if I am using that library correctly.
let color = new Color("lab(50% 0 0)");
color.coords[1] = Infinity
color
color.toGamut('srgb')
I've added a few tests for serialization of calc(Infinity)
: https://github.com/web-platform-tests/wpt/pull/39137
I have no idea how NaN
should be handled, so I skipped that for now.
I have no idea how
NaN
should be handled, so I skipped that for now.
If I am not mistaken, only nested math functions (unresolved before computed value) can produce NaN
: calc(min(1em, 0px / 0) + 1px)
would serialize to calc(1px + min(1em, NaN * 1px))
as a specified value.
If a top-level calculation (a math function not nested inside of another math function) would produce a value whose numeric part is
NaN
, it instead act as though the numeric part is0
.
https://drafts.csswg.org/css-values-4/#top-level-calculation
I think your tests should serialize Infinity
to lowercase.
As usual for CSS keywords, these are ASCII case-insensitive. Thus,
calc(InFiNiTy)
is perfectly valid. However,NaN
must be serialized with this canonical casing.
https://drafts.csswg.org/css-values-4/#calc-error-constants
Hope it helps. =)
Thank you @cdoublev
I've changed the tests to lowercase infinity
, this indeed matches tests under css-values
.
The behavior of NaN is specified in https://drafts.csswg.org/css-values/#top-level-calculation - it gets censored to 0 when it would escape the calculation tree. It serializes as NaN
, tho, similar to infinity, at whatever level it's been able to infect to, given the simplification rules and what value stage you're serializing.
For lab(calc(NaN) 0 0)
or lab(calc(0 / 0) 0 0)
:
lab(0 0 0)
lab(calc(NaN) 0 0)
Would that be correct?
It serializes as
NaN
[...] at whatever level it's been able to infect to
Thanks for the clarification. My understanding of the spec was that it only serializes as NaN
in non-top level calculations (or NaN * 1 unit
when the calculation type is not empty).
Would that be correct?
I am not in the position to give an answer but it would say yes, then.
@romainmenke yes
@cdoublev What part of the spec is leading you to believe that a top-level NaN doesn't serialize as such? I might have made a mistake.
I've added some test for NaN
: https://github.com/web-platform-tests/wpt/pull/39137/commits/789c766a0f30486e6f77f869d08f495b1240707b
What part of the spec is leading you to believe that a top-level
NaN
doesn't serialize as such? I might have made a mistake.
The part that we both referred to (edited for clarity):
- If a top-level calculation would produce a value whose numeric part is
NaN
, it instead act as though the numeric part is0
.- If a top-level calculation would produce a value whose numeric part is
0⁻
, it instead acts as though the numeric part is the standard "unsigned" zero.
Taking Example 38 following this part:
calc(-5 * 0)
produces an unsigned zero — the calculation resolves to0⁻
, but as it’s a top-level calculation, it’s then censored to an unsigned zero.
calc(0 / 0)
produces 0
— the calculation resolves to NaN
, but as it’s a top-level calculation, it’s then censored to zero.
Basically my understanding meant that NaN
could only appear in the serialization of unresolved nested calculations.
I am also missing why Chrome currently serializes the next example to calc(infinity)
instead of calc(-infinity)
.
On the other hand,
calc(1 / calc(-5 * 0))
produces−∞
[...] the inner calc resolves to0⁻
, and as it’s not a top-level calculation, it passes it up unchanged to the outer calc to produce−∞
The part that we both referred to (edited for clarity):
That's about how the value actually acts in the property it's used in, when the final result is a NaN. The simplification and serialization of the calculation itself is well-defined by the simplification and serialization algos, and they preserve NaNs at whatever level they show up in.
I am also missing why Chrome currently serializes the next example
Looks like a bug.
The tests were merged : https://github.com/web-platform-tests/wpt/pull/39137
That only leaves the open question of how to handle enormous values :
Rendering is just a consequence of the "clamp it to your actual allowed range" behavior that V&U specifies for infinite (and other arbitrarily-large finite) values. I'm not sure what the actual behavior of a ginormous a value should be, but whatever it is, that's the correct rendering. (I suspect white? But I have no actual knowledge here.)
I suspect the right value will just fall out of implementations that are free of bugs but maybe good to have a few examples/tests?
@svgeesus Do you know or have an opinion on how something like lab(50% calc(Infinity) 0)
which has an absurdly large a
should behave?
Anyone writing lab(50% calc(Infinity) 0)
manually should expect bogus results, but infinity
can be the result of a calculation.
@svgeesus Do you know or have an opinion on how something like
lab(50% calc(Infinity) 0)
which has an absurdly largea
should behave?
All colors of the form lab(50% calc((var(--n)*100) 0)
where n
> 0 have the same (LCH) hue, and increasing chroma. Thus, even if the color itself is imaginary, the gamut mapped used value of the color should be lab(50% x 0)
where x
is the greatest chroma that can be displayed for that hue and lightness. I would apply the same logic to +Infinity (and -Infinity gives the opposite hue).
Where b
is not zero, as a
increases the contribution of b
on the resulting hue decreases, so as a
tends to infinity the hue tends towards b
being effectively zero. So I guess infinite values of either a
or b
make the other component powerless? And thus there are eight effective hues that the color tends towards, depending on the sign of the component and whether only one, or both, have Infinity values?
And thus there are eight effective hues that the color tends towards, depending on the sign of the component and whether only one, or both, have Infinity values?
This is specific to lab
and oklab
right?
For rgb
or xyz
color spaces an increase in value for any channel also has an effect on lightness, so any enormous value forces the color to black or white depending on the sign.
Interesting, not what I would have guessed but it makes a lot of sense!
This is specific to lab and oklab right?
Yes.
For rgb or xyz color spaces an increase in value for any channel also has an effect on lightness, so any enormous value forces the color to black or white depending on the sign.
Yes (Negative values in XYZ are not supposed to occur, although they are possible as a result of chromatic adaptation; large negative ones should be treated as black I guess).
For
lab(calc(NaN) 0 0)
orlab(calc(0 / 0) 0 0)
:
- computed value
lab(0 0 0)
- serializes as
lab(calc(NaN) 0 0)
Would that be correct?
@tabatkins Even if this is correct, per spec, why do we want this behavior? Does it have significant use-cases? Do other CSS properties perform this way? The reason I ask is because this would invalidate a couple major assumptions that chromium's color parser makes. Namely that the result of parsing a color function will either be failure or a Color
, i.e. something that can be encoded with a colorspace and 4 values, which are numbers or "None."
Tests verifying this are part of interop 2023 and all UAs are currently failing:
https://wpt.fyi/results/css/css-color/parsing/color-valid-lab.html
To pass these tests will involve restructuring things quite a bit and creating a sort of "unresolved color" type so that the color can be serialized differently at parse time than at computed time. Obviously, none of this is impossible, but I simply wanted to highlight that it is a non-trivial amount of work to support. If it's a minor use-case and other UAs are uninterested in supporting it, than I'd propose changing the spec such that:
For lab(calc(NaN) 0 0)
or lab(calc(0 / 0) 0 0)
:
lab(0 0 0)
lab(0 0 0)
And for lab(0 calc(infinity) 0)
:
lab(0 Infinity 0)
serializes as lab(0 Infinity 0)
So, importantly, dropping the "calc" so that the channels are all numbers. Why is it important that they roundtrip in this context?
So, importantly, dropping the "calc" so that the channels are all numbers. Why is it important that they roundtrip in this context?
Part of the reason for initially opening this issue is that dropping calc
"breaks" the value.
lab(0 Infinity 0)
-> invalidlab(0 calc(Infinity) 0)
-> something very pink-ish in ChromeSerializing into an invalid value seems like a bug to me.
But maybe lab(0 calc(Infinity) 0)
should be invalid, if lab(0 Infinity 0)
is invalid? Adding a calc
to a single term to make something valid seems like a bug to me.
Adding a calc to a single term to make something valid seems like a bug to me.
This is not to make something valid, this is done to express NaN
or Infinity
: https://drafts.csswg.org/css-values-4/#calc-serialize
There will be other examples that don't have anything to do with color :)
Thanks! I understand that now. So color(srgb 0 calc(Infinity) 0)
represents a color with an infinite green color channel, whereas color(srgb 0 Infinity 0)
is literally an invalid string to represent a color?
This makes sense. Now my next question would be, why are we forcing valid colors out of these inputs? Wouldn't the least surprising outcome for the user be to reject them entirely as colors?
@svgeesus Apologies if there was already a debate about this that I missed out on, but from what I currently understand I think it would be best if these were all invalid colors.
I think it would be best if these were all invalid colors
I certainly can't think of an actual use case for such colors.
Is this something already happens in other values, functions, ...?
All things that I am familiar with do not become invalid when out of range.
How would this affect @supports
, fallbacks, ... ?
There's no meaningful difference between calc(infinity)
and calc(1e6)
. If the number has an enforced range at parse time, they'll both act as the clamped value; if it's unclamped, they'll both be essentially identical in behavior anyway. We shouldn't be treating the two cases differently.
All things that I am familiar with do not become invalid when out of range.
A number of things have ranges that are checked at parse-time. They'll be invalid if you use an out-of-range value by itself, but math functions change that behavior - see https://drafts.csswg.org/css-values/#calc-range. Instead, if a value is the result of a math function, it's treated as valid at parse time, and clamped at computed and/or used-value time to the allowed range. For example, width: -1px
is invalid, but width: calc(-1px);
is valid and equivalent to width: 0;
. We do this because it's not always possible to tell how large a value is going to be, and even whether it'll be positive or negative, until computed or used value time. (For example, calc(16px - 1em)
can be positive, negative, or zero depending on the size of the em
, which isn't known until computed-value time.)
This is why the serialization algorithm has a branch for computed or later values in the first step; specified values have to preserve the fact that the value is in a math function, in case the value is outside the allowed range, so you can round-trip the value.
To pass these tests will involve restructuring things quite a bit and creating a sort of "unresolved color" type so that the color can be serialized differently at parse time than at computed time.
This is required anyway to handle cases like a color-mix()
or relative color referring to currentcolor
, which also isn't known until computed-value time.
If we decide these are valid colors, then as far as I understand it:
NaN
are serialized as calc(NaN)
, and infinity is serialized as calc(infinity)
or calc(-infinity)
NaN
s become zero. Infinity remains infinity unless it expressing a clamped value, in which case it becomes the maximum possible value in that range. Negative infinity does the same, going to the minimum possible value.Is that, in general, correct? If so, I'll start adding more test cases to wpt.
At parse time:
More specifically, all math functions serialize as some variety of math function when they're specified values, regardless of what's inside them. If they've been able to simplify down to a single value, they'll serialize with a calc()
around them. (Browsers have, generally, not done this correctly in many places, colors being a notable example.)
At computed value time:
Yup, correct. (And note, just for completeness, that the infinity
keywords have to still be serialized with a calc()
wrapper, because they're only valid as keywords inside a calculation.)
What about for rgb()
/rgba()
values? I know that there was some discussion here stating that rgb()
was de-facto bounded.
Currently, Firefox does not parse non-finite inputs. Chromium and Safari do the following:
input: rgb(calc(infinity), 0, 0)
parsed: rgb(255, 0, 0)
computed: rgb(255, 0, 0)
Chrome and Safari also do this, which seems like definitely a bug:
input: rgb(calc(NaN), 0, 0)
parsed: rgb(255, 0, 0)
computed: rgb(255, 0, 0)
Should the proper behavior be:
input: rgb(calc(infinity), 0, 0)
parsed: rgb(calc(infinity), 0, 0)
computed: rgb(255, 0, 0)
and
input: rgb(calc(NaN), 0, 0)
parsed: rgb(calc(NaN, 0, 0)
computed: rgb(0, 0, 0)
With -infinity going to zero as well? Also, what about for alpha channels, I would assume the following:
input: rgba(0, 0, 0, calc(NaN))
parsed: rgba(0, 0, 0, calc(NaN))
computed: rgba(0, 0, 0, 0)
input: rgba(0, 0, 0, calc(infinity))
parsed: rgba(0, 0, 0, calc(infinity))
computed: rgb(0, 0, 0)
(fully opaque)
input: rgba(0, 0, 0, calc(-infinity))
parsed: rgba(0, 0, 0, calc(-infinity))
computed: rgba(0, 0, 0, 0)
Safari and Chrome turn calc(NaN)
into fully opaque, at parse time.
For HSL/HWB I would assume that all channels are unbounded: infinity remains infinity and NaN
becomes 0 at computed value time.
If I'm correct in all this I'll add some tests to WPT and to interop 2023.
The CSS Working Group just discussed [css-color-4] Computed value and serialization of `Infinity` and `NaN` in color functions
, and agreed to the following:
RESOLVED: No (further) change
This comment above says...
[...] the "clamp it to your actual allowed range" behavior that V&U specifies for infinite (and other arbitrarily-large finite) values.
... like this code comment on WPT:
calc(infinity)
resolves to the upper bound whilecalc(-infinity)
andcalc(NaN)
resolves the lower bound.
But this is not what I understand from what CSS Values and Units:
[...] the value resulting from a top-level calculation must be clamped to the range allowed in the target context.
Out of range channel values are allowed in all color functions.
Therefore I think it would be great to clarify when and how calc(infinity)
resolves when serializing (non-mixed) hsl()
and hwb()
as components of a declared value, ie. either before or after color space conversion.
Re-opening because arguing on the basis of one legacy function rgba()
which happens to be clamped, was not sufficient.
What are the expected values and serializations for these examples?
lab(calc(Infinity) 0 0)
lab(50% calc(Infinity) 0)
lab(calc(NaN) 0 0)
lab(50% calc(NaN) 0)
manually setting these values seems illogical but they can be the result of calculations and variables. e.g.
calc(var(--foo) / var(--bar))
where--bar
is possibly zero.Currently browsers do very different things in some area's.
Chrome and Safari seem to agree that
lab(50% calc(Infinity) 0)
serializes aslab(50% Infinity 0)
, but that no longer roundtrips because the requiredcalc
was removed. That seems like an obvious bug, but maybe I overlooked something.Chrome renders
lab(50% calc(Infinity) 0)
as white. Safari renderslab(50% calc(Infinity) 0)
as black.Chrome also sometimes renders a pink area in certain edge cases
WPT doesn't have any tests for these kinds of values.