-
-
Notifications
You must be signed in to change notification settings - Fork 84
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
Add the HCT color space #380
Conversation
✅ Deploy Preview for colorjs ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
As a note, this does not port Google's Material Color Utility implementation. This implements the color space as described in https://material.io/blog/science-of-color-design. It combines CAM16 JCh and Lab D65. I did an experiment with Google's lib, and they snap conversions to HCT to chroma bands and only convert back to about 8 bits worth of precision (hex sRGB). This implements the full color space and is not restricted to sRGB. It generally does well when converting colors within the visible spectrum, but pushing out beyond that, conversion precision will drop. Even CAM16's geometry gets really weird as you push outside the visible gamut, so this isn't much of a surprise. The last PR should implement a color distancing algorithm while in HCT and a way to gamut map in HCT such that tonal palettes can be produced (similar to Google's). An example of what should be achievable: https://facelessuser.github.io/coloraide/colors/hct/#tonal-palettes. |
40e6244
to
2e95b69
Compare
I want to investigate a couple of extreme cases, so putting this in draft. |
I think edge cases are understood. Because the only way to reverse transform this space is via approximation, it is simply impossible to perfectly convert every color value that may be thrown at it. I ran a script for a while and it would randomly create a color within some of the largest spaces: Rec2020, Rec2100 PQ, Rec. 2100 HLG, ACEScg. It would then convert them to HCT and then back and see if the colors were the same up to about 5 digits. After 6 million +, I only had 3 cases, all of which were barely outside of the target:
I don't think any of these are worth spending time on more iterations or more complex code. There are of course some more extreme cases. When you push HCT (which is basically CAM16) past the visible spectrum, you can get some harder to calculate colors. The algorithm will have a more trouble.
I've been able to implement code that can actually steer it in the right direction in both of these cases, but even then, it can't fix all cases, and not always in a reasonable amount of iterations. To explain why this happens, we can look at CAM16 JMh. Consider the low light color with a chroma way larger than a color at that lightness has any right to have. If we try and round trip the CAM16 color, it ends up with an inverse saturation and a rotated hue. It is out of the visible gamut. This is not unique to our implementation, this behavior has been confirmed in other CAM16 implementations as well. >>> Color('blue').convert('cam16-jmh').set('j', 2)
color(--cam16-jmh 2 62.442 282.75 / 1)
>>> Color('blue').convert('cam16-jmh').set('j', 2).convert('srgb').convert('cam16-jmh')
color(--cam16-jmh 2 -42.607 102.75 / 1)
>>> Color('blue').convert('cam16-jmh').set('j', 2).convert('xyz-d65')
color(xyz-d65 -0.04159 0.00375 -0.31127 / 1) This of course affects our ability to approximate colors in HCT to the same accuracy. While some workarounds can help these converge better, it doesn't change the behavior because this is how CAM16 works with some of these extreme colors. So, does this affect tonal palettes? We are able to generate tonal palettes that pretty much align with Material's results: Results can be viewed via the link below. I was experimenting with generating OkLCh tonal palettes vs HCT. Spoiler, you can actually generate some pretty good tonal palettes with OkLCh without all of the complicated overhead of HCT, it just requires toeing the lightness and tightening the gamut mapping: https://facelessuser.github.io/coloraide/playground/?notebook=https%3A%2F%2Fgist.githubusercontent.com%2Ffacelessuser%2F0235cb0fecc35c4e06a8195d5e18947b%2Fraw%2F735d97fa895a0f9f1610189a31028b7bb1bbc2bb%2Fexploring-tonal-palettes.md I think with this analysis, I am confident to say that I feel the conversion algorithm is good enough. As I mentioned there are maybe two things that could additionally be done to improve the accuracy of some already bad cases of colors outside the visible spectrum, but they are still inherently bad just due to how CAM16 handles these extreme colors. I will move this back into the review state. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great overall, wasn't sure about the tests though.
test/conversions.js
Outdated
}, | ||
tests: [ | ||
{ | ||
name: "sRGB white to CAM16 JMh", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to CAM16 JMh or to HCT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, missed this. I copied the CAM16 JMh tests tests and altered them for HCT. I forgot to change the names though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in latest commit
Does the Google implementation map to the sRGB gamut in CAM16 JMh or in some other space? |
I'm not quite sure I understand the question, so I'll answer as best I can. The Material library is restricted to the sRGB gamut. That's an implementation detail not a limitation of the space. Their interface allows you to specify 8bit, sRGB hex codes and it converts them to HCT. Conversion back gives you 8 bit, sRGB hex codes. I think their white point is a slightly different, rounded D65 white point than what CSS uses: We do not clamp this implementation. It gives full resolution of the space. In order to convert back, we must convert both the LCh part and the CAM16 parts. Since the LCh Y part is easy to convert back to XYZ with no additional context, the bulk of the conversion part is trying to figure out the missing CAM16 J part in order to convert the chroma and hue parts of CAM16 JCh, which is required for the algorithm to accurately convert back to XYZ and then to any other gamut. The accuracy of the algorithm is limited by time. The Newton algorithm's success is also influenced by the initial guess. There are probably more time-consuming ways to get a better initial guess, but for 99% of the gamuts that fall within the visible spectrum, we are able to hit our accuracy target with a "close enough guess" using our simple polynomial. The CAM16 JMh, JCh, etc. algorithm breaks down a bit with colors outside the visible spectrum, so HCT will also start to distort when the visible spectrum is exceeded. You can see CAM16 wrapping blue back into itself with ProPhoto which pushes these colors outside the visible spectrum. HCT does the same and will have less accuracy in these regions which will push Newton's method harder. |
* main: Fix toPrecition (was off by one for fractional inputs) (color-js#384) Add the HCT color space (color-js#380) Add gamutSpace to ColorSpace Add CJS type defs for node16 resolution. (color-js#383)
Implements the HCT color space.