Re: [csswg-drafts] [css-color-4] Channel clipping breaks author expectations, especially when using 'perceptually uniform' spaces (#9449)

I’ve just read all of this thread, and I have _many_ things to say.
But first, a new idea.

## A new O(1) gamut mapping algorithm: Scale LH

Performance has been cited as one of the reasons to go with naive clipping. Presumably, that has a lot to do with the fact that the CSS gamut mapping algorithm is iterative, and takes an unknown number of steps to converge.

So, @svgeesus and I spent some time fleshing out an idea I had for an O(1) gamut mapping algorithm (O(1) in the sense that it does not require an indeterminate number of steps to converge).

My original goal was to produce something fast that would be a better substitute than clipping, and fix the immediate pain point while we reach a better solution. However, preliminary results look like it produces roughly equivalent (or only slightly worse), and often even _better_ results than the CSS gamut mapping algorithm. More specifically, it seems to do significantly better at preserving hues, and slightly worse at preserving lightness.

I’ve made an interactive playground where you can compare the different gamut mapping algorithms (including this new one) here: https://colorjs.io/apps/gamut-mapping/

It assumes a P3 target gamut, but could be easily customized to support sRGB too or any other bounded RGB space.

The following screenshots are from it.

<img width="1419" alt="image" src="https://github.com/w3c/csswg-drafts/assets/175836/4d701708-ffc2-4527-b099-fbc32cc59c79">
<img width="1412" alt="image" src="https://github.com/w3c/csswg-drafts/assets/175836/7975cc49-f1bd-485b-b597-fbe98a75bafe">
<img width="1409" alt="image" src="https://github.com/w3c/csswg-drafts/assets/175836/044d1289-7ef3-44d7-8c40-828a11971131">
<img width="1411" alt="image" src="https://github.com/w3c/csswg-drafts/assets/175836/ca38ad02-bce5-4e08-9a25-d35622530527">
<img width="1433" alt="image" src="https://github.com/w3c/csswg-drafts/assets/175836/b3e88fdb-c68d-4f07-b2a2-6c90e632c918">
<img width="1407" alt="image" src="https://github.com/w3c/csswg-drafts/assets/175836/cae0b73d-a718-48ef-8d09-2c45efb82587">


As a side effect, by playing with the swatch you can see how easy it is to get out of gamut (P3+ w/ orange background is Rec.2020, P3+ w/ red background is outside even of Rec.2020).

### The Scale LH algorithm

1. Let mappedColor = Scale(color)
2. Set the lightness and chroma of the mappedColor equal to the lightness and chroma of the color
4. Let Result = Scale(mappedColor)

Where Scale(color) scales a color’s components to the 0 .. 1 range by bringing components closer to 0.5 in a linear RGB space. Steps for that:

1. Let linearColor = Convert color to linear RGB version of target color space
2. Let deltas = Subtract 0.5 from each linearColor coordinate
3. Let maxDistance = Maximum absolute delta
4. Let scalingFactor = maxDistance / 0.5
5. Let scaledCoords = Deltas divided by scalingFactor + 0.5
6. Let Result = Linear RGB color with scaledCoords converted to gamma-encoded RGB

For those who find code easier, it's actually pretty readable: 
- [Scale LH](https://github.com/color-js/color.js/blob/main/apps/gamut-mapping/methods.js#L15)
- [Scale](https://github.com/color-js/color.js/blob/main/apps/gamut-mapping/methods.js#L28)

Another advantage of this algorithm is that `Scale` even by itself _already_ produces far better results than `Clip`, and the LH + second Scale step refines them further, so it could even be done in two passes in cases where that is desirable.

-- 
GitHub Notification of comment by LeaVerou
Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/9449#issuecomment-1943282824 using your GitHub account


-- 
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config

Received on Wednesday, 14 February 2024 08:28:22 UTC