palette/
oklab.rs

1//! Types for the Oklab color space.
2
3use 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// Using recalculated matrix values from
34// https://github.com/LeaVerou/color.js/blob/master/src/spaces/oklab.js
35//
36// see https://github.com/w3c/csswg-drafts/issues/6642#issuecomment-943521484
37// and the following https://github.com/w3c/csswg-drafts/issues/6642#issuecomment-945714988
38
39/// XYZ to LSM transformation matrix
40#[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/// LMS to XYZ transformation matrix
50#[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/// LMS to Oklab transformation matrix
60#[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/// Oklab to LMS transformation matrix
70#[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/// The [Oklab color space](https://bottosson.github.io/posts/oklab/).
81///
82/// # Characteristics
83/// `Oklab` is a *perceptual* color space. It does not relate to an output
84/// device (a monitor or printer) but instead relates to the [CIE standard
85/// observer](https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_standard_observer)
86/// -- an averaging of the results of color matching experiments under
87/// laboratory conditions.
88///
89/// `Oklab` is a uniform color space ([Compare to the HSV color
90/// space](https://bottosson.github.io/posts/oklab/#comparing-oklab-to-hsv)). It
91/// is useful for things like:
92/// * Turning an image grayscale, while keeping the perceived lightness the same
93/// * Increasing the saturation of colors, while maintaining perceived hue and
94///   lightness
95/// * Creating smooth and uniform looking transitions between colors
96///
97/// `Oklab`'s structure is similar to [L\*a\*b\*](crate::Lab). It is based on
98/// the [opponent color model of human
99/// vision](https://en.wikipedia.org/wiki/Opponent_process), where red and green
100/// form an opponent pair, and blue and yellow form an opponent pair.
101///
102/// `Oklab` uses [D65](https://en.wikipedia.org/wiki/Illuminant_D65)'s
103/// whitepoint -- daylight illumination, which is also used by sRGB, rec2020 and
104/// Display P3 color spaces -- and assumes normal well-lit viewing conditions,
105/// to which the eye is adapted. Thus `Oklab`s lightness `l` technically is a
106/// measure of relative brightness -- a subjective measure -- not relative
107/// luminance. The lightness is scale/exposure-independend, i.e. independent of
108/// the actual luminance of the color, as displayed by some medium, and even for
109/// blindingly bright colors or very bright or dark viewing conditions assumes,
110/// that the eye is adapted to the color's luminance and the hue and chroma are
111/// perceived linearly.
112///
113///
114/// `Oklab`'s chroma is unlimited. Thus it can represent colors of any color
115/// space (including HDR). `l` is in the range `0.0 .. 1.0` and `a` and `b` are
116/// unbounded.
117///
118/// # Conversions
119/// [`Oklch`] is a cylindrical form of `Oklab`.
120///
121/// `Oklab` colors converted from valid (i.e. clamped) `sRGB` will be in the
122/// `sRGB` gamut.
123///
124/// [`Okhsv`], [`Okhwb`][crate::Okhsv] and [`Okhsl`] reference the `sRGB` gamut.
125/// The transformation from `Oklab` to one of them is based on the assumption,
126/// that the transformed `Oklab` value is within `sRGB`.
127///
128/// `Okhsv`, `Okhwb` and `Okhsl` are not applicable to HDR, which also come with
129/// color spaces with wider gamuts. They require [additional
130/// research](https://bottosson.github.io/posts/colorpicker/#ideas-for-future-work).
131///
132/// When a `Oklab` color is converted from [`Srgb`](crate::rgb::Srgb) or a
133/// equivalent color space, e.g. [`Hsv`][crate::Hsv], [`Okhsv`],
134/// [`Hsl`][crate::Hsl], [`Okhsl`], [`Hwb`][crate::Hwb],
135/// [`Okhwb`][crate::Okhwb], it's lightness will be relative to the (user
136/// controlled) maximum contrast and luminance of the display device, to which
137/// the eye is assumed to be adapted.
138///
139/// # Clamping
140/// [`Clamp`][crate::Clamp]ing will only clamp `l`. Clamping does not guarantee
141/// the color to be inside the perceptible or any display-dependent color space
142/// (like *sRGB*).
143///
144/// To ensure a color is within the *sRGB* gamut, it can first be converted to
145/// `Okhsv`, clamped there and converted it back to `Oklab`.
146///
147/// ```
148/// # use approx::assert_abs_diff_eq;
149/// # use palette::{convert::FromColorUnclamped,IsWithinBounds, LinSrgb, Okhsv, Oklab};
150/// # use palette::Clamp;
151/// // Display P3 yellow according to https://colorjs.io/apps/convert/?color=color(display-p3%201%201%200)&precision=17
152/// let oklab = Oklab::from_color_unclamped(LinSrgb::new(1.0, 1.0, -0.098273600140966));
153/// let okhsv: Okhsv<f64> = Okhsv::from_color_unclamped(oklab);
154/// assert!(!okhsv.is_within_bounds());
155/// let clamped_okhsv = okhsv.clamp();
156/// assert!(clamped_okhsv.is_within_bounds());
157/// let linsrgb = LinSrgb::from_color_unclamped(clamped_okhsv);
158/// let  expected = LinSrgb::new(1.0, 0.9876530763223166, 0.0);
159/// assert_abs_diff_eq!(expected, linsrgb, epsilon = 0.02);
160/// ```
161/// Since the conversion contains a gamut mapping, it will map the color to one
162/// of the perceptually closest locations in the `sRGB` gamut. Gamut mapping --
163/// unlike clamping -- is an expensive operation. To get computationally cheaper
164/// (and perceptually much worse) results, convert directly to [`Srgb`] and
165/// clamp there.
166///
167/// # Lightening / Darkening
168/// [`Lighten`](crate::Lighten)ing and [`Darken`](crate::Darken)ing will change
169/// `l`, as expected. However, either operation may leave an implicit color
170/// space (the percetible or a display dependent color space like *sRGB*).
171///
172/// To ensure a color is within the *sRGB* gamut, first convert it to `Okhsl`,
173/// lighten/darken it there and convert it back to `Oklab`.
174
175#[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    /// `l` is the lightness of the color. `0` gives absolute black and `1` gives the
186    /// full white point luminance of the display medium.
187    ///
188    /// [`D65` (normalized with Y=1, i.e. white according to the adaption of the
189    /// eye) transforms to
190    /// L=1,a=0,b=0](https://bottosson.github.io/posts/oklab/#how-oklab-was-derived).
191    /// However intermediate values differ from those of CIELab non-linearly.
192    pub l: T,
193
194    /// `a` changes the hue from reddish to greenish, when moving from positive
195    /// to negative values and becomes more intense with larger absolute values.
196    ///
197    /// The exact orientation is determined by `b`
198    pub a: T,
199
200    /// `b` changes the hue from yellowish to blueish, when moving from positive
201    /// to negative values and becomes more intense with larger absolute values.
202    ///
203    /// [Positive b is oriented to the same yellow color as
204    /// CAM16](https://bottosson.github.io/posts/oklab/#how-oklab-was-derived)
205    pub b: T,
206}
207
208impl<T> Oklab<T> {
209    /// Create an Oklab color.
210    pub const fn new(l: T, a: T, b: T) -> Self {
211        Self { l, a, b }
212    }
213
214    /// Convert to a `(L, a, b)` tuple.
215    pub fn into_components(self) -> (T, T, T) {
216        (self.l, self.a, self.b)
217    }
218
219    /// Convert from a `(L, a, b)` tuple.
220    pub fn from_components((l, a, b): (T, T, T)) -> Self {
221        Self::new(l, a, b)
222    }
223}
224
225// component bounds
226// For `Oklab` in general a and b are unbounded.
227// In the sRGB gamut `Oklab`s chroma (and thus a and b) are bounded.
228impl<T> Oklab<T>
229where
230    T: Zero + One,
231{
232    /// Return the `l` value minimum.
233    pub fn min_l() -> T {
234        T::zero()
235    }
236
237    /// Return the `l` value maximum.
238    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    /// Returns the chroma.
251    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            // Use direct sRGB to Oklab conversion
345            // Rounding errors are likely a contributing factor to differences.
346            // Also the conversion via XYZ doesn't use pre-defined matrices (yet)
347            linear_srgb_to_oklab(rgb.into_linear().reinterpret_as())
348        } else {
349            // Convert via XYZ
350            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
371/// # See
372/// See [`okhsl_to_srgb`](https://bottosson.github.io/posts/colorpicker/#hsl-2)
373impl<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        // Interpolate the three values for C so that:
406        // At s=0: dC/ds = cs.zero, C = 0
407        // At s=0.8: C = cs.mid
408        // At s=1.0: C = cs.max
409
410        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            // pure black
452            return Self {
453                l: T::zero(),
454                a: T::zero(),
455                b: T::zero(),
456            };
457        }
458
459        if hsv.saturation == T::zero() {
460            // totally desaturated color -- the triangle is just the 0-chroma-line
461            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        // first we compute L and V as if the gamut is a perfect triangle
479
480        // L, C, when v == 1:
481        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        // then we compensate for both toe and the curved top part of the triangle:
488        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        // the values may be outside the normal range
496        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        /// Asserts that, for any color space, the lightness of pure white is converted to `l == 1.0`
553        #[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            // use f64 to be comparable to javascript
575            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            // values from Ok Color Picker, which seems to use  Björn Ottosson's original
580            // algorithm (from the direct srgb2oklab conversion, not via the XYZ color space)
581            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            // from https://github.com/bottosson/bottosson.github.io/blob/master/misc/ok_color.h
590            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            // from https://github.com/bottosson/bottosson.github.io/blob/master/misc/ok_color.h
598            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            // from https://github.com/bottosson/bottosson.github.io/blob/master/misc/ok_color.h
611            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                // a and b are unbounded --> not part of test
669            }
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}