Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exposing ColorSpaceWhitePoint is too heavyweight, and doesn't actually fix equality #30

Open
LeaVerou opened this issue Feb 13, 2023 · 11 comments
Labels
api-design Issues related to API shape

Comments

@LeaVerou
Copy link
Collaborator

Right now, the API defines a ColorSpaceWhitePoint class, with static predefined values ColorSpaceWhitePoint.D50 and ColorSpaceWhitePoint.D65 that authors are supposed to reference in creating ColorSpace 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 like if (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:

  • A private map of white point names to their x/y coords, pre-populated with D50 and D65, possibly a few more too.
  • A static method on ColorSpace to register a custom white point, which does not allow registration of duplicate values
  • A static method on ColorSpace to look up white point chromaticity values by their name
  • ColorSpaceOptions.white would be a string, referencing a registered whitepoint

This would allow handling white points as plain strings making equality checks easy.

Thoughts @svgeesus @tabatkins ?

@LeaVerou LeaVerou added the api-design Issues related to API shape label Feb 13, 2023
@tabatkins
Copy link
Collaborator

Alternate proposal:

  • Define a ColorSpaceWhitePoint dict with x and y members. ColorSpace.white takes this dict.
  • Define a static ColorSpace.D50 and ColorSpace.D65 that hold objects of that shape.
  • The getter for 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.

@svgeesus
Copy link
Collaborator

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.

@tabatkins
Copy link
Collaborator

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.

@LeaVerou
Copy link
Collaborator Author

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.

@tabatkins
Copy link
Collaborator

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?

@LeaVerou
Copy link
Collaborator Author

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.

@LeaVerou
Copy link
Collaborator Author

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 class
  • constructor({x, y})
  • ColorSpace.WhitePoint#equals() with signatures:
    • ColorSpace.WhitePoint#equals(otherWhitePoint [, ε])
    • ColorSpace.WhitePoint#equals({x, y} [, ε])
    • Default ε selected to make @svgeesus happy 😁
  • 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.

@tabatkins
Copy link
Collaborator

Sounds good to me.

@svgeesus
Copy link
Collaborator

svgeesus commented Feb 16, 2023

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.

details The DCI white is an ‘ugly green-white’ that was only there because projector manufacturers were concerned about achieving maximum brightness at 14fL for colors on the daylight axis. Getting to D60 or D65 was considered 5-12% less efficient.

D65 comes across as both a bit too ‘cool’ in appearance for live action content in a dim theatrical grading environment, and at the same time with Xenon projection lamps can have a magenta bias when looking at a full grey field. This is a reaction between the spectrum of Xenon and the eye color sensitivities. Testing at the Academy of Motion Pictures Arts and Sciences showed that D60 is a more appropriate ‘creative white’ (also called adopted white) for live action content. This is why it is chosen as the white point of the ACES system.
source

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.

@svgeesus
Copy link
Collaborator

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.

@svgeesus
Copy link
Collaborator

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.

@LeaVerou LeaVerou added this to the Level 2+ milestone Mar 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-design Issues related to API shape
Projects
None yet
Development

No branches or pull requests

3 participants