Skip to content

Commit 6d81a7a

Browse files
committed
Fix Oklab from Oklch hue conversion
The Oklch -> Oklab conversion inverted the hue by swapping the a and b components. This corrects the calculation and adds an Oklch -> Oklab -> Oklch roundtrip test.
1 parent 9bc1fd3 commit 6d81a7a

File tree

2 files changed

+76
-4
lines changed

2 files changed

+76
-4
lines changed

palette/src/oklab.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -357,13 +357,13 @@ where
357357
T: RealAngle + Zero + MinMax + Trigonometry + Mul<Output = T> + Clone,
358358
{
359359
fn from_color_unclamped(color: Oklch<T>) -> Self {
360-
let (sin_hue, cos_hue) = color.hue.into_cartesian();
360+
let (a, b) = color.hue.into_cartesian();
361361
let chroma = color.chroma.max(T::zero());
362362

363363
Oklab {
364364
l: color.l,
365-
a: cos_hue * chroma.clone(),
366-
b: sin_hue * chroma,
365+
a: a * chroma.clone(),
366+
b: b * chroma,
367367
}
368368
}
369369
}

palette/src/oklch.rs

+73-1
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,82 @@ unsafe impl<T> bytemuck::Pod for Oklch<T> where T: bytemuck::Pod {}
160160

161161
#[cfg(test)]
162162
mod test {
163-
use crate::Oklch;
163+
use crate::convert::FromColorUnclamped;
164+
use crate::rgb::Rgb;
165+
use crate::visual::{VisualColor, VisuallyEqual};
166+
use crate::{encoding, LinSrgb, Oklab, Oklch};
164167

165168
test_convert_into_from_xyz!(Oklch);
166169

170+
#[cfg_attr(miri, ignore)]
171+
#[test]
172+
fn test_roundtrip_oklch_oklab_is_original() {
173+
let colors = [
174+
(
175+
"red",
176+
Oklab::from_color_unclamped(LinSrgb::new(1.0, 0.0, 0.0)),
177+
),
178+
(
179+
"green",
180+
Oklab::from_color_unclamped(LinSrgb::new(0.0, 1.0, 0.0)),
181+
),
182+
(
183+
"cyan",
184+
Oklab::from_color_unclamped(LinSrgb::new(0.0, 1.0, 1.0)),
185+
),
186+
(
187+
"magenta",
188+
Oklab::from_color_unclamped(LinSrgb::new(1.0, 0.0, 1.0)),
189+
),
190+
(
191+
"black",
192+
Oklab::from_color_unclamped(LinSrgb::new(0.0, 0.0, 0.0)),
193+
),
194+
(
195+
"grey",
196+
Oklab::from_color_unclamped(LinSrgb::new(0.5, 0.5, 0.5)),
197+
),
198+
(
199+
"yellow",
200+
Oklab::from_color_unclamped(LinSrgb::new(1.0, 1.0, 0.0)),
201+
),
202+
(
203+
"blue",
204+
Oklab::from_color_unclamped(LinSrgb::new(0.0, 0.0, 1.0)),
205+
),
206+
(
207+
"white",
208+
Oklab::from_color_unclamped(LinSrgb::new(1.0, 1.0, 1.0)),
209+
),
210+
];
211+
212+
const EPSILON: f64 = 1e-14;
213+
214+
for (name, color) in colors {
215+
let rgb: Rgb<encoding::Srgb, u8> =
216+
crate::Srgb::<f64>::from_color_unclamped(color).into_format();
217+
println!(
218+
"\n\
219+
roundtrip of {} (#{:x} / {:?})\n\
220+
=================================================",
221+
name, rgb, color
222+
);
223+
224+
println!("Color is white: {}", color.is_white(EPSILON));
225+
226+
let oklch = Oklch::from_color_unclamped(color);
227+
println!("Oklch: {:?}", oklch);
228+
let roundtrip_color = Oklab::from_color_unclamped(oklch);
229+
assert!(
230+
Oklab::visually_eq(roundtrip_color, color, EPSILON),
231+
"'{}' failed.\n{:?}\n!=\n{:?}",
232+
name,
233+
roundtrip_color,
234+
color
235+
);
236+
}
237+
}
238+
167239
#[test]
168240
fn ranges() {
169241
// chroma: 0.0 => infinity

0 commit comments

Comments
 (0)