w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.42k stars 652 forks source link

[css-color-4] How to handle infinite values in color functions? #10507

Open cdoublev opened 2 months ago

cdoublev commented 2 months ago

Unless otherwise specified, color channel values are not clamped, which applies to values produced by math functions, which can be infinite.

Infinite color channel values produce NaN in most conversion functions.

Specifically... - `color(srgb-linear) -> color(xyz-d65)` - when one or more channels are infinite and one or more are the opposite infinite - `color(xyz-d50) -> color(xyz-d65)` - `color(xyz-d65) -> color(srgb-linear)` - when `x` and `y` are the same infinite - when `x`/`y` and `z` are infinite - `color(xyz-d50) -> lab()` - when `x` and `y` are the same infinite - when `y` and `z` are the same infinite - `color(xyz-d65) -> color(xyz-d50)` - when `x` and `y` are infinite - when `y` and `z` are infinite - when `x` and `z` are the same infinite - `color(xyz-d65) -> oklab()` - `oklab() -> color(xyz-d65)` - `rgb() -> hsl()` - when one or more channel is infinite - `hsl() -> rgb()` - when `s` is infinite and `l` is 0/100 - when `l` is infinite - `hwb() -> rgb()` - when `w` is infinite - when `b` is -Infinity - `lab() -> color(xyz-d50)` - when `l` is infinite and `a` is the opposite infinite - when `l` and `b` are the same infinite - `lch() -> lab()` - `oklch() -> oklab()` - when `c` is infinite and `h` is 0 This [CodePen](https://codepen.io/creativewave/pen/eYaKyVX) allows to play with conversion functions, which have comments for `NaN` cases.

In colorjs.io@0.5.1, NaN seems to be replaced by none, which seems incorrect:

let color = new Color('hsl', [0, 0, Infinity])
color.to('srgb').toString(); // rgb(none none none)

Clarification on how to handle infinite values has already been asked in #8629, which was resolved with no further change because if you put an infinite calculation into an rgba(), the behavior is well-defined: clamp to the allowed range.

At least, this is not true for some channels of other color functions than rgb(), and for relative colors.

Following this comment, I tried to guess the color resulting from a color function specified with one infinite channel value. Since I am personally only interested by serialized values, I only did it for hsl() -> rgb() and hwb() -> rgb(), but it should presumably be done for all conversions producing NaN.

When converting hsl() to rgb() and saturation is +Infinity or -Infinity: [...] - when `l === 0`, `r`, `g`, `b`, are `0` - when `l < 0`, same as when `l > 0` but with inverted infinite signs - when `l > 0`: | `h` | `r` | `g` | `b` | --------------- | -------------- | -------------- | -------------- | `330 < h < 30` | `+Infinity` | `-Infinity` | `-Infinity` | `h === 30` | `+Infinity` | depends on `l` | `-Infinity` | `30 < h < 90` | `+Infinity` | `+Infinity` | `-Infinity` | `h === 90` | depends on `l` | `+Infinity` | `-Infinity` | `90 < h < 150` | `-Infinity` | `+Infinity` | `-Infinity` | `h === 150` | `-Infinity` | `+Infinity` | depends on `l` | `150 < h < 210` | `-Infinity` | `+Infinity` | `+Infinity` | `h === 210` | `-Infinity` | depends on `l` | `+Infinity` | `210 < h < 270` | `-Infinity` | `-Infinity` | `+Infinity` | `h === 270` | depends on `l` | `-Infinity` | `+Infinity` | `270 < h < 330` | `+Infinity` | `-Infinity` | `+Infinity` | `h === 330` | `+Infinity` | `-Infinity` | depends on `l` When converting `hsl()` to `rgb()` and `l === +Infinity`: - when `-100 <= saturation <= 100`, `r`, `g`, `b`, are `+Infinity` - when `saturation < -100`, same as `saturation > 100` but with inverted infinite signs - when `saturation > 100`, `r`, `g`, `b`, are `+Infinity`/`-Infinity` depending on `saturation` and `h` - there are 6 intervals defined by `h` - the exact `h` interval values depend on `saturation` - for each sibling intervals, one of `r`/`g`/`b` has a different sign When converting `hsl()` to `rgb()` and `l === -Infinity`: - when `-100 <= saturation <= 100`, `r`, `g`, `b`, are `-Infinity` - when `saturation < -100` or `saturation > 100`, same as when `lightness === +Infinity` but with different intervals

Let's skip hwb() -> rgb() because it becomes more complicated when more than one channel value is infinite.

For example (lab() -> color(xyz)), when a === 0, z tends towards +Infinity when l tends towards +Infinity, but z tends towards -Infinity when b also tends towards +Infinity. Should z be 0 when l and b are +Infinity? Should some "precedence" between channel values be defined (eg. l over a and b)?

Note that channel values do not always tend linearly towards an infinite value:

Am I missing something?

svgeesus commented 2 months ago

I agree that coercing values to infinity is going to cause no end of problems with color math; most color spaces are not designed to cope with such extreme values.

@tabatkins why are we doing this, again?

Clarification on how to handle infinite values has already been asked in #8629, which was resolved with no further change because if you put an infinite calculation into an rgba(), the behavior is well-defined: clamp to the allowed range.

For color spaces are clamped, yes. Most are not, including color(srgb ...) and oklab() and so on.

I just re-opened #8629 because the WG discussion apparently consisted of @tabatkins asserting that it was fine (for rgba()) and thus fine everywhere else.

romainmenke commented 2 months ago

Not sure if this is applicable to browser implementations but I found that clamping to a really large value gives good results : https://github.com/csstools/postcss-plugins/blob/20ccaebde3de250e858adbbd3d519b01b7cf5304/packages/css-color-parser/src/functions/hsl-normalize-channel-values.ts#L111

Having a really large but finite value makes the color math work as expected.