Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

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

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

Already on GitHub? # to your account

Target not equal to source when converting rgb(1,1,1) to oklch, and then back to rgb. #236

Closed
Jeremy-Knudsen opened this issue May 10, 2024 · 2 comments

Comments

@Jeremy-Knudsen
Copy link

I store original source rgb colors in an array variable, which are taken from the Rgb color space. I then have a user interface that allows a user to manipulate these source colors in the Oklch color space, which then renders the user-manipulated colors back in the Rgb color space.

The problem is that the target never ends up equaling its source even when there is no manipulation of hue, chroma, or luminosity as shown here:

With using clampRgb:
`
culori.clampRgb(culori.rgb(culori.oklch({
"mode": "rgb",
"r": 1,
"g": 1,
"b": 1
})))

{mode: 'rgb', r: 0.9999999608276361, g: 1, b: 1} // Result: expected value of r = 1, but got a value < 1
`

Without using clampRgb:
`
culori.rgb(culori.oklch({
"mode": "rgb",
"r": 1,
"g": 1,
"b": 1
}))

{mode: 'rgb', r: 0.9999999608276361, g: 1.00000000005861, b: 1.0000001062495965} // Result: expected value of g < 1 and b < 1, but got g and b values > 1
`

Why does this matter? Because when a user increases the chroma of pure white, I would expect pure white to stay pure white since it is achromatic. But because of this issue, white can becomes a color (often magenta) when chroma is added.

@danburzo
Copy link
Collaborator

Hi Jeremy, thanks for the issue.

The results you’re seeing are caused by the fact that a roundtrip from sRGB to Oklch and back introduces subtle precision errors. They are, to some extent, unavoidable and unfortunately need to be actively managed depending on the usage.

I believe we are currently using the matrices from the original Oklab article, while the CSS Color Level 4 spec has updated them (see this discussion). It offers better roundtripping but does not completely eliminate these sort of small errors.

You may want to experiment with rounding these small values to a number of decimals that make practical sense for your use case. Culori has the culori.round() helper for that:

import { round } from 'culori';
round(7)(0.9999999608276361); // => 1

(As a sidenote, you may also want to look at culori.mapper as a way to apply round() to all components in a color)

In the specific case of Oklch colors, you may want to consider colors with a very small chroma to be achromatic:

import { rgb, oklch } from 'culori';
const color = oklch(rgb(oklch('white'))); 
// => { mode: "oklch", l: 0.9999999934735458, c: 1.5700924586837752e-16, h: 135 }
// make color with infinitely small chroma achromatic.
if (color.c < 1e-8) {
	color.c = 0;
	delete color.h;
}

@Jeremy-Knudsen
Copy link
Author

Dan, thank you so much for your quick reply and for all of the helpful tips and code examples! A good bit to add to the documentation.

@Evercoder Evercoder locked and limited conversation to collaborators May 12, 2024
@danburzo danburzo converted this issue into discussion #238 May 12, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants