-
Notifications
You must be signed in to change notification settings - Fork 60
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 Okhsv
, Okhsl
and Okhwb
#282
Conversation
* added OKhsl and Okhsv color spaces
I don't know much about color in general and palette's structure in particular. So I expect this PR to require very, very serious corrections and refactoring. Be nice, but please be clear. Don't sugar coat. |
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.
Thanks a lot for opening this PR! I think it looks good from a first look-through but I need to wrap my head around more of the details to be able to give a deeper review. I have at least added some surface level comments to begin with. Note that I didn't comment on every occasion of, for example, a print that will has to be removed before merging, but it applies for everything.
You pointed out that you wanted more info on the conversion macro. The macro is essentially a substitute for specialization, and connects all color types with each other. The manual implementations make up a tree of "closest neighbors", with Xyz
as the root, that are used as the foundation. It then figures out the appropriate conversion implementations to delegate to for all other cases. For example to go from Rgb
to Lch
it finds Rgb -> Xyz -> Lab -> Lch
.
There is a list of all the color types and their "closest neighbors" towards Xyz
in palette_derive/src/lib.rs
. You will have to add these two types and say that they should be connected to Oklab
. The Ok*
colors are also a bit special since they only use the D65
white point, so you will have to add them as exceptions in a few places. You can look at Oklch
as a reference and search for it in the macro files. I hope it helps, but let me know otherwise.
It would also be super if we can have a counterpart to HWB
, which is just a different form of HSV
. I know the original blog post doesn't mention it but I think we can add it as a custom extension since it's such an easy thing to implement. It's just a bit tedious. You can also skip it if you don't feel like it and just not close the the issue this time around. I or someone else can add it afterwards.
Another thing that we can keep as extra credits is to make the implementation SIMD friendly. This means replacing if
/if else
/else
with lazy_select!
and friends. If you don't want to get into that, you can just add HasBoolMask<Mask=bool>
to every bounds that has PartialOrd
or PartialEq
. That signals that we don't expect SIMD types (for now).
Let me know if you need some more info or help! I'll be able to do more during the weekend but can always answer questions.
palette/src/okhsl.rs
Outdated
@@ -0,0 +1,626 @@ | |||
use approx::AbsDiffEq; | |||
use std::fmt::Debug; |
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.
Just a reminder to change this to core
or gate behind the std
feature.
palette/src/okhsl.rs
Outdated
/// * 240° to a kind of blue (RBG #00aefe). | ||
/// | ||
/// For s == 0 or v == 0, the hue is irrelevant. | ||
pub h: T, |
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.
I would prefer if we can use a hue type here. I have to check what exactly is going on in the conversion but it looks like it's just the oklab hue, but rotated 180 degrees. Unless the negation of a
and b
is an error or I'm misunderstanding.
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.
Also, prefer full names over single letters, so hue
rather than h
, etc.
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.
There is something strange about Okhsl's hue.
Björn Ottosson's blog starts
To derive Okhsv, we will start with OkLCh, use its estimate for hue, hhh, as is
which sounds like Okhsv should have the same hue as OklCh, which is defined as h∘=atan2(b,a)
The reference implementation however computes and returns the hue as
float h = 0.5f + 0.5f * atan2f(-lab.b, -lab.a) / pi;
The unit here is degrees/360
and there is the 180° rotation, you mentioned.
I changed that to normal degrees, but didn't know what to make of the rotation. Online color pickers, that just copied the Javascript implementation of the interactive demo stick to the rotation. The CSS Color Modul Level 4 draft defines Oklab, but not Okhsl or Okhsv.
What format do you suggest for the hue?
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.
That actually explains it all, it's all the same hue. The addition of 0.5 corrects it all by adding back the missing 180 degrees. It's probably related to the scaling. I also found this where they don't negate a
and b
: https://github.com/Evercoder/culori/blob/main/src/okhsl/convertOklabToOkhsl.js
But that means we should use OklabHue<T>
as the hues.
palette/src/okhsl.rs
Outdated
T: Real + Zero + One + PartialOrd + Debug + Copy + Arithmetics, | ||
{ | ||
/// Create an Okhsl color. | ||
// FIXMe: cannot make constructor constant because zero and one are not constant |
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.
Constructors in this lib are usually simple and don't perform any logic, since there are some cases where having "strange" values is useful. Arithmetic calculations, for example. The IsWithinBounds
trait is meant for validating the values.
You can remove all the corrections and asserts. And the print. 🙂
palette/src/okhsl.rs
Outdated
|
||
/// # See | ||
/// See [`okhsl_to_srgb`](https://bottosson.github.io/posts/colorpicker/#hsl-2) | ||
impl<T> FromColorUnclamped<Okhsl<T>> for Oklab<T> |
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.
I prefer to have the implementations for the types in their files, so it would be great if you can move this to oklab.rs
.
* integrate Okhsv, Okhsl and Okhwb in conversion macro * move FromColorUnclamped implementations to module containing struct * remove println * remove assertions and corrections from constructors * refactor functions and types used in several Ok* modules in ok_ultils module * use OklabHue type for hues of Ok* * rename fields of Ok* to full names * replace std by core
* convert variables to snake case * added missing comments * commented out unused assignment
palette/src/oklab.rs
Outdated
//FIXME:https://colorjs.io/docs/spaces.html#oklab uses different values to the documentation above and these min- and max- values | ||
// From my understanding Chroma in Oklab is unlimited, as it is only limited, when used in reference to the gamut of a specific display technology. | ||
// So here, a and b should be unlimited. Only in Okhsl and Okhsv should chroma be limited, as they reference the sRGB gamut. | ||
// For a different Okhsl and Okhsv, which references HDR (https://bottosson.github.io/posts/colorpicker/#ideas-for-future-work) they'd need to be different again. |
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.
A note on HDR is that Palette doesn't try to handle it in any special way at the moment. It's a plus if it happens to work but not necessary for now. Let's start with sRGB for now. A future extension could be to look into supporting other gamuts, maybe by generalizing over RgbSpace
if that's sufficient.
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.
So should I just remove this fixme and be done with it or should I add a comment somewhere? And if so, what should it say and where would I put it?
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.
You can mention in the implementation, where it adjusts to the gamut, that we assume sRGB. Maybe also in the descriptions of Okhsv
, Okhsl
and Okhwb
so it's clear what they represent. Otherwise, it's enough to refer to the reference implementation so there's something to compare to when debugging or reworking something. I appreciate that you have already added comments with clarifications in some places! It helps a lot!
impl<T> FromColorUnclamped<Okhsv<T>> for Xyz<D65, T> | ||
where | ||
Okhsv<T>: IntoColorUnclamped<Oklab<T>>, | ||
Self: FromColorUnclamped<Oklab<T>>, | ||
{ | ||
fn from_color_unclamped(color: Okhsv<T>) -> Self { | ||
let oklab: Oklab<T> = color.into_color_unclamped(); | ||
Self::from_color_unclamped(oklab) | ||
} | ||
} | ||
|
||
impl<T> FromColorUnclamped<Okhsl<T>> for Xyz<D65, T> | ||
where | ||
Okhsl<T>: IntoColorUnclamped<Oklab<T>>, | ||
Self: FromColorUnclamped<Oklab<T>>, | ||
{ | ||
fn from_color_unclamped(color: Okhsl<T>) -> Self { | ||
let oklab: Oklab<T> = color.into_color_unclamped(); | ||
Self::from_color_unclamped(oklab) | ||
} | ||
} | ||
|
||
impl<T> FromColorUnclamped<Okhwb<T>> for Xyz<D65, T> | ||
where | ||
Okhwb<T>: IntoColorUnclamped<Okhsv<T>>, | ||
Self: FromColorUnclamped<Okhsv<T>>, | ||
{ | ||
fn from_color_unclamped(color: Okhwb<T>) -> Self { | ||
let okhsv: Okhsv<T> = color.into_color_unclamped(); | ||
Self::from_color_unclamped(okhsv) | ||
} | ||
} |
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.
These should not be necessary. This is precisely the type of code the macro should generate. Did you run into any issues with them? There may very well be a hole in its logic.
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.
I didn't run into issues. I just added stuff without any understanding until the compiler was satisfied. Then I stopped. Still without understanding :(
I will remove them.
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.
Let me know if the compiler complains without them (and without mentioning them in the exclusion list above), and we can figure it out. 🙂
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.
I've removed them locally, and the compiler is fine.
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.
Nice! Then all should be fine if it's still possible to convert to Xyz
. It's a bit tricky that it doesn't complain until you try to use it somewhere. There are a bunch of tests in convert.rs
that checks this but it looks like the Ok* colors are not there yet.
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.
I shouldn't have counted our chickens before they hatched. The compiler is fine, but when running tests, 3 doc tests fail. So I will leave them in.
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.
Can you paste the compiler output here, including hints and notes? I would like to see what it complains about.
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.
error[E0308]: mismatched types
--> src\cast.rs:39:42
|
6 | let color = cast::from_array::<Srgb>([23u8, 198]); // Too few components.
| ---------------------------- ^^^^^^^^^^^ expected an array with a fixed size of 3 elements, found one with 2 elements
| |
| arguments to this function are incorrect
|
note: function defined here
--> C:\Users\Navid\Softwareentwicklung\rust_projects\palette\palette\src\cast\array.rs:200:8
|
200 | pub fn from_array(array: T::Array) -> T
| ^^^^^^^^^^
error: aborting due to previous error
For more information about this error, try rustc --explain E0308
.
warning: derive helper attribute is used before it is introduced
--> src\convert.rs:220:3
|
11 | #[palette(
| ^^^^^^^
...
15 | #[derive(PartialEq, Debug, FromColorUnclamped, WithAlpha)]
| ------------------ the attribute is introduced here
|
= note: #[warn(legacy_derive_helpers)]
on by default
= warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
= note: for more information, see issue #79202 rust-lang/rust#79202
error[E0277]: the trait bound Xyz: FromColorUnclamped<Okhwb>
is not satisfied
--> src\convert.rs:224:28
|
15 | #[derive(PartialEq, Debug, FromColorUnclamped, WithAlpha)]
| ^^^^^^^^^^^^^^^^^^ the trait FromColorUnclamped<Okhwb>
is not implemented for Xyz
|
= help: the following other types implement trait FromColorUnclamped<T>
:
<Xyz as FromColorUnclamped>
<Xyz<D65, T> as FromColorUnclamped<Oklab>>
<Xyz<D65, T> as FromColorUnclamped<Oklch>>
<Xyz<Wp, T> as FromColorUnclamped<Alpha<_C, _A>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsl<_S, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsluv<Wp, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsv<_S, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hwb<_S, T>>>
and 8 others
= note: required because of the requirements on the impl of FromColorUnclamped<Okhwb>
for Rgb
= help: see issue #48214
= help: add #![feature(trivial_bounds)]
to the crate attributes to enable
= note: this error originates in the derive macro FromColorUnclamped
(in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound Xyz: FromColorUnclamped<Okhsl>
is not satisfied
--> src\convert.rs:224:28
|
15 | #[derive(PartialEq, Debug, FromColorUnclamped, WithAlpha)]
| ^^^^^^^^^^^^^^^^^^ the trait FromColorUnclamped<Okhsl>
is not implemented for Xyz
|
= help: the following other types implement trait FromColorUnclamped<T>
:
<Xyz as FromColorUnclamped>
<Xyz<D65, T> as FromColorUnclamped<Oklab>>
<Xyz<D65, T> as FromColorUnclamped<Oklch>>
<Xyz<Wp, T> as FromColorUnclamped<Alpha<_C, _A>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsl<_S, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsluv<Wp, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsv<_S, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hwb<_S, T>>>
and 8 others
= note: required because of the requirements on the impl of FromColorUnclamped<Okhsl>
for Rgb
= help: see issue #48214
= help: add #![feature(trivial_bounds)]
to the crate attributes to enable
= note: this error originates in the derive macro FromColorUnclamped
(in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound Xyz: FromColorUnclamped<Okhsv>
is not satisfied
--> src\convert.rs:224:28
|
15 | #[derive(PartialEq, Debug, FromColorUnclamped, WithAlpha)]
| ^^^^^^^^^^^^^^^^^^ the trait FromColorUnclamped<Okhsv>
is not implemented for Xyz
|
= help: the following other types implement trait FromColorUnclamped<T>
:
<Xyz as FromColorUnclamped>
<Xyz<D65, T> as FromColorUnclamped<Oklab>>
<Xyz<D65, T> as FromColorUnclamped<Oklch>>
<Xyz<Wp, T> as FromColorUnclamped<Alpha<_C, _A>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsl<_S, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsluv<Wp, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsv<_S, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hwb<_S, T>>>
and 8 others
= note: required because of the requirements on the impl of FromColorUnclamped<Okhsv>
for Rgb
= help: see issue #48214
= help: add #![feature(trivial_bounds)]
to the crate attributes to enable
= note: this error originates in the derive macro FromColorUnclamped
(in Nightly builds, run with -Z macro-backtrace for more info)
error: aborting due to 3 previous errors; 1 warning emitted
For more information about this error, try rustc --explain E0277
.
Couldn't compile the test.
error[E0277]: the trait bound Xyz: FromColorUnclamped<Okhwb>
is not satisfied
--> src\convert.rs:91:28
|
7 | #[derive(PartialEq, Debug, FromColorUnclamped)]
| ^^^^^^^^^^^^^^^^^^ the trait FromColorUnclamped<Okhwb>
is not implemented for Xyz
|
= help: the following other types implement trait FromColorUnclamped<T>
:
<Xyz as FromColorUnclamped>
<Xyz<D65, T> as FromColorUnclamped<Oklab>>
<Xyz<D65, T> as FromColorUnclamped<Oklch>>
<Xyz<Wp, T> as FromColorUnclamped<Alpha<_C, _A>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsl<_S, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsluv<Wp, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsv<_S, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hwb<_S, T>>>
and 8 others
= help: see issue #48214
= help: add #![feature(trivial_bounds)]
to the crate attributes to enable
= note: this error originates in the derive macro FromColorUnclamped
(in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound Xyz: FromColorUnclamped<Okhsl>
is not satisfied
--> src\convert.rs:91:28
|
7 | #[derive(PartialEq, Debug, FromColorUnclamped)]
| ^^^^^^^^^^^^^^^^^^ the trait FromColorUnclamped<Okhsl>
is not implemented for Xyz
|
= help: the following other types implement trait FromColorUnclamped<T>
:
<Xyz as FromColorUnclamped>
<Xyz<D65, T> as FromColorUnclamped<Oklab>>
<Xyz<D65, T> as FromColorUnclamped<Oklch>>
<Xyz<Wp, T> as FromColorUnclamped<Alpha<_C, _A>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsl<_S, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsluv<Wp, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsv<_S, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hwb<_S, T>>>
and 8 others
= help: see issue #48214
= help: add #![feature(trivial_bounds)]
to the crate attributes to enable
= note: this error originates in the derive macro FromColorUnclamped
(in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound Xyz: FromColorUnclamped<Okhsv>
is not satisfied
--> src\convert.rs:91:28
|
7 | #[derive(PartialEq, Debug, FromColorUnclamped)]
| ^^^^^^^^^^^^^^^^^^ the trait FromColorUnclamped<Okhsv>
is not implemented for Xyz
|
= help: the following other types implement trait FromColorUnclamped<T>
:
<Xyz as FromColorUnclamped>
<Xyz<D65, T> as FromColorUnclamped<Oklab>>
<Xyz<D65, T> as FromColorUnclamped<Oklch>>
<Xyz<Wp, T> as FromColorUnclamped<Alpha<_C, _A>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsl<_S, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsluv<Wp, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsv<_S, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hwb<_S, T>>>
and 8 others
= help: see issue #48214
= help: add #![feature(trivial_bounds)]
to the crate attributes to enable
= note: this error originates in the derive macro FromColorUnclamped
(in Nightly builds, run with -Z macro-backtrace for more info)
error: aborting due to 3 previous errors
For more information about this error, try rustc --explain E0277
.
Couldn't compile the test.
error[E0369]: cannot add Rgb<Srgb, {float}>
to Rgb<Srgb, {float}>
--> src\lib.rs:26:37
|
9 | let whatever_it_becomes = orangeish + blueish; // Does not compile
| --------- ^ ------- Rgb<Srgb, {float}>
| |
| Rgb<Srgb, {float}>
error: aborting due to previous error
For more information about this error, try rustc --explain E0369
.
error[E0277]: the trait bound Xyz: FromColorUnclamped<Okhwb>
is not satisfied
--> src\lib.rs:1772:10
|
12 | #[derive(FromColorUnclamped, WithAlpha)]
| ^^^^^^^^^^^^^^^^^^ the trait FromColorUnclamped<Okhwb>
is not implemented for Xyz
|
= help: the following other types implement trait FromColorUnclamped<T>
:
<Xyz as FromColorUnclamped>
<Xyz<D65, T> as FromColorUnclamped<Oklab>>
<Xyz<D65, T> as FromColorUnclamped<Oklch>>
<Xyz<Wp, T> as FromColorUnclamped<Alpha<_C, _A>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsl<_S, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsluv<Wp, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsv<_S, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hwb<_S, T>>>
and 8 others
= note: required because of the requirements on the impl of FromColorUnclamped<Okhwb>
for Rgb
= help: see issue #48214
= help: add #![feature(trivial_bounds)]
to the crate attributes to enable
= note: this error originates in the derive macro FromColorUnclamped
(in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound Xyz: FromColorUnclamped<Okhsl>
is not satisfied
--> src\lib.rs:1772:10
|
12 | #[derive(FromColorUnclamped, WithAlpha)]
| ^^^^^^^^^^^^^^^^^^ the trait FromColorUnclamped<Okhsl>
is not implemented for Xyz
|
= help: the following other types implement trait FromColorUnclamped<T>
:
<Xyz as FromColorUnclamped>
<Xyz<D65, T> as FromColorUnclamped<Oklab>>
<Xyz<D65, T> as FromColorUnclamped<Oklch>>
<Xyz<Wp, T> as FromColorUnclamped<Alpha<_C, _A>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsl<_S, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsluv<Wp, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsv<_S, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hwb<_S, T>>>
and 8 others
= note: required because of the requirements on the impl of FromColorUnclamped<Okhsl>
for Rgb
= help: see issue #48214
= help: add #![feature(trivial_bounds)]
to the crate attributes to enable
= note: this error originates in the derive macro FromColorUnclamped
(in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound Xyz: FromColorUnclamped<Okhsv>
is not satisfied
--> src\lib.rs:1772:10
|
12 | #[derive(FromColorUnclamped, WithAlpha)]
| ^^^^^^^^^^^^^^^^^^ the trait FromColorUnclamped<Okhsv>
is not implemented for Xyz
|
= help: the following other types implement trait FromColorUnclamped<T>
:
<Xyz as FromColorUnclamped>
<Xyz<D65, T> as FromColorUnclamped<Oklab>>
<Xyz<D65, T> as FromColorUnclamped<Oklch>>
<Xyz<Wp, T> as FromColorUnclamped<Alpha<_C, _A>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsl<_S, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsluv<Wp, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hsv<_S, T>>>
<Xyz<Wp, T> as FromColorUnclamped<Hwb<_S, T>>>
and 8 others
= note: required because of the requirements on the impl of FromColorUnclamped<Okhsv>
for Rgb
= help: see issue #48214
= help: add #![feature(trivial_bounds)]
to the crate attributes to enable
= note: this error originates in the derive macro FromColorUnclamped
(in Nightly builds, run with -Z macro-backtrace for more info)
error: aborting due to 3 previous errors
For more information about this error, try rustc --explain E0277
.
Couldn't compile the test.
error: cannot find macro lazy_select
in this scope
--> src\macros\lazy_select.rs:4:14
|
3 | let result = lazy_select! {
| ^^^^^^^^^^^
error: aborting due to previous error
Couldn't compile the test.
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.
Thanks! Too bad it doesn't say a lot, but I hoped it would give a hint. Anyway, I can see if I can fix it later. You gave me access after all 👍
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.
Damn, I forgot to investigate why these are needed... 😬
That unused macro rule warning is new. Feel free to comment out that rule if you don't happen to need it. |
You mean
I am not getting that locally. When I set Rustflatgs -D warnings, the compiler is fine. Maybe because of different rust versions? I'm on a not totally current nightly by default. Oh, that test was on nightly as well. But a fresher nightly I guess |
Exactly, this is on the latest nightly, so it's on the horizon. I probably just copy-pasted that macro, so the rule was either necessary for something else or no longer in use. |
There is a more general problem with the Ok colors: Instead of Bjorn Ottoson's oklab_to_linear_srgb functions, I'm using palette's conversions. In each Oklab -> Okhsl/v conversion, there is an additional conversion in the Would you like to investigate the source of the difference? Should I replace pallete's conversions by oklab_to_linear_srgb? |
It should be possible to replace it, so it's more optimal. If you exclude impl<S, T> FromColorUnclamped<Rgb<S, T>> for Oklab<T>
where
// ...
{
fn from_color_unclamped(rgb: Rgb<S, T>) -> Self {
if TypeId::of::<<S as RgbStandard>::Space>() == TypeId::of::<Srgb>() {
// Use direct sRGB to Oklab conversion
} else {
// Convert via XYZ
}
}
} It's not beautiful but it's as good as it gets without specialization. Alternatively, it could be generalized to work with any RGB gamut, unless the process gets super complicated. |
With the current nightly I see the error, too. And I've commented the macro rule out. But when running the tests one fails with
What should I do about that? |
Ouch, I don't think that macro is exported. You can add |
I've never worked with TypeId like that. How do I cast? With the following I get a non-primitive cast error: impl<S, T> FromColorUnclamped<Rgb<S, T>> for Oklab<T>
where
T: Real + Cbrt + Arithmetics + FromScalar + Copy,
T::Scalar: Recip
+ IsValidDivisor<Mask = bool>
+ Arithmetics
+ FromScalar<Scalar = T::Scalar>
+ Real
+ Zero
+ One
+ Clone,
S: RgbStandard,
S::TransferFn: IntoLinear<T, T>,
S::Space: RgbSpace<WhitePoint = D65> + 'static,
<S::Space as RgbSpace>::Primaries: Primaries<T::Scalar>,
{
fn from_color_unclamped(rgb: Rgb<S, T>) -> Self {
if TypeId::of::<<S as RgbStandard>::Space>() == TypeId::of::<Srgb>() {
// Use direct sRGB to Oklab conversion
linear_srgb_to_oklab(rgb as LinSrgb<T>)
} else {
// Convert via XYZ
Xyz::from_color_unclamped(rgb).into_color_unclamped()
}
}
} |
It's not exactly what |
…th reference implementation (and possibly higher precision) * replace macro implementations of equality with tolerances (abbs_diff_eq, relative_eq, ulps_eq) by manual implementations that care about semantic equality (e.g. black is black no matter the unused hue number) * comment out broken doc-test in lazy_select.rs, and unnecessary branches of impl_uint_casts_other and impl_eq macros
…th reference implementation (and possibly higher precision) * replace macro implementations of equality with tolerances (abbs_diff_eq, relative_eq, ulps_eq) by manual implementations that care about semantic equality (e.g. black is black no matter the unused hue number) * comment out broken doc-test in lazy_select.rs, and unnecessary branches of impl_uint_casts_other and impl_eq macros
This push highlights a problem with ´f32:´ At least for the color blue (#0000ff) the precision is insufficient to detect the correct path for the conversion to Okhsv. See ´okhsv::tests::blue´ I have contacted Björn Ottoson about this via twitter on how to handle this. |
Thanks a lot for looking deeper into it! |
# Conflicts: # palette/src/oklab.rs
@bottosson I have written to you via Twitter about the issue with f32. (I used @projekt_kultur there). If you happen to have the time, please take a look. @Ogeon Alternatively we could do all computations that might have accuracy problems in f64, no matter the formal type. What do you think? |
The tests failing in CI now, are because of the f32 issue. |
Yes and I see it's not a small error. I would of course prefer if we can keep it fully generic if possible, but |
I get the exact same value as the reference implementation (when I set In my (very limited understanding) the To cope with limited precision the algorithm would either need to compare >=, or use an epsilon tolerance (like the abs_diff_eq macro) or actually compute all three equations for all three colors and go with the largest. I think the last option would be preferable, but I don't know enough about color-math to compute the equation. Neither do I know enough to tell if using an epsilon-tolerance wouldn't break anything. I believe the reference implementation hides the error unintentionally, by doing just one optimization round. When you do more than one (my implementation currently does 2), the error becomes very visible, as the error does not lie with the optimization but with the optimization goal, which is decided before, when choosing the critical color. |
I see, but that's promising! I think we can go with one iteration for now, so it matches the reference, and return to it later if there's any response to your questions. I'm more than happy with something that just replicates the reference/specification as a first version, since it's usually also easier to verify if it doesn't diverge. Have you seen any particular issues that using 2 iterations over 1 solved? Other than a generally larger error. |
A comment in the reference implementation is
I think Björn Ottoson saw the problem, but attributed it erroneously to the optimization process. Going by his recommendation I added iterations. But now I believe it is not an optimization problem and the single optimization step accidentally just hides the root problem. I do believe 2 iterations do warrant the additional computational expense over a single iteration (40 multiplications+1 division), even for benign hues. But of course that is a matter of opinion and depends on the use case. Personally I prefer using f64 to get a correct solution. But I can reduce the number of iterations to 1, to match the reference implementation. What should I do with the tests? Should I just comment out / remove the failing cases to make CI happy? |
Going with what the reference does now doesn't prevent us from improving it later. But that said, I'm also open for other workarounds. Are the reference values from the C++ implementation? Or the online color picker? The color picker uses 64 bit floats, since it's JavaScript, so some small differences are expected. But it's also running only one iteration. We can run those test with |
...and tests with both |
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.
I didn't get as much time as I wanted, so I will have to look at the rest tomorrow. But so far, so good and thanks for organizing things into smaller files. The file lengths have been a bit annoying but I never got around to do much to shorten them (only adding macros here and there).
For Oklab
, it seem like its ranges were originally based on a misunderstanding and I think it's good to be explicit about the lack of well defined bounds. I'm fine with removing some random generation and validation that assume otherwise.
I'm a bit indecisive regarding what I would like to be public or not, so I'm just letting things pass for now. I tend to avoid exposing circumstantial or single-use parts, but I'll look through that later. Probably after merging.
I'll continue looking through this tomorrow. Again feel free to remind me if I forget. Also, don't hesitate to comment here in the code if I have missed anything you have questions for or want me to have a closer look at.
palette/src/num.rs
Outdated
|
||
/// Returns the negative infinity (−∞). | ||
#[must_use] | ||
fn neg_inf() -> Self; | ||
|
||
/// Returns the infinity (∞). | ||
#[must_use] | ||
fn inf() -> Self; |
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.
The idea with this trait is to not be float specific, so it may allow types that can't represent infinity. Would it be an issue to either rename these methods to something like get_min/get_max or move them to a separate trait for types with infinity representations?
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.
If these are just for the Oklab
limits, I'm more willing to drop the limits.
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.
It's also the upper limit for the chroma of Oklch (technically they are also for CIELab and CIELch -- "The a* and b* axes are unbounded, and depending on the reference white they can easily exceed ±150 to cover the human gamut. Nevertheless, software implementations often clamp these values for practical reasons." ). The failed test in CI is exactly about the chroma of Oklch. A last-minute change I made and forgot to rerun the test for.
The doc of Real
says "Numbers that belong to the real number set." How is that not float specific? Do you plan to include complex numbers? Or special kinds of real numbers that are finite only? In the later case, a trait like the following would make sense
/// Methods for the finite limits of the numbers.
pub trait Limits{
/// Returns a finite number, for which `x <= Self::max_value()` holds for all comparable `x`.
fn max_value() -> Self;
/// Returns a finite number, for which `x >= Self::min_value()` holds for all comparable `x`.
fn min_value() -> Self;
}
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.
Oklch and Oklab are more or less just different coordinate systems in the same space, so I suppose what goes for one goes for the other. Unless there's some definition that says otherwise. I think CIELab has a bit of an exception in that each axis is expected to fit in an i8
when rounded. CIELch is a bit more difficult to nail down, but the radii are based on the CIELab square.
The doc of Real says "Numbers that belong to the real number set." How is that not float specific? Do you plan to include complex numbers?
I'm referring to the mathematical real number set, which doesn't include infinity as far as I know. But it's less about number sets and more about widening the set of allowed types to anything that can be used as a substitute for floats. Not all of them has a representation of infinity, so this particular trait is very minimal.
A Limits
trait could be useful but I prefer to first question why an unbounded color space has bounds. 🤔
palette/src/oklab.rs
Outdated
pub fn min_a() -> T { | ||
T::from_f64(-1.0) | ||
T::neg_inf() | ||
} | ||
|
||
/// Return the `a` value maximum. | ||
pub fn max_a() -> T { | ||
T::from_f64(1.0) | ||
T::inf() | ||
} | ||
|
||
/// Return the `b` value minimum. | ||
pub fn min_b() -> T { | ||
T::from_f64(-1.0) | ||
T::neg_inf() | ||
} | ||
|
||
/// Return the `b` value maximum. | ||
pub fn max_b() -> T { | ||
T::from_f64(1.0) | ||
T::inf() | ||
} |
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.
I'm thinking of dropping these completely if there's no well defined range for them. The idea is mostly to use them for validation or calculations that need a min/max, so infinity doesn't give much anymore.
T: RealAngle + Zero + Arithmetics + Trigonometry + Clone + PartialEq, | ||
{ | ||
/// Returns the hue, if at least one of `a` and `b` are non-zero | ||
pub fn try_hue(&self) -> Option<OklabHue<T>> { |
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.
This could be neat to have in the Hue
trait. I'll think about that one. 🤔
palette/src/oklab/random.rs
Outdated
// `a` and `b` both range from (-1.0, 1.0) | ||
// TODO: the choice for a and b is random and rather bad: | ||
// 1. a and b are unlimited. Oklab can express the whole electro-magnetic spectrum | ||
// 2. Oklab is a perceptual color space. It would make sense to limit random | ||
// values to the limits of human perception. | ||
// https://bottosson.github.io/posts/oklab/#luo-rigg-dataset-and-full-gamut | ||
// shows, that at least for some L, a should not be greater than 0.5, to | ||
// avoid leaving the perceivable gamut. Though it could go to -2.5. | ||
// 3. If people want random sRGB values: Expressing the sRGB bounds in Oklab is | ||
// beyond my abilities. However, it is not necessary either. It can be done in | ||
// Okhsv, Okhsl or Okhwb. Maybe we should not offer the ability in Oklab and | ||
// encourage sampling in the color spaces that are limited to sRGB gamut. |
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.
These are very good points. I don't have it fresh in mind at the moment but there is an option to sample a specific range. I think that still makes sense to keep, since I don't think there's any reason to prevent that. If a default alternative is still necessary (again, I would have to take a look later), this could be kept as is, since it allows easy scaling to other ranges by multiplication. Similar to a random function returning 0.0-1.0.
* Use impl_eq macro for Oklab instead of impl_eq_hue
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.
Ok, I hope I got all of it. Let me know if I missed anything. So the general points of improvements are:
- See if we can avoid pretending that Lab and Lch colors have bounds in the chroma plane. May make the (de)saturate traits a bit sad.
- Remove any trait bounds that aren't strictly necessary for their
impl
blocks and functions. Makes things more flexible and allowsconst
functions. - Trim/postpone any extra traits (and functions I suppose), if possible, to focus on the Ok* types in this PR. Helps with reviewing and getting this off the ground.
After that, and this has been merged, it's easier to focus on expanding the API where it's useful.
palette/src/rgb/rgb.rs
Outdated
// fixme: why does the direct conversion produce relevant differences | ||
// to the conversion via XYZ? |
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.
Do you think we need to investigate this before pushing the button? My guess is that it's just due to it being better code for the situation. Probably fewer steps too.
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.
Its entirely possible, that it is just due to rounding errors, but I don't have more than a guess here, either.
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.
That's likely a contributing factor. The conversion via XYZ doesn't use pre-defined matrices (yet), so that's a difference I know of.
Well, I'm happy that it's better, regardless. I think this bit of the comment can be removed. I also prefer to have remaining todos/fixmes in the issue tracker, rather than in code, so I would like to make sure there are none left.
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.
I also prefer to have remaining todos/fixmes in the issue tracker, rather than in code, so I would like to make sure there are none left.
There are two issues, that I can see in the TODOs/FIXMEs
- The range of colors created for distributions of Oklab and Oklch. We should resolve this before we publish the PR. And the FIXMEs/TODOs won't be necessary then.
- The erroneous conversion using f32 on some CPUs (or maybe just on my machine, maybe all AMD CPUs) , that I need @bottosson 's guidance for. This has one FIXME/TODO in the code and two in the tests. I think they need to stay in the tests to avoid a misleading appearance of the tests and to be able to find the relevant tests, if and when we find a way to correct the error. As long as there is no fix, maybe we should elevate the FIXME to a warning in the documentation or even a trait bound, that allows only
f64
, as the error might be catastrophic for some library users.
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.
Yes we definitely need to do something about point 1. In the (likely, it seems) case that we don't get a response for point 2, we should add a note in the documentation about it being sensitive to precision. It could be that it's not important for everyone, but it's a good idea to be a bit proactive with a warning that explains the phenomenon a bit.
impl<T> FromColorUnclamped<Okhsv<T>> for Xyz<D65, T> | ||
where | ||
Okhsv<T>: IntoColorUnclamped<Oklab<T>>, | ||
Self: FromColorUnclamped<Oklab<T>>, | ||
{ | ||
fn from_color_unclamped(color: Okhsv<T>) -> Self { | ||
let oklab: Oklab<T> = color.into_color_unclamped(); | ||
Self::from_color_unclamped(oklab) | ||
} | ||
} | ||
|
||
impl<T> FromColorUnclamped<Okhsl<T>> for Xyz<D65, T> | ||
where | ||
Okhsl<T>: IntoColorUnclamped<Oklab<T>>, | ||
Self: FromColorUnclamped<Oklab<T>>, | ||
{ | ||
fn from_color_unclamped(color: Okhsl<T>) -> Self { | ||
let oklab: Oklab<T> = color.into_color_unclamped(); | ||
Self::from_color_unclamped(oklab) | ||
} | ||
} | ||
|
||
impl<T> FromColorUnclamped<Okhwb<T>> for Xyz<D65, T> | ||
where | ||
Okhwb<T>: IntoColorUnclamped<Okhsv<T>>, | ||
Self: FromColorUnclamped<Okhsv<T>>, | ||
{ | ||
fn from_color_unclamped(color: Okhwb<T>) -> Self { | ||
let okhsv: Okhsv<T> = color.into_color_unclamped(); | ||
Self::from_color_unclamped(okhsv) | ||
} | ||
} |
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.
Damn, I forgot to investigate why these are needed... 😬
* limit VisualColor and VisuallyEqual traits to testing, move their implementations to separate files and also limit them to testing * remove neg_inf and inf methods from Real trait and add separate traits LowerBounded and UpperBounded * Remove more superfluous trait bounds -- specially Real
I think the questions we need to answer when it comes to the Lab and Lch colors are these:
For clamping and validation, we can just not check the upper bound for Lch, so I see those as more or less non-issues. If there is some kind of conventional limit, we could lean on those (like with the CIE colors). Another idea is to use something like the sRGB gamut as a limit, but I don't know if that's a feasible idea. |
These questions and problems share common sources in implicit assumptions: Let me illustrate the problem with the current saturation trait : A call to This formula has the assumptions:
These assumptions hold for (Ok)hsv and (Ok)hsl, but not for Ok/CIELab and Ok/CIELch. Ok)hsv and (Ok)hsl refer to sRGB -- a display-dependent color space and thus bounded color space, that has a non-linear, but bijective conversion. Ok/CIELab and Ok/CIELch are display-independent. Formally they are infinite. Materially they refer to the perceivable gamut. The common solution for CIELab is to limit the color space to the perceivable gamut and even ignoring some part of it, as as that part is rarely used anyway. Limiting the color space to the perceivable gamut makes sense. Also, assumption 3 does not hold. More generally, I think the traits themselves have the problematic, implicit assumption, that
This assumption becomes most obvious when the material color space is display-independent: Some users will want to generate random colors in sRGB, others in Display P3. CSS Color 4 also suggests Prophoto, REC.2020 and Adobe 1998 RGB. Thus from the perspective of the ideal API (not from the perspective of what I am able to implement) there should not be a single saturation trait(s) and a single random generator or a single clamp or gamut-map trait. There should be one for each display-dependent color space and every color space should implement all traits for its maximum as well as all smaller color spaces. I guess the smallest being sRGB. Obviously I have strayed from the question of how to implement Oklab to the general library design. But the two are connected. |
You are absolutely right and hit the nail on the head when it comes to what makes this difficult. The fundamental assumptions do not match these spaces. These traits are simplistic tools, so let's not try to make them do more than they are made for. Let's cut anything that requires substantial re-design of the API. That's for another time. If it doesn't fit the trait's model, it doesn't implement it. The (de)saturation traits assume a finite range for the relative method. The absolute method could perhaps be implemented if we settle on a reasonable scale. Their visual uniformity helps here too. For the sake of this PR, we can just say that we don't implement them for now, and later add them again if there's a practical way of doing it. Probably by splitting the traits into relative and absolute variants. The idea with the random generation is to be a bit better than just randomly sampling each parameter, just so whatever they get looks somewhat evenly distributed (pretty much a non-issue for Cartesian spaces, though). I think something like sampling a well specified space by default (regardless of how much it covers) has some value on its own. It's a foundation for something more advanced, like, perhaps rejection sampling. A tool, rather than a definition. That's why I suggested sampling 0 to 1 or -1 to 1 and letting the user scale them to their liking. For CIELab (and CIELch), I'm still inclined to keep it as it was, just because it has its conventional |
So my remaining todo list is
Other open questions:
|
You can remove the infinite bounds and skip checking them when clamping. It's fine to just cap the numbers without taking gamuts into account. That would require something more customizable. Lightness has a conventional upper bound (the lightness of white) so clamping it to that would be expected from the trait. Lighten/darken can use the same upper bound. Once again, it's a simplistic tool. Arithmetics with colors are there as a way of performing some operation on all components at once. More like SIMD. For example, you can average two colors with just Premultiplying is alpha masking, so it's a specific application for multiplication. You can see that it has its own wrapper type that keeps track of whether a color is masked or not. |
* Remove max_chroma saturation implementation and upper bound clamping for Oklch * Remove Bounded traits
* Fix approximate implementation for hues
I think I made all the changes. Did I miss something, @Ogeon? |
Hey, sorry for being bad at responding! I just haven't had a good moment to take a proper look. I also think this will be all for this PR so you can relax and I'll message you here if there happen to be anything more. Thank you so much for your time, effort, and insights and for putting up with my questions and opinions! It's not always easy to balance API design with correctness, so I'm happy that things turned out as well as they did and that we found some good points of improvement. And many, if not most, of the things we had to compromise on are also fixable but as subjects for future PRs. |
Hi @Ogeon , I'm building a vector graphics editor and I'm using palette for color picking and conversion. For now, the color picker uses HSLuv, but I'd like to switch Okhsl, so I'd be really interested to have this PR merged. What's missing to merge it? Can I help in any way? |
Hi, thanks a lot for asking and offering to help! I have been taking a break from programming outside work for a while, so that had been the general blocker right now. For this PR, I think the only thing I wanted to investigate before merging is how to make the derive macro properly support the added color spaces. The rest should be all good, excluding potential API polishing. I'm not sure I will be back at this just yet, considering I have stuff to do before the winter holidays, but would merging this as it is unblock you for now? Or is it necessary to also have it published on crates.io? |
Thanks for your quick reply! Merging it as is sounds good. I'm using palette 0.6 currently, but switching to master in the short term should be fine. I'm going to try it. If you can publish a new palette version later in the winter, that would be great too. What is the issue with the derive macro? Is it related to Clone and Copy being declared by Okhsl, Okhsv and Okhwb? |
Sorry to follow with a slow response, but I have been sick for the past few days and didn't have the required brain juice. It's getting better, though!
That sounds great! I can merge this as it is. It's doing what it's supposed to do, it's really just the macro that has some hole in the logic. Feel free to report any issues you find too! It's nice to catch them before releasing for real.
That sounds like a good target. I can't promise an ETA, but I think it's a good idea to cut this release short and get what's there through the door. I just need to check that the new things are in good shape as they are. I had initially intended to at least finish the trait rework too, but some of them are a bit tricky and I don't want to block other things for too long.
It doesn't generate the conversions to |
bors r+ |
Build succeeded: |
Thanks for the merge! I'm looking forward to integrate Okhsl next week and see how it goes. I don't have suggestions for the macro issues, I'm quite new to Rust, but I get what the problem is now. |
@Ogeon I didn't find the time to integrate Okhsl until this week. Now it's done and everything went smoothly 👍 I need to test color conversions a bit more and let you know if I find any issues. Thanks again for the quick merge! Palette is a great library! |
Awesome! Nice that it seem to be all good so far 🎉 |
Closed Issues
Breaking Change
The updated matrix will change colors, if those colors have been selected visually (e.g. using a hsl color picker) and stored as Oklab. If they have been stored in any other format, a transformation to Oklab should produce more correct colors.