Open LeaVerou opened 1 year ago
Alternate proposal:
ColorSpaceWhitePoint
dict with x
and y
members. ColorSpace.white
takes this dict.ColorSpace.D50
and ColorSpace.D65
that hold objects of that shape.cs.white
checks the x/y values, and if they're equal to D50 or D65 (within a tiny tolerance), returns the static objects; otherwise returns a fresh JS object.This way authors can check if it's the predefined whitepoint by using reference equality: myCS.white === ColorSpace.D65
, even if they set it to the predefined whitespace manually. And we don't have to worry about registrations.
A static method on ColorSpace to register a custom white point, which does not allow registration of duplicate values
This is a forward-compatibility hazard, since we'll be prepopulating it. To make it safe to add more predefined whitepoints in the future we'd have to allow new regs to override existing ones.
The getter for cs.white checks the x/y values, and if they're equal to D50 or D65 (within a tiny tolerance)
That sounds reasonable (and is what CSS Color 4 was doing, at first) but led to a world of pain and multiple rewrites of conversion matrices over the last couple of years due to weird non-chromatic values on neutrals from color conversions. These were actually visible (yellowing) as well as being unexpected and annoying.
Which is why CSS Color 4: white points now says:
To avoid cumulative round-trip errors, it is important that the identical chromaticity values are used consistently, at all places in a calculation. Thus, for maximum compatibility, for this specification, the following two standard daylight-simulating white points are defined:
and then gives the exact values with 6-digit precision, with zeroes for any subsequent digits.
So it was important for round-trippability that D65 was [0.312700, 0.329000]
and we had to recalculate the sRGB matrices which originally used the ASTM E308-01 values [ 0.3127266146810121, 0.32902313032606195]
. Similarly we had to recalculate the Oklab conversion, because the original publication used ASTM values rounded to 5 places: [0.31272, 0.32902]
. And Oklab also had earlier used slightly different, rounded values.
I'm aware that we can't really compare floats for equality, but the tiny tolerance is going to be 0.0000001
or smaller.
Comparing objects does sound like a better way forward.
We should probably be doing the test on set actually, not get, and if it's within a tolerance of the predefined, just correct it to exactly the predefined.
The downside with this approach is that if we have multiple color spaces sharing the same (non D50, non D65) white point, we don't get the convenient "if close enough, cast to the same object" behavior. Perhaps we still need a registration mechanism?
We should probably also not hang these directly on ColorSpace
. We also had thoughts about exposing predefined color spaces there, the namespace is starting to get polluted.
Yeah, you do lose the easy testing there, but... how important is that? How common are whitepoints other than D50 and D65? And if people have easy testing of those two, is it worth adding a whole registration mechanism over just having people write a convenience method that compares two whitepoints?
How common are whitepoints other than D50 and D65?
That's a question for @svgeesus . IIRC there was only one HDR color space that used a different white point.
Yeah, you do lose the easy testing there, but... how important is that? And if people have easy testing of those two, is it worth adding a whole registration mechanism over just having people write a convenience method that compares two whitepoints?
It's not so much the inconvenience, but the inconsistency that rubs me the wrong way. There is a certain "magic" built-in behavior that is not exposed and cannot be extended. I worry it's a bit of a footgun. I also cannot think of any other API in the Web Platform that does something similar.
Another direction we could go is to only have predefined whitepoints in L1 (either as objects or as strings), and see what happens and design L2 based on input from use cases.
The downside of having predefined static attributes directly on a class is that there's no easy way to enumerate these whitepoints. You'd need to enumerate all static properties and filter out those that don't match. And if there is no class for white points this can only be done with duck typing (if (o.x && o.y) ...
).
It's a tradeoff though, cause you also don't want some super long thing like ColorSpaceWhitePoint.common.D50
.
What about the following design:
ColorSpace.WhitePoint
classconstructor({x, y})
ColorSpace.WhitePoint#equals()
with signatures:
ColorSpace.WhitePoint#equals(otherWhitePoint [, ε])
ColorSpace.WhitePoint#equals({x, y} [, ε])
ColorSpace.WhitePoint.D50
and ColorSpace.WhitePoint.D65
Then Color API would use ColorSpace.WhitePoint#equals()
internally at color space construction time to cast whitepoints to predefined ones. ColorSpace specs could also use strings like "D50"
, but colorSpace.white
will still be an object.
Note that how this is done is important, not just for developers wishing to compare two ColorSpace objects for whitepoints (rather uncommon), but primarily internally: Color API needs to know when two color spaces have the same or different white point, to see if it needs to do chromatic adaptation when converting between these spaces. @svgeesus suggested that if whitepoints are marginally different, chromatic adaptation won't change the color much, but I really don't like this solution. It's wasteful wrt resources, and authors don't like seeing their numbers being fudged.
Sounds good to me.
How common are whitepoints other than D50 and D65?
The Digital Cinema Initiative colorspace, DCI-P3, uses a weird greenish white (0.314, 0.351) for reasons to do with the xenon bulb used in digital projectors.
The Academy Color Encoding Specification (ACES) colorspaces use (0.32168, 0.33767) which is similar to D60.
So, not very, but a fully-fledged system does need to account for them. An MVP system can do just fine with D65 and D50.
Oh, and I am answering the more specific question "How common are color spaces that use whitepoints other than D50 and D65?" here.
In general color science they are a lot more common, used for tasks like "I measured these printed colors with this illuminant, now adapt them to D50 please". That is well outside the scope of this API, but that is why we support arbitrary white points and multiple chromatic adaptation algorithms in color.js.
Color API needs to know when two color spaces have the same or different white point, to see if it needs to do chromatic adaptation when converting between these spaces.
Yes, this is crucial otherwise all the affected conversions are just plain wrong.
if whitepoints are marginally different, chromatic adaptation won't change the color much, but I really don't like this solution. It's wasteful wrt resources, and authors don't like seeing their numbers being fudged
My point was that if someone registers myD65 which is similar but not identical to D65, the system will treat that as different, compute a chromatic adaptation matrix which is similar but not identical to unity, and apply it to get the right result. In other words it accepts it gracefully and gives the correct result.
Right now, the API defines a
ColorSpaceWhitePoint
class, with static predefined valuesColorSpaceWhitePoint.D50
andColorSpaceWhitePoint.D65
that authors are supposed to reference in creatingColorSpace
objects.This means that in the case where the white point is something else, code would need to do things like
if (c.white.x === ACES.x && c.white.y === ACES.y)
to compare with it, not to mention that things likeif (c.white === ColorSpaceWhitePoint.D65)
do not account for creating ColorSpaceWhitePoint objects with the same x and y values as the predefined ones (which should really be identified as D50/D65).Also, exposing a whole other interface just for this seems pointlessly heavyweight.
I would propose a different design:
ColorSpace
to register a custom white point, which does not allow registration of duplicate valuesColorSpace
to look up white point chromaticity values by their nameColorSpaceOptions.white
would be a string, referencing a registered whitepointThis would allow handling white points as plain strings making equality checks easy.
Thoughts @svgeesus @tabatkins ?