w3c / csswg-drafts

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

[css-colors-4] Clip negative whiteness/blackness of `<hwb()>`? #10368

Open cdoublev opened 1 month ago

cdoublev commented 1 month ago

Browsers serialize hwb(0 -10 0) with rgb(255, 0, 0). The conversion from hwb to rgb returns [1, -0.1, -0.1].

Where is clipping now specified in the specification?

I presume it should still apply otherwise resolving infinity to 255 after the conversion to rgb() would not make sense. Should it apply before or after the conversion to rgb()? Should it only apply when it is not mixed with another color?

Whiteness/blackness was clipped:

Note: I asked a similar question for saturation > 100 in hwb() but the issue is closed.

cdoublev commented 3 weeks ago

Converting HWB to sRGB produces NaN when whiteness is -Infinity or +Infinity (produced by a math function) or blackness is -Infinity.

For example, the current version of Chrome (126) serializes rgb(NaN, NaN, NaN) from hwb(0 calc(-infinity) 0). FF clamps whiteness and blackness before the conversion.

Serializing hwb() requires converting to rgb(), therefore resolving math functions (extracting their result) before, as noted in 14.1. Resolving sRGB values:

For historical reasons, when calc() in sRGB colors resolves to a single value, the declared value serialises without the "calc(" ")" wrapper.

Note: calc() should be replaced with a math function.

CSS Color 4 then says:

Also for historical reasons, when calc() is simplified down to a single value, the color values are clamped to [0.0, 255.0].

But it does not require clamping hwb() channel values before conversion to rgb().

I do not think NaN cases can be specifically handled in the conversion algorithm.

When whiteness/blackness is < 0 or > 100, the produced color is different depending on which of whiteness/blackness (line in FF) or RGB (like in Chrome) values are clamped:

  // whiteness < 0
  $el1.style.color = 'hwb(90 -200 0)'
  $el2.style.color = 'hwb(90 -100 0)'
  $el3.style.color = 'hwb(90  -50 0)'
  $el4.style.color = 'hwb(90    0 0)'
  getComputedStyle($el1).color; // rgb(128, 255, 0) in FF, rgb(0, 255, 0) in Chrome
  getComputedStyle($el2).color; // rgb(128, 255, 0) in FF, rgb(0, 255, 0) in Chrome
  getComputedStyle($el3).color; // rgb(128, 255, 0) in FF, rgb(64, 255, 0) in Chrome
  getComputedStyle($el4).color; // rgb(128, 255, 0) in FF and Chrome

  // whiteness > 100
  $el5.style.color = 'hwb(0 100 50)'
  $el6.style.color = 'hwb(0 200 50)'
  getComputedStyle($el5).color; // rgb(170, 170, 170) in FF and Chrome
  getComputedStyle($el6).color; // rgb(170, 170, 170) in FF, rgb(204, 204, 204) in Chrome

When whiteness/blackness is < 0 and not clamped, the hue is shifted: hwb(90 -100 0) becomes hwb(120 0 0).

When whiteness/blackness is > 100 and not clamped, the lightness is shifted: hwb(0 200 50) becomes hwb(0 100 25).

Finally, when (whiteness + blackness) > 1, I am not sure I understand the current logic. If the algorithm should match the result of mixing paints, I do not understand why the produced color is not white when (whiteness - blackness) > 100, or black when (blackness - whiteness) > 100. whiteness + blackness should probably not be greater than 100.


I am not sure I understand the reasons, the conditions, the timing, for clamping out of range values.

HSL is off topic but similar observations can be made:

Currently, channel values that must be clamped are:

They must be clamped at parse time. They must not be clamped in a relative or origin color (and possibly in color-mix(), #10414).

For RGB, CSS Color 3 optionally requires gamut mapping, without defining how or when. CSS Color 4 required clamping at computed value time before.

For saturation, CSS Color 4 follows CSS Color 3, which requires clamping, without defining when, and lets UAs optionally gamut mapping other channel values (like RGB). CSS Color 4 required clamping saturation and lightness at computed value time before.

For chromaticity and lightness of ok?lab() and ok?lch(), CSS Color 4 required clamping at computed value time before.

If the reason for clamping RGB and saturation is backward compatibility, what are the reasons for clamping chromaticity and lightness of ok?lab() and ok?lch()?