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

Approach to HCT Tonal Palettes #390

Closed
facelessuser opened this issue Jan 31, 2024 · 6 comments · Fixed by #420
Closed

Approach to HCT Tonal Palettes #390

facelessuser opened this issue Jan 31, 2024 · 6 comments · Fixed by #420

Comments

@facelessuser
Copy link
Collaborator

Now that the HCT color space is in, one of the reasons for people's interest is the ability to make tonal palettes.

To do this, we need to be able to gamut map in HCT in order to pull off values that align close to Material's. This issue is meant to discuss how Color.js would like to approach this.

  1. Color.js currently has two approaches to gamut mapping. One that is specifically CSS centric, the other more generic for LCh-like spaces.

  2. In order to calculate tonal palettes that are similar to Material we need a few things.

    1. We need to gamut map in HCT.
    2. We need to gamut map with essentially no JND or very, very small JND.
    3. HCT would need to clamp SDR black and white (like the CSS gamut mapping does).

Currently, Color.js does not allow changing the JND of either approach. The CSS approach provides the clamping of SDR white and black that we need, but the generic approach does not. The generic method allows for giving an arbitrary LCh space which we need. Ideally, we'd like to not leave the HCT color space due to the high conversion cost to convert out of HCT for distancing, but maybe this is not necessarily required. If we did want to use a different distancing more HCT-centric, there is no interface to swap out different distancing algorithms for gamut mapping either.

So, my question is how we'd like to approach this.

  1. Allow more configuration of one or more of the existing gamut solutions to meet the requirements? If so, which one?
  2. Provide an HCT-specific gamut mapping approach.
@svgeesus
Copy link
Member

svgeesus commented Feb 4, 2024

My preference is to leave the CSS-specific one alone, because converging on that exact algorithm here in in SS Color 4 took a fair bit of work and other projects are depending on it.

Extending the generic method to add optional parameters, such that existing code continues to work but there is more flexibility, seems a good approach.

That would include adding a new HCT deltaE metric, which could be specified along with the low JND and the clamping behavior to get the desired gamut mapping result.

Finally, a utility function with a method keyword, which produced the correct function call with the correct optional parameters for that specific case, would make it easier to use.

@facelessuser
Copy link
Collaborator Author

Finally, a utility function with a method keyword, which produced the correct function call with the correct optional parameters for that specific case, would make it easier to use.

I'm not exactly sure what is meant by this utility function, but I think I get the overall gist from your statement. I will most likely modify the non-CSS approach and extend the parameters to allow for the needed configuration to pull off tonal palettes.

If you can provide an example of the expectation for the utility function request, I am more than happy to implement it, I'm just currently not certain as to what is precisely being requested.

@svgeesus
Copy link
Member

svgeesus commented Feb 5, 2024

Instead of something like

let Color2 = Color1.toGamut({method: hct.c, jnd: 0.00005, blackWhiteClamp: true})

it would be good to have a method keyword that does all that for you.

@facelessuser
Copy link
Collaborator Author

Ah, that makes sense. Some new recognized keyword that just applies all of that. I will work on the implementation of the logic and expose some keyword that does what is asked. I won't worry too much about the name of that keyword and let the final name be decided in the review.

@facelessuser
Copy link
Collaborator Author

@svgeesus in the current gamut mapping algorithm, we always have the original color which is then converted to the target gamut space and then the gamut space gets converted to the mapping color space. Is there a reason for this? Does it force the normalization of undefined values or something like that? Or is there no intentional reason for this?

I ask because I'd like to change it so that the original color is converted to the gamut color, and then the original color is converted to the mapping color. My reason is that HCT and CAM16 have some particular cases that can be out of the visible gamut and when round-tripped will produce different colors. This is just a limit of CAM16 and CAM16-based spaces.

> const { default: Color } = require("colorjs.io");
undefined
> let c = new Color('blue').to('hct').set('t', 5);
undefined
> c.coords
[ 282.762176394358, 87.22803916105873, 5 ]
> c.to('srgb').to('hct').coords
[ 102.76217639435498, -59.52229328095306, 52.175560526116016 ]

In order to get good tonal maps, we have to gamut map the colors in HCT, ideally, we'd like to use the original color without round-tripping it if it is already in HCT. If there was a reason for this round tripping, I'd like to know the reason and address it directly opposed to introducing this round-tripping.

Below I will illustrate the results of keeping things the way they are vs my proposal. This can be particularly problematic in some low lightness cases.

Screenshot 2024-02-07 at 2 49 53 PM Screenshot 2024-02-07 at 2 48 34 PM

@svgeesus
Copy link
Member

@svgeesus in the current gamut mapping algorithm, we always have the original color which is then converted to the target gamut space and then the gamut space gets converted to the mapping color space. Is there a reason for this? Does it force the normalization of undefined values or something like that? Or is there no intentional reason for this?

In the CSS GMA there is no such step: after checking whether the destination space is unbounded, the second step is to convert the color to the mapping colorspace (Oklab).

I don't think there is a reason for having a two-step conversion; it is mixing up an in-gamut check with the GMA and would be better as two parallel conversion (original to target, to see if it is in gamut; then original to mapping spec if needed). And I agree that the current two-step conversion can introduce round-off error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants