w3c / csswg-drafts

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

[css-color] Add OKLab, OKLCH #6642

Closed svgeesus closed 2 years ago

svgeesus commented 2 years ago

For the background and explanation of why to add this feature, please see this Color Workshop presentation which has a video presentation, plus associated slides and transcript. You can just skim the slides/transcript if you are pressed for time.

Having done so, and given the rationale presented, the specific proposal is to add OKLab as follows: Edit: items re-ordered so highest priority is first.

CSS Color 4

  1. in the gamut mapping section, require gamut reduction to use OKLCH chroma reduction and deltaE OK, rather than CIE LCH chroma reduction and deltaE 2000. This is both better and faster. If we don't do this, I am not confident of being able to write a satisfactory gamut mapping section in CSS Color 4. See examples below
  2. in the interpolation section, for non-legacy color formats, if the host syntax does not specify a colorspace, change lab to oklab as the default. If we don't do this, color interpolation in gradients, animations and transitions will not be as good, and will sometimes give surprising hue shifts.
  3. add oklab() and oklch() (as well as the existing lab() and lch() which are still useful) and add their implementation to the sample code. Not doing this means we have a colorspace of proven usefulness, implemented inside the browser, but we deny users access to it.

CSS Color 5

  1. add oklab and oklch as defined color spaces which can be used in color-mix(). Depends on 3.
  2. add oklab() and oklch() to Relative Color Syntax. Depends on 3.

@argyleink @una @tabatkins @LeaVerou

svgeesus commented 1 year ago

And here is new perceptually uniform color spaces in CSS: easy to maintain color contrast, create accessible palettes, happy days! Except the fact we now should bring back a solar powered Casio calculator back to the working table and deal with acceptable values or AB mixture.

No you would just use an online tool, like everyone else.

svgeesus commented 1 year ago

address an issue (human-friendliness) I'm talking about in exchange of some trade-offs. Which is good enough for most of the use-cases.

The trade-offs are that they are no longer perceptually uniform, and that they are restricted to sRGB. These are very significant limitations; the former is the whole point of Oklab and the latter is deeply restricting when the Web has moved to display-p3 and is headed to Rec BT.2100.

Ptico commented 1 year ago

No you would just use an online tool, like everyone else.

I know, I'm exaggerating a bit, but jokes aside: I tried all of them and had almost memetic experience I wish to forget

JamesEggleton commented 1 year ago

@svgeesus I only just became aware of these efforts to improve the behaviour of the OKLab matrices when used with reference white of xy=(0.3127, 0.3290). This is work I have also done independently, so I thought I should share my results.

I have adjusted the four matrices such that they are accurate to float64 (IEEE 754 double precision), see code fragment below.

As far as I can tell the XYZtoLMS/LMStoXYZ/LMStoOKLab/OKLabtoLMS matrices currently in use (see here) are accurate to float32 precision. You'll notice that they match my numbers to somewhere between 6 and 8 significant figures, i.e. the expected precision of float32, or thereabouts. You should find that that my proposed values are slightly better behaved due to their additional precision, i.e. a D65 grey will map to LMS with components L=M=S, and OKLab with components a=0 and b=0.

My methodology has much in common to those proposed by @bottosson (see https://github.com/w3c/csswg-drafts/issues/6642#issuecomment-945714988) and @facelessuser (see https://github.com/w3c/csswg-drafts/issues/6642#issuecomment-943521484).

The M1 matrix fix (which relates XYZtoLMS and LMStoXYZ) involves applying gains in LMS space such that a chromaticity of xy=(0.3127, 0.3290) is exactly mapped to LMS=(1,1,1).

The M2 matrix fix (which relates to LMStoOKLab and OKLabtoLMS) involves interpreting the originally published numbers as float32 numbers, and then emitting the resulting matrix and its inverse in float64 precision. While this might sound like a bad idea, it is the correct approach because Bjorn's original optimisation emitted M1 and M2 at float32 precision, printed to 10 d.p. . Therefore it is correct to interpret them as float32 values. The result of handling the numbers in their native datatype is that my OKLabtoLMS matrix ends up with exact values (1.0, 1.0, 1.0) for its first column, and the rows of LMStoOKLab sum to (1, 0, 0). Note that due to the inherent precision of float64 there may be discrepencies in the order of 1e-15 when summing matrix rows, which is unavoidable and of no real world consequence.

Note that my knowledge of M1 and M2 matrices being defined to float32 precision comes from private communications with Bjorn in February 2021.

Note that all my intermediate calculations were carried out in "infinite precision" using a rational number library before being rounded to float64. The numeric values in the code snippet below have sufficient decimal places to exactly map to the intended float64 value, generally 16 or 17 significant figures.

function XYZ_to_OKLab(XYZ) {
    // Given XYZ relative to D65, convert to OKLab
    var XYZtoLMS = [
        [ 0.819022442647055,   0.3619062604571286, -0.12887378595068213 ],
        [ 0.0329836558550745,  0.9292868602921457,  0.03614466586101669 ],
        [ 0.04817719382823899, 0.2642395276821537,  0.6335478283072317  ]
    ];
    var LMStoOKLab = [
        [ 0.21045426824930336,   0.7936177747759865, -0.00407204302528986 ],
        [ 1.977998539071675,    -2.4285922502018127,  0.4505937111301374  ],
        [ 0.025904030547901084,  0.7827717270900503, -0.8086757576379514  ]
    ];

    var LMS = multiplyMatrices(XYZtoLMS, XYZ);
    return multiplyMatrices(LMStoOKLab, LMS.map(c => Math.cbrt(c)));
    // L in range [0,1]. For use in CSS, multiply by 100 and add a percent
}

function OKLab_to_XYZ(OKLab) {
    // Given OKLab, convert to XYZ relative to D65
    var LMStoXYZ =  [
        [  1.226879868224423,   -0.5578149934498884,   0.281391052277137   ],
        [ -0.04057574728813353,  1.1122868053350754,  -0.07171105804694196 ],
        [ -0.0763729441641797,  -0.42149332236303205,  1.5869240172870902  ]
    ];
    var OKLabtoLMS = [
        [ 1.0,  0.3963377773761749,   0.21580375730991364 ],
        [ 1.0, -0.10556134581565857, -0.0638541728258133  ],
        [ 1.0, -0.08948418498039246, -1.2914855480194092  ]
    ];

    var LMSnl = multiplyMatrices(OKLabtoLMS, OKLab);
    return multiplyMatrices(LMStoXYZ, LMSnl.map(c => c ** 3));
}

I can share my workings if desired, but it will take a bit of time to make it work as a standalone code fragment that is decoupled from proprietary libraries. In the mean time please feel free to try out these refined numbers.

facelessuser commented 1 year ago

@JamesEggleton I'd love to see the work! One of the issues with the current matrices is that Oklab and OkLCh just don't get chroma as close to zero when colors are achromatic. That leads to messy results when assuming zero for chroma or zero for a and b in Oklab, especially as lightness increases. This also makes the hue have more influence in good conversions as chroma is not quite small enough to completely drown out its influence.

>>> Color('white').convert('oklch', norm=False).set('hue', 0).set('chroma', 0).convert('srgb')[:]
[0.9999999610079292, 0.9999999999787738, 1.0000001063154036, 1.0]

But your matrices actually yield some pretty good results and have a "tightness" in the conversion that is more inline with the current Lab and LCh conversions:

>>> Color('white').convert('oklch', norm=False).set('hue', 0).set('chroma', 0).convert('srgb')[:]
[1.0000000000000004, 0.9999999999999997, 0.9999999999999999, 1.0]
>>> Color('white').convert('lch', norm=False).set('hue', 0).set('chroma', 0).convert('srgb')[:]
[0.9999999999999999, 0.9999999999999999, 0.9999999999999997, 1.0]
JamesEggleton commented 1 year ago

@facelessuser I'm happy to see you achieved decent results using these matrices. I'll clean up and share my workings as soon as I get a chunk of free time, hopefully in the next week or so...

facelessuser commented 1 year ago

Objectively, as someone that supports Oklab in a library outside of CSS, I can generally say these matrices seem to give values that you would expect while giving a better response for chroma and pretty much eliminating the effect of hue on achromatic colors.


This has nothing to do with CSS

With that said, and this would not specifically be a problem for CSS, I also support Okhsl and Okhsv. Using both the XYZtoLMS and LMStoOKLab matrix seems to have a noticeable impact on those two color spaces. I've tried using the workbook provided by Björn in his blog, updating the values to give better Okhsl and Okhsv support with these matrices, but there seems to be some tuning in the translation algorithm's not quite explained. Blue was pushed further out of the gamut, and then the algorithm would generate values too large for the language I was in and cause an overflow (probably due to the high precision of the values).

What I did find was that by simply using the more accurate LMStoOKLab matrix, I was able to get the same great response driving chroma to zero without causing a disparity between Oklab and Okhsl/Okhsv. I did not increase the precision of matrix calculations in Okhsl/Okhsv. Round trip was excellent between all these spaces.

I am most interested in getting the float 64 representation of this matrix.

[
    [0.77849780, 0.34399940, -0.12249720],
    [0.03303601, 0.93076195, 0.03620204],
    [0.05092917, 0.27933344, 0.66973739]
]

I wonder if the fact that I use a non-rational XYZtoRGB matrix everywhere else and that the calculation as you suggested used a rational number calculated XYZtoRGB matrix causes some disparity. Or if the Okhsl and Okhsv algorithm just isn't tuned well for these high-precision matrices and a slight shift to the response.

Regardless, using the high-precision LMStoOKLab matrix is more than sufficient to fix achromatic issues and provide good translations.

facelessuser commented 1 year ago

Okay, I was more systematic and was able to get Okhsl/Okhsv working with higher precision matrices but only if using the newly calculated LMStoOKLab matrix. I was able to use the Okhsl/Okhsv notebook provided in the related blog to update the tuning and no longer get any of the issues I did previously. It even gave better conversions for these spaces as well.

When I added the newly calculated LMS matrix in addition to the Oklab matrix, it seems to throw things a little off (pushing blue further out of gamut as an example), so for now, I'll probably stick with just the improved Oklab matrix until I can verify the LMS matrix. Considering how much better the calculations are with just the OKLAB matrix, I think the LMS matrix at slightly lower accuracy wasn't the driving force in bad chroma calculations.

>>> Color('gray').convert('oklab')[:]
[0.5998708056221469, -5.551115123125783e-17, -5.551115123125783e-17, 1.0]
>>> Color('white').convert('oklab')[:]
[1.0, -2.7755575615628914e-16, 0.0, 1.0]
>>> Color('darkgray').convert('oklab')[:]
[0.7348085828066983, -1.1102230246251565e-16, -1.1102230246251565e-16, 1.0]

I do wonder if creating the matrices with rational numbers (which also I assume includes the white point) causes the calculation to be different as everywhere else in my code, the white point is applied with floating point math, which likely leads to a disparity in white points causing calculations to not be quite right. It's also very likely that the Okhsl/Okhsv algorithm just needs some more manual tuning at whatever extreme the new matrix puts it at. I'll look into this more once the work is posted.

Regardless, even with just the improved Oklab matrix, things are actually no worse than when including the LMS matrix. And great conversion back to sRGB:

>>> Color('white').convert('oklab').convert('srgb')[:]
[0.9999999999999999, 0.9999999999999999, 0.9999999999999997, 1.0]
jessestricker commented 10 months ago

@JamesEggleton Did you find some time to publish your work yet? Looking forward to it! 🙂