1use core::{any::TypeId, fmt::Debug, ops::Mul};
4
5pub use alpha::Oklaba;
6
7use crate::{
8 angle::RealAngle,
9 bool_mask::HasBoolMask,
10 convert::{FromColorUnclamped, IntoColorUnclamped},
11 encoding::{IntoLinear, Srgb},
12 matrix::multiply_xyz,
13 num::{Arithmetics, Cbrt, Hypot, MinMax, One, Powi, Real, Sqrt, Trigonometry, Zero},
14 ok_utils::{toe_inv, ChromaValues, LC, ST},
15 rgb::{Rgb, RgbSpace, RgbStandard},
16 white_point::D65,
17 LinSrgb, Mat3, Okhsl, Okhsv, Oklch, Xyz,
18};
19
20pub use self::properties::Iter;
21
22#[cfg(feature = "random")]
23pub use self::random::UniformOklab;
24
25mod alpha;
26mod properties;
27#[cfg(feature = "random")]
28mod random;
29#[cfg(test)]
30#[cfg(feature = "approx")]
31mod visual_eq;
32
33#[rustfmt::skip]
41fn m1<T: Real>() -> Mat3<T> {
42 [
43 T::from_f64(0.8190224432164319), T::from_f64(0.3619062562801221), T::from_f64(-0.12887378261216414),
44 T::from_f64(0.0329836671980271), T::from_f64(0.9292868468965546), T::from_f64(0.03614466816999844),
45 T::from_f64(0.048177199566046255), T::from_f64(0.26423952494422764), T::from_f64(0.6335478258136937),
46 ]
47}
48
49#[rustfmt::skip]
51pub(crate) fn m1_inv<T: Real>() -> Mat3<T> {
52 [
53 T::from_f64(1.2268798733741557), T::from_f64(-0.5578149965554813), T::from_f64(0.28139105017721583),
54 T::from_f64(-0.04057576262431372), T::from_f64(1.1122868293970594), T::from_f64(-0.07171106666151701),
55 T::from_f64(-0.07637294974672142), T::from_f64(-0.4214933239627914), T::from_f64(1.5869240244272418),
56 ]
57}
58
59#[rustfmt::skip]
61fn m2<T: Real>() -> Mat3<T> {
62 [
63 T::from_f64(0.2104542553), T::from_f64(0.7936177850), T::from_f64(-0.0040720468),
64 T::from_f64(1.9779984951), T::from_f64(-2.4285922050), T::from_f64(0.4505937099),
65 T::from_f64(0.0259040371), T::from_f64(0.7827717662), T::from_f64(-0.8086757660),
66 ]
67}
68
69#[rustfmt::skip]
71#[allow(clippy::excessive_precision)]
72pub(crate) fn m2_inv<T: Real>() -> Mat3<T> {
73 [
74 T::from_f64(0.99999999845051981432), T::from_f64(0.39633779217376785678), T::from_f64(0.21580375806075880339),
75 T::from_f64(1.0000000088817607767), T::from_f64(-0.1055613423236563494), T::from_f64(-0.063854174771705903402),
76 T::from_f64(1.0000000546724109177), T::from_f64(-0.089484182094965759684), T::from_f64(-1.2914855378640917399),
77 ]
78}
79
80#[derive(Debug, Copy, Clone, ArrayCast, FromColorUnclamped, WithAlpha)]
176#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
177#[palette(
178 palette_internal,
179 white_point = "D65",
180 component = "T",
181 skip_derives(Oklab, Oklch, Okhsv, Okhsl, Xyz, Rgb)
182)]
183#[repr(C)]
184pub struct Oklab<T = f32> {
185 pub l: T,
193
194 pub a: T,
199
200 pub b: T,
206}
207
208impl<T> Oklab<T> {
209 pub const fn new(l: T, a: T, b: T) -> Self {
211 Self { l, a, b }
212 }
213
214 pub fn into_components(self) -> (T, T, T) {
216 (self.l, self.a, self.b)
217 }
218
219 pub fn from_components((l, a, b): (T, T, T)) -> Self {
221 Self::new(l, a, b)
222 }
223}
224
225impl<T> Oklab<T>
229where
230 T: Zero + One,
231{
232 pub fn min_l() -> T {
234 T::zero()
235 }
236
237 pub fn max_l() -> T {
239 T::one()
240 }
241}
242
243impl_reference_component_methods!(Oklab, [l, a, b]);
244impl_struct_of_arrays_methods!(Oklab, [l, a, b]);
245
246impl<T> Oklab<T>
247where
248 T: Hypot + Clone,
249{
250 pub(crate) fn get_chroma(&self) -> T {
252 T::hypot(self.a.clone(), self.b.clone())
253 }
254}
255
256impl<T> FromColorUnclamped<Oklab<T>> for Oklab<T> {
257 fn from_color_unclamped(color: Self) -> Self {
258 color
259 }
260}
261
262impl<T> FromColorUnclamped<Xyz<D65, T>> for Oklab<T>
263where
264 T: Real + Cbrt + Arithmetics,
265{
266 fn from_color_unclamped(color: Xyz<D65, T>) -> Self {
267 let m1 = m1();
268 let m2 = m2();
269
270 let Xyz {
271 x: l, y: m, z: s, ..
272 } = multiply_xyz(m1, color.with_white_point());
273
274 let l_m_s_ = Xyz::new(l.cbrt(), m.cbrt(), s.cbrt());
275
276 let Xyz {
277 x: l, y: a, z: b, ..
278 } = multiply_xyz(m2, l_m_s_);
279
280 Self::new(l, a, b)
281 }
282}
283
284fn linear_srgb_to_oklab<T>(c: LinSrgb<T>) -> Oklab<T>
285where
286 T: Real + Arithmetics + Cbrt + Copy,
287{
288 let l = T::from_f64(0.4122214708) * c.red
289 + T::from_f64(0.5363325363) * c.green
290 + T::from_f64(0.0514459929) * c.blue;
291 let m = T::from_f64(0.2119034982) * c.red
292 + T::from_f64(0.6806995451) * c.green
293 + T::from_f64(0.1073969566) * c.blue;
294 let s = T::from_f64(0.0883024619) * c.red
295 + T::from_f64(0.2817188376) * c.green
296 + T::from_f64(0.6299787005) * c.blue;
297
298 let l_ = l.cbrt();
299 let m_ = m.cbrt();
300 let s_ = s.cbrt();
301
302 Oklab::new(
303 T::from_f64(0.2104542553) * l_ + T::from_f64(0.7936177850) * m_
304 - T::from_f64(0.0040720468) * s_,
305 T::from_f64(1.9779984951) * l_ - T::from_f64(2.4285922050) * m_
306 + T::from_f64(0.4505937099) * s_,
307 T::from_f64(0.0259040371) * l_ + T::from_f64(0.7827717662) * m_
308 - T::from_f64(0.8086757660) * s_,
309 )
310}
311
312pub(crate) fn oklab_to_linear_srgb<T>(c: Oklab<T>) -> LinSrgb<T>
313where
314 T: Real + Arithmetics + Copy,
315{
316 let l_ = c.l + T::from_f64(0.3963377774) * c.a + T::from_f64(0.2158037573) * c.b;
317 let m_ = c.l - T::from_f64(0.1055613458) * c.a - T::from_f64(0.0638541728) * c.b;
318 let s_ = c.l - T::from_f64(0.0894841775) * c.a - T::from_f64(1.2914855480) * c.b;
319
320 let l = l_ * l_ * l_;
321 let m = m_ * m_ * m_;
322 let s = s_ * s_ * s_;
323
324 LinSrgb::new(
325 T::from_f64(4.0767416621) * l - T::from_f64(3.3077115913) * m
326 + T::from_f64(0.2309699292) * s,
327 T::from_f64(-1.2684380046) * l + T::from_f64(2.6097574011) * m
328 - T::from_f64(0.3413193965) * s,
329 T::from_f64(-0.0041960863) * l - T::from_f64(0.7034186147) * m
330 + T::from_f64(1.7076147010) * s,
331 )
332}
333
334impl<S, T> FromColorUnclamped<Rgb<S, T>> for Oklab<T>
335where
336 T: Real + Cbrt + Arithmetics + Copy,
337 S: RgbStandard,
338 S::TransferFn: IntoLinear<T, T>,
339 S::Space: RgbSpace<WhitePoint = D65> + 'static,
340 Xyz<D65, T>: FromColorUnclamped<Rgb<S, T>>,
341{
342 fn from_color_unclamped(rgb: Rgb<S, T>) -> Self {
343 if TypeId::of::<<S as RgbStandard>::Space>() == TypeId::of::<Srgb>() {
344 linear_srgb_to_oklab(rgb.into_linear().reinterpret_as())
348 } else {
349 Xyz::from_color_unclamped(rgb).into_color_unclamped()
351 }
352 }
353}
354
355impl<T> FromColorUnclamped<Oklch<T>> for Oklab<T>
356where
357 T: RealAngle + Zero + MinMax + Trigonometry + Mul<Output = T> + Clone,
358{
359 fn from_color_unclamped(color: Oklch<T>) -> Self {
360 let (a, b) = color.hue.into_cartesian();
361 let chroma = color.chroma.max(T::zero());
362
363 Oklab {
364 l: color.l,
365 a: a * chroma.clone(),
366 b: b * chroma,
367 }
368 }
369}
370
371impl<T> FromColorUnclamped<Okhsl<T>> for Oklab<T>
374where
375 T: RealAngle
376 + One
377 + Zero
378 + Arithmetics
379 + Sqrt
380 + MinMax
381 + PartialOrd
382 + HasBoolMask<Mask = bool>
383 + Powi
384 + Cbrt
385 + Trigonometry
386 + Clone,
387 Oklab<T>: IntoColorUnclamped<LinSrgb<T>>,
388{
389 fn from_color_unclamped(hsl: Okhsl<T>) -> Self {
390 let h = hsl.hue;
391 let s = hsl.saturation;
392 let l = hsl.lightness;
393
394 if l == T::one() {
395 return Oklab::new(T::one(), T::zero(), T::zero());
396 } else if l == T::zero() {
397 return Oklab::new(T::zero(), T::zero(), T::zero());
398 }
399
400 let (a_, b_) = h.into_cartesian();
401 let oklab_lightness = toe_inv(l);
402
403 let cs = ChromaValues::from_normalized(oklab_lightness.clone(), a_.clone(), b_.clone());
404
405 let mid = T::from_f64(0.8);
411 let mid_inv = T::from_f64(1.25);
412
413 let chroma = if s < mid {
414 let t = mid_inv * s;
415
416 let k_1 = mid * cs.zero;
417 let k_2 = T::one() - k_1.clone() / cs.mid;
418
419 t.clone() * k_1 / (T::one() - k_2 * t)
420 } else {
421 let t = (s - &mid) / (T::one() - &mid);
422
423 let k_0 = cs.mid.clone();
424 let k_1 = (T::one() - mid) * &cs.mid * &cs.mid * &mid_inv * mid_inv / cs.zero;
425 let k_2 = T::one() - k_1.clone() / (cs.max - cs.mid);
426
427 k_0 + t.clone() * k_1 / (T::one() - k_2 * t)
428 };
429
430 Oklab::new(oklab_lightness, chroma.clone() * a_, chroma * b_)
431 }
432}
433
434impl<T> FromColorUnclamped<Okhsv<T>> for Oklab<T>
435where
436 T: RealAngle
437 + PartialOrd
438 + HasBoolMask<Mask = bool>
439 + MinMax
440 + Powi
441 + Arithmetics
442 + Clone
443 + One
444 + Zero
445 + Cbrt
446 + Trigonometry,
447 Oklab<T>: IntoColorUnclamped<LinSrgb<T>>,
448{
449 fn from_color_unclamped(hsv: Okhsv<T>) -> Self {
450 if hsv.value == T::zero() {
451 return Self {
453 l: T::zero(),
454 a: T::zero(),
455 b: T::zero(),
456 };
457 }
458
459 if hsv.saturation == T::zero() {
460 let l = toe_inv(hsv.value);
462 return Self {
463 l,
464 a: T::zero(),
465 b: T::zero(),
466 };
467 }
468
469 let h_radians = hsv.hue.into_raw_radians();
470 let a_ = T::cos(h_radians.clone());
471 let b_ = T::sin(h_radians);
472
473 let cusp = LC::find_cusp(a_.clone(), b_.clone());
474 let cusp: ST<T> = cusp.into();
475 let s_0 = T::from_f64(0.5);
476 let k = T::one() - s_0.clone() / cusp.s;
477
478 let l_v = T::one()
482 - hsv.saturation.clone() * s_0.clone()
483 / (s_0.clone() + &cusp.t - cusp.t.clone() * &k * &hsv.saturation);
484 let c_v =
485 hsv.saturation.clone() * &cusp.t * &s_0 / (s_0 + &cusp.t - cusp.t * k * hsv.saturation);
486
487 let l_vt = toe_inv(l_v.clone());
489 let c_vt = c_v.clone() * &l_vt / &l_v;
490
491 let mut lightness = hsv.value.clone() * l_v;
492 let mut chroma = hsv.value * c_v;
493 let lightness_new = toe_inv(lightness.clone());
494 chroma = chroma * &lightness_new / lightness;
495 let rgb_scale: LinSrgb<T> =
497 Oklab::new(l_vt, a_.clone() * &c_vt, b_.clone() * c_vt).into_color_unclamped();
498 let lightness_scale_factor = T::cbrt(
499 T::one()
500 / T::max(
501 T::max(rgb_scale.red, rgb_scale.green),
502 T::max(rgb_scale.blue, T::zero()),
503 ),
504 );
505
506 lightness = lightness_new * &lightness_scale_factor;
507 chroma = chroma * lightness_scale_factor;
508
509 Oklab::new(lightness, chroma.clone() * a_, chroma * b_)
510 }
511}
512
513impl_tuple_conversion!(Oklab as (T, T, T));
514
515impl<T> HasBoolMask for Oklab<T>
516where
517 T: HasBoolMask,
518{
519 type Mask = T::Mask;
520}
521
522impl<T> Default for Oklab<T>
523where
524 T: Zero,
525{
526 fn default() -> Self {
527 Self::new(T::zero(), T::zero(), T::zero())
528 }
529}
530
531#[cfg(feature = "bytemuck")]
532unsafe impl<T> bytemuck::Zeroable for Oklab<T> where T: bytemuck::Zeroable {}
533
534#[cfg(feature = "bytemuck")]
535unsafe impl<T> bytemuck::Pod for Oklab<T> where T: bytemuck::Pod {}
536
537#[cfg(test)]
538mod test {
539 use crate::Oklab;
540
541 test_convert_into_from_xyz!(Oklab);
542
543 #[cfg(feature = "approx")]
544 mod conversion {
545 use core::str::FromStr;
546
547 use crate::{
548 convert::FromColorUnclamped, rgb::Rgb, visual::VisuallyEqual, white_point::D65,
549 FromColor, Lab, LinSrgb, Oklab, Srgb,
550 };
551
552 #[test]
554 fn lightness_of_white_is_one() {
555 let rgb: Srgb<f64> = Rgb::from_str("#ffffff").unwrap().into_format();
556 let lin_rgb = LinSrgb::from_color_unclamped(rgb);
557 let oklab = Oklab::from_color_unclamped(lin_rgb);
558 println!("white {rgb:?} == {oklab:?}");
559 assert_abs_diff_eq!(oklab.l, 1.0, epsilon = 1e-7);
560 assert_abs_diff_eq!(oklab.a, 0.0, epsilon = 1e-7);
561 assert_abs_diff_eq!(oklab.b, 0.0, epsilon = 1e-7);
562
563 let lab: Lab<D65, f64> = Lab::from_components((100.0, 0.0, 0.0));
564 let rgb: Srgb<f64> = Srgb::from_color_unclamped(lab);
565 let oklab = Oklab::from_color_unclamped(lab);
566 println!("white {lab:?} == {rgb:?} == {oklab:?}");
567 assert_abs_diff_eq!(oklab.l, 1.0, epsilon = 1e-4);
568 assert_abs_diff_eq!(oklab.a, 0.0, epsilon = 1e-4);
569 assert_abs_diff_eq!(oklab.b, 0.0, epsilon = 1e-4);
570 }
571
572 #[test]
573 fn blue_srgb() {
574 let rgb: Srgb<f64> = Rgb::from_str("#0000ff").unwrap().into_format();
576 let lin_rgb = LinSrgb::from_color_unclamped(rgb);
577 let oklab = Oklab::from_color_unclamped(lin_rgb);
578
579 assert_abs_diff_eq!(oklab.l, 0.4520137183853429, epsilon = 1e-9);
582 assert_abs_diff_eq!(oklab.a, -0.03245698416876397, epsilon = 1e-9);
583 assert_abs_diff_eq!(oklab.b, -0.3115281476783751, epsilon = 1e-9);
584 }
585
586 #[test]
587 fn red() {
588 let a = Oklab::from_color(LinSrgb::new(1.0, 0.0, 0.0));
589 let b = Oklab::new(0.6279553606145516, 0.22486306106597395, 0.1258462985307351);
591 assert!(Oklab::visually_eq(a, b, 1e-8));
592 }
593
594 #[test]
595 fn green() {
596 let a = Oklab::from_color(LinSrgb::new(0.0, 1.0, 0.0));
597 let b = Oklab::new(
599 0.8664396115356694,
600 -0.23388757418790812,
601 0.17949847989672985,
602 );
603 assert!(Oklab::visually_eq(a, b, 1e-8));
604 }
605
606 #[test]
607 fn blue() {
608 let a = Oklab::from_color(LinSrgb::new(0.0, 0.0, 1.0));
609 println!("Oklab blue: {:?}", a);
610 let b = Oklab::new(0.4520137183853429, -0.0324569841687640, -0.3115281476783751);
612 assert!(Oklab::visually_eq(a, b, 1e-8));
613 }
614 }
615
616 #[cfg(feature = "approx")]
617 mod visually_eq {
618 use crate::{visual::VisuallyEqual, Oklab};
619
620 #[test]
621 fn black_eq_different_black() {
622 assert!(Oklab::visually_eq(
623 Oklab::new(0.0, 1.0, 0.0),
624 Oklab::new(0.0, 0.0, 1.0),
625 1e-8
626 ));
627 }
628
629 #[test]
630 fn white_eq_different_white() {
631 assert!(Oklab::visually_eq(
632 Oklab::new(1.0, 1.0, 0.0),
633 Oklab::new(1.0, 0.0, 1.0),
634 1e-8
635 ));
636 }
637
638 #[test]
639 fn white_ne_black() {
640 assert!(!Oklab::visually_eq(
641 Oklab::new(1.0, 1.0, 0.0),
642 Oklab::new(0.0, 0.0, 1.0),
643 1e-8
644 ));
645 assert!(!Oklab::visually_eq(
646 Oklab::new(1.0, 1.0, 0.0),
647 Oklab::new(0.0, 1.0, 0.0),
648 1e-8
649 ));
650 }
651
652 #[test]
653 fn non_bw_neq_different_non_bw() {
654 assert!(!Oklab::visually_eq(
655 Oklab::new(0.3, 1.0, 0.0),
656 Oklab::new(0.3, 0.0, 1.0),
657 1e-8
658 ));
659 }
660 }
661
662 #[test]
663 fn ranges() {
664 assert_ranges! {
665 Oklab<f64>;
666 clamped {
667 l: 0.0 => 1.0
668 }
670 clamped_min {}
671 unclamped {}
672 };
673 }
674
675 #[test]
676 fn check_min_max_components() {
677 assert_eq!(Oklab::<f32>::min_l(), 0.0);
678 assert_eq!(Oklab::<f32>::max_l(), 1.0);
679 }
680
681 struct_of_arrays_tests!(
682 Oklab[l, a, b],
683 super::Oklaba::new(0.1f32, 0.2, 0.3, 0.4),
684 super::Oklaba::new(0.2, 0.3, 0.4, 0.5),
685 super::Oklaba::new(0.3, 0.4, 0.5, 0.6)
686 );
687
688 #[cfg(feature = "serializing")]
689 #[test]
690 fn serialize() {
691 let serialized = ::serde_json::to_string(&Oklab::new(0.3, 0.8, 0.1)).unwrap();
692
693 assert_eq!(serialized, r#"{"l":0.3,"a":0.8,"b":0.1}"#);
694 }
695
696 #[cfg(feature = "serializing")]
697 #[test]
698 fn deserialize() {
699 let deserialized: Oklab = ::serde_json::from_str(r#"{"l":0.3,"a":0.8,"b":0.1}"#).unwrap();
700
701 assert_eq!(deserialized, Oklab::new(0.3, 0.8, 0.1));
702 }
703
704 test_uniform_distribution! {
705 Oklab {
706 l: (0.0, 1.0),
707 a: (-1.0, 1.0),
708 b: (-1.0, 1.0)
709 },
710 min: Oklab::new(0.0, -1.0, -1.0),
711 max: Oklab::new(1.0, 1.0, 1.0)
712 }
713}