csswg-drafts
csswg-drafts copied to clipboard
[css-color-5] Clarification on how `channel keywords` with multiple specified types work
In the relative color syntax section of CSS Color 5, some of the channel keywords
specify more than one type. For instance, the channel color l
for the lab()
relative color syntax states:
l
is a<percentage>
or<number>
that corresponds to the origin color’s CIE Lightness
(http://w3c.github.io/csswg-drafts/css-color-5/##valdef-lab-l)
I am unclear on what this means with respect to using l
in a calc()
expression. For instance, in the following contrived case:
lab(from purple calc(l + 2) a b / 100%)
should I be interpreting l
as a <percentage>
or a <number>
? (or am I missing something fundamental about calc that makes this not an issue?)
Edit: linked to stable drafts, and corrected to CSS Color 5, not 4
I think this is a case of the editors not appreciating the impact of other changes. At the time it was introduced to CSS, CIE L was a <number>
in [0..100]. Then it became a mandatory <percentage>
for a long time, and relatively recently became <percentage>
| <number>
. You are right that this means RCS no longer defines what you actually calculate with for calc()
inside RCS.
Following the advice that @LeaVerou gives in her CSS Variables talks:
converting a number to a value with units is easy,
calc(var(--foo) * 100%)
converting a value with units to a number requires things likecalc(var(--foo) / 1%)
which don't exist yet so make your custom properties be pure data with no units
Then I suggest resolving this such that <percentage>
gets resolved to <number>
because that will be the most convenient thing to manipulate in calc()
So
--base: oklab(50% 0.2 -12%);
--darker: oklab(from var(--base) calc(l * 0.8) a b);
Using the percent reference ranges for oklab the value of --base becomes oklab(0.5 0.2 -0.048) and the value of --darker becomes
oklab(0.4 0.2 -0.048)'
Converting the <percentage>
values to <number>
is also consistent with how those values are serialized
I think this is a case of the editors not appreciating the impact of other changes. At the time it was introduced to CSS, CIE L was a
<number>
in [0..100]. Then it became a mandatory<percentage>
for a long time, and relatively recently became<percentage>
|<number>
. You are right that this means RCS no longer defines what you actually calculate with forcalc()
inside RCS.
Yeah, this appears to have been some sort of mass change.
Following the advice that @LeaVerou gives in her CSS Variables talks:
converting a number to a value with units is easy,
calc(var(--foo) * 100%)
converting a value with units to a number requires things likecalc(var(--foo) / 1%)
which don't exist yet so make your custom properties be pure data with no unitsThen I suggest resolving this such that
<percentage>
gets resolved to<number>
because that will be the most convenient thing to manipulate incalc()
As a general principle, we should not be designing the language long-term based on short-term UA limitations.
However, I don't see anything wrong with erring on <number>
whenever possible, to limit the number of conversions needed. That could be a general principle for RCS.
Another point is that forcing users to convert from percentage form themselves requires them to memorize or look up the percentage reference ranges for each component. 100% maps to 100 for CIE L, 1.0 for Oklab L, 125 for CIE a, 0.4 for Oklab a. Having the implementation do this will reduce errors.
Another point is that forcing users to convert from percentage form themselves requires them to memorize or look up the percentage reference ranges for each component. 100% maps to 100 for CIE L, 1.0 for Oklab L, 125 for CIE a, 0.4 for Oklab a. Having the implementation do this will reduce errors.
Correct, though it may make operations easier, e.g. calc(l + 20%)
becomes more meaningful across color spaces.
calc(l * 1.2)
seems easy enough
Yes, but they do different things. Both are useful.
Perhaps we can make it <number-percentage>
so that math with percentages still works? @tabatkins is that a thing? Can it be?
There is no more <number-percentage>
, it was removed.
Yeah, number-percentage doesn't exist (and can't - it breaks the typing system). We do need to decide on one or the other.
I'm fine with making it number.
So:
- @tabatkins is fine with
<number>
, - @LeaVerou wanted
<number-percentage>
which isn't possible, but earlier said<number>
was fine, and - I argued for
<number>
because if the implementation resolves percentages, that is less error prone than the user doing it.
No-one has argued for <percentage>
only, and it is agreed that the channel keywords need to resolve to a single type.
In the interest of having concrete text to discuss on the call today, I am going to do the edits to make all channel keywords resolve to <number>
.
For clarity, this only applies to channel keywords which are currently specified as multiple types, like <percentage>
or <number>
, as reported by @weinig.
Channel keywords which use other (single) types, like <angle>
for hues or <percentage>
for the saturation and lightness of HSL, are not affected and they don't change to <number>
.
Ok so now:
- all channel keywords specify a single type
- examples updated to consistently use that type
- added an example of resolving specified percentage values to numbers
In particular, I added an explicit:
Except as specified for individual color functions, (for example, hues return an
<angle>
), the channel keywords return a<number>
; if they were originally specified as a<percentage>
, that percentage is resolved to a<number>
.
Channel keywords which use other (single) types, like
<angle>
for hues
Erm, hue is <number> | <angle>
(and <angle>
isn't actually used much at all in the wild)
Again, this is not about what types are valid for input, but rather what type the channel keywords are as output - those have to be a single known type or else the type of a calculation is completely undefined.
@svgeesus in hwb() you're still specifying the w and b keywords as percentages. Otherwise the edits look good.
Again, this is not about what types are valid for input, but rather what type the channel keywords are as output - those have to be a single known type or else the type of a calculation is completely undefined.
I’m aware, but the rule we are discussing is "RCS keywords return <number>
if <number>
is one of the accepted input types for the component".
in hwb() you're still specifying the w and b keywords as percentages.
Yes, because in CSS Color 4
The syntax of the hwb() function is:
hwb() = hwb( [
<hue>
| none] [<percentage>
| none] [<percentage>
| none] [ / [<alpha-value>
| none] ]?
and
The second argument specifies the amount of white to mix in, as a percentage from 0% (no whiteness) to 100% (full whiteness). Similarly, the third argument specifies the amount of black to mix in, also from 0% (no blackness) to 100% (full blackness).
So percentage is required and number is not allowed. Should that be changed? So numbers are not accepted, only percentages. Same as the non-hue components in HSL.
The CSS Working Group just discussed channel keywords
, and agreed to the following:
-
RESOLVED: keywords with multiple specified types result in number
The full IRC log of that discussion
<fantasai> Topic: channel keywords<fantasai> github: https://github.com/w3c/csswg-drafts/issues/7876
<fantasai> chris: main issue raised was that the keywords could have two possible types, doesn't work
<fantasai> chris: either number or percentage
<fantasai> chris: went with number to be consistent with serialization
<fantasai> TabAtkins: nit, you accidentaly did percentage for wmb, but otherwise it's great
<fantasai> chris: It's because color-4 it only takes a percentage
<fantasai> TabAtkins: Ah, in that case it's completely fine
<fantasai> chris: any other comments?
<fantasai> chris: anyone need more time?
<fantasai> ntim: didn't have a chance to look at it
<fantasai> lea: does that mean for rgb they resolve to 0-255 range?
<fantasai> chris: yes, but remember it's 0.0 to 255.0 so you don't lose precision
<fantasai> lea: but inconsistent with rgb models
<fantasai> chris: because it was invented poorly
<fantasai> lea: I agree but does that mean we don't need relative color syntax for rgb?
<Rossen_> q?
<fantasai> chris: I have a lot of trouble coming up with use cases
<Rossen_> ack ntim
<fantasai> chris: I haven't found a good example
<fantasai> lea: I think it's primarily for ocmpletenes, but maybe we should not do it just for completeness
<fantasai> lea: restrict to color()?
<fantasai> chris: I wouldn't go that far
<fantasai> chris: let's resolve this and deal in other issues?
<fantasai> chris: get consensus on going to number?
<fantasai> Rossen_: So do we have enough consensus?
<lea> s/ocmpletenes, but maybe we should not do it/completeness, but we don't generally do things/
<fantasai> lea: consensus is about every component that's "number or something else" resolves to number?
<fantasai> lea: so hues resolve to numbers?
<fantasai> chris: yeah, all examples treat hues as number
<fantasai> chris: so I think most ppl are treating as numbers
<fantasai> lea: yes, that's what authors do
<fantasai> [...]
<argyle> +1 to number
<fantasai> chris: Proposal, keywords with multiple specified types result in number
<fantasai> Rossen_: Any additional feedback or objections?
<fantasai> jensimmons: this is fine with us from Apple
<fantasai> RESOLVED: keywords with multiple specified types result in number
@tabatkins @svgeesus Maybe this issue should be re-opened. The WPT tests were never updated to match the spec change and Safari shipped relative color syntax in 16.4 based on an old version of the specification.
This is an unfortunate breaking change for folks using pre-processors that already support this syntax. It means that something like lch(from indianred calc(l + 10%) c h)
isn't possible anymore. You have to write it as lch(from indianred calc(l + 10) c h)
which seems less intuitive, especially since you can normally write the l
channel as a percentage (e.g. lch(63.9252% 51.2776 26.8448)
).
It's even less intuitive depending on the channel, since each one has a different percentage basis. For example, with oklch
you'd have to do calc(l + .1)
instead of calc(l + 10)
for lch because the number range is [0, 1] instead of [0, 100]. This makes it harder to intuitively "lighten a color by 10%".
Why can't we have it be a number or percentage depending on which one type checks? That's how I had implemented it in Lightning CSS - it first tries the calculation where l
is a number, and if that fails, tries it as a percentage.
Why can't we have it be a number or percentage depending on which one type checks?
CSS used to have a <number-or-percentage>
type but it no longer exists. @tabatkins said that it can't and could explain better than I why that had to be removed.
For example, with
oklch
you'd have to docalc(l + .1)
instead ofcalc(l + 10)
for lch because the number range is [0, 1] instead of [0, 100].
For 10% lighter I would probably do calc(l * 1.1)
in both cases.
But I do see your point, and sorry for the breaking change.
Another question about this – WPT has this test: https://github.com/web-platform-tests/wpt/blob/7c0ad5dcce2aa7a073b6f09b99213325b0c4574a/css/css-color/parsing/color-computed-relative-color.html#L90
test_computed_value(`color`, `rgb(from rebeccapurple b alpha r / g)`, `rgba(153, 255, 102, 0.2)`);
This implies either that alpha
values are stored in 0-255 range rather than 0-1 as specified by <alpha-value>
, or that they are somehow converted. Here the alpha becomes the green channel (1 becomes 255), and the green channel becomes the alpha channel (51 becomes 0.2 = 51 / 255).
Is this the intentional behavior or should the output actually be rgba(153, 1, 102, 1)
?
The WPT tests were never updated after the changes from mixed types to percentages or numbers.
should the output actually be
rgba(153, 1, 102, 1)
That should be correct
This seems quite unintuitive vs treating all channels consistently as percentages, because you have to account for different ranges yourself in any calculations. The behavior of something like lch(from lch(70% 45 30) alpha c h / l)
and oklch(from oklch(70% 45 30) alpha c h / l)
is very different. With lch
, the l
channel becomes 1%
, but with oklch
it is 100%
due to the different ranges. Treating channels as percentages in calculations would make this a lot more consistent.
Also given that Safari 16.4 already shipped, and matches WPT as it is now, would it not be a breaking change for them also to switch to this new interpretation?
I don't know why <number>
was favored over <percentage>
but I also find it very hard to work with.
Components as specified can have these types in RCS :
-
<number>
-
<percentage>
-
<angle>
It is much harder to effectively write calc
expressions for components because of this.
If <percentage>
was favored it could be limited to :
-
<percentage>
(everything nothue
) -
<angle>
(onlyhue
)
even hue
could theoretically be mapped from percentage to degrees [0% 100%]
-> [0deg 360deg]
It would also be much more intuitive because + 10%
is "a little bit more" in any notation and for any component. Whereas + 1
could be out of gamut or barely noticeable.
A downside of percentages is that it limits the possible operations.
For example multiplying channels by alpha becomes impossible because percentages can not be multiplied.
I think that what I actually want is to be able to use numbers but have all number ranges have the same scales, which is just not how the different number notations work.
also multiplying by 1.1
and adding 10%
are different things.
lch(from slateblue calc(l * 1.1) c h) => lch(49.0282% 65.7776 296.794)
lch(from slateblue calc(l + 10) c h) => lch(54.5711% 65.7776 296.794)
The first is 110% of the original l
value. The second adds 10% of the overall range of the l
channel.
The WPT tests were never updated after the changes from mixed types to percentages or numbers.
They have now all been updated.