acolor/
oklab.rs

1// acolor::oklab
2//
3//! A non-linear, perceptually uniform color space.
4//!
5//! Oklch is the corresponding polar form of Oklab.
6//!
7//! Uses a D65 whitepoint.
8//!
9//! # Links
10//! - <https://bottosson.github.io/posts/oklab/>
11//! - <https://www.w3.org/TR/css-color-4/#ok-lab>
12//! - <https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklab>
13//
14
15#[cfg(any(feature = "std", feature = "no_std"))]
16use crate::srgb::{LinearSrgb32, LinearSrgba32, Srgb32, Srgb8, Srgba32, Srgba8};
17use devela::cmp::{pclamp, pmax};
18
19#[cfg(all(feature = "no_std", not(feature = "std")))]
20use libm::{atan2f, cbrtf, cosf, hypotf, powf, sinf};
21
22/* definitions */
23
24/// Oklab color representation using `3` × [`f32`] components.
25///
26/// # Fields
27/// - l: perceived luminosity
28/// - a: green/red axis
29/// - b: blue/yellow axis
30///
31/// Best suited for perceptual color manipulation.
32#[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd)]
33pub struct Oklab32 {
34    /// Perceived lightness. A percentage between 0% and 100%.
35    pub l: f32,
36    /// The distance along the `a` axis from **greenish cyan** to **purplish red**.
37    pub a: f32,
38    /// The distance along the `b` axis, from **sky blue** to **mustard yellow**.
39    pub b: f32,
40}
41
42/// # Constructors
43impl Oklab32 {
44    /// New Oklab color.
45    ///
46    /// # Arguments
47    /// - **lighness**, tipically between `0.` and `100.`, range: `> 0.`.
48    /// - **a**, cyan..red axis, range: `-0.5..0.5`.
49    /// - **b**, blue..yellow axis, range: `-0.5..0.5`.
50    pub fn new(lightness: f32, a: f32, b: f32) -> Oklab32 {
51        let l = pmax(0.0, lightness);
52        let a = pclamp(a, -0.5, 0.5);
53        let b = pclamp(b, -0.5, 0.5);
54
55        Self { l, a, b }
56    }
57}
58
59/// # Constants
60impl Oklab32 {
61    ///
62    pub const L_MIN: f32 = 0.;
63    ///
64    pub const L_MAX: f32 = 100.;
65
66    ///
67    pub const A_MIN: f32 = -0.5;
68    ///
69    pub const A_MAX: f32 = 0.5;
70
71    ///
72    pub const B_MIN: f32 = -0.5;
73    ///
74    pub const B_MAX: f32 = 0.5;
75}
76
77/// # Operations
78impl Oklab32 {
79    /// Measures the perceptual distance to another Oklab color
80    // CHECK:FIX: saturate
81    #[inline]
82    #[cfg(any(feature = "std", feature = "no_std"))]
83    #[cfg_attr(
84        feature = "nightly",
85        doc(cfg(any(feature = "std", feature = "no_std")))
86    )]
87    pub fn squared_distance(&self, other: &Oklab32) -> f32 {
88        #[cfg(feature = "std")]
89        return (self.l - other.l).powi(2)
90            + (self.a - other.a).powi(2)
91            + (self.b - other.b).powi(2);
92
93        #[cfg(not(feature = "std"))]
94        return powf(self.l - other.l, 2.)
95            + powf(self.a - other.a, 2.)
96            + powf(self.b - other.b, 2.);
97    }
98
99    // ///
100    // /// - <https://www.w3.org/TR/css-color-4/#color-difference-OK>
101    // #[inline]
102    // pub fn distance(&self, other: &Oklab32) -> f32 {
103    //     ((self.l - other.l).powi(2) + (self.a - other.a).powi(2) + (self.b - other.b).powi(2))
104    //         .sqrt()
105    // }
106}
107
108/// Oklch color representation using `3` × [`f32`] components.
109///
110/// # Fields
111/// - l: perceived luminosity
112/// - c: chromacity
113/// - h: hue
114///
115/// Best suited for perceptual color manipulation.
116#[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd)]
117pub struct Oklch32 {
118    /// Perceived lightness. A percentage between 0% and 100%.
119    pub l: f32,
120    /// Chromacity
121    pub c: f32,
122    /// Hue angle.
123    /// - 0º points along the positive `a` axis (purplish red).
124    /// - 90º points along the positive `b` axis (mustard yellow).
125    /// - 180º points along the negative `a` axis (greenish cyan).
126    /// - 90º points along the negative `b` axis (sky blue).
127    pub h: f32,
128}
129
130/// # Constructors
131impl Oklch32 {
132    /// New Oklch color with clamped values.
133    pub fn new(luminance: f32, chroma: f32, hue: f32) -> Oklch32 {
134        let l = pclamp(luminance, 0.0, 100.0);
135        let c = pclamp(chroma, 0.0, 0.5);
136        let h = pclamp(hue, 0.0, 360.);
137
138        Self { l, c, h }
139    }
140}
141
142/// # Constants
143impl Oklch32 {
144    /// Luminance minimum value.
145    pub const L_MIN: f32 = 0.;
146    /// Luminance maximum value.
147    pub const L_MAX: f32 = 100.;
148
149    /// Chroma minimum value.
150    pub const C_MIN: f32 = 0.;
151    /// Chroma maximum value.
152    pub const C_MAX: f32 = 0.5;
153
154    /// Hue minimum value.
155    pub const H_MIN: f32 = 0.;
156    /// Hue maximum value.
157    pub const H_MAX: f32 = 360.;
158}
159
160// // TODO
161// /// # Operations
162// impl Oklch32 {
163// }
164
165/* conversions */
166
167// Converts from [`Oklab32`] to [`Oklch32`] color spaces.
168#[inline]
169#[cfg(any(feature = "std", feature = "no_std"))]
170fn oklab32_to_oklch32(c: Oklab32) -> Oklch32 {
171    #[cfg(feature = "std")]
172    {
173        use core::f32::consts::PI as PI_32;
174        let hue = c.b.atan2(c.a) * 180. / PI_32;
175        #[rustfmt::skip]
176        let h = if hue >= 0. { hue } else { hue + 360. };
177
178        Oklch32 {
179            l: c.l,
180            c: c.a.hypot(c.b),
181            h,
182        }
183    }
184
185    #[cfg(not(feature = "std"))]
186    {
187        use core::f32::consts::PI as PI_32;
188        let hue = atan2f(c.b, c.a) * 180. / PI_32;
189        #[rustfmt::skip]
190        let h = if hue >= 0. { hue } else { hue + 360. };
191
192        Oklch32 {
193            l: c.l,
194            c: hypotf(c.a, c.b),
195            h,
196        }
197    }
198}
199
200// Converts from [`Oklch32`] to [`Oklab32`] color spaces.
201#[inline]
202#[cfg(any(feature = "std", feature = "no_std"))]
203fn oklch32_to_oklab32(c: Oklch32) -> Oklab32 {
204    #[cfg(feature = "std")]
205    {
206        use core::f32::consts::PI as PI_32;
207        Oklab32 {
208            l: c.l,
209            a: c.c * (c.h * PI_32 / 180.).cos(),
210            b: c.c * (c.h * PI_32 / 180.).sin(),
211        }
212    }
213
214    #[cfg(not(feature = "std"))]
215    {
216        use core::f32::consts::PI as PI_32;
217        Oklab32 {
218            l: c.l,
219            a: c.c * cosf(c.h * PI_32 / 180.),
220            b: c.c * sinf(c.h * PI_32 / 180.),
221        }
222    }
223}
224
225/// Converts from [`LinearSrgb32`] to [`Oklab32`] color spaces.
226#[cfg(any(feature = "std", feature = "no_std"))]
227fn linear_srgb32_to_oklab32(c: LinearSrgb32) -> Oklab32 {
228    #[cfg(feature = "std")]
229    let l = (0.4122214708 * c.r + 0.5363325363 * c.g + 0.0514459929 * c.b).cbrt();
230    #[cfg(not(feature = "std"))]
231    let l = cbrtf(0.4122214708 * c.r + 0.5363325363 * c.g + 0.0514459929 * c.b);
232    #[cfg(feature = "std")]
233    let m = (0.2119034982 * c.r + 0.6806995451 * c.g + 0.1073969566 * c.b).cbrt();
234    #[cfg(not(feature = "std"))]
235    let m = cbrtf(0.2119034982 * c.r + 0.6806995451 * c.g + 0.1073969566 * c.b);
236    #[cfg(feature = "std")]
237    let s = (0.0883024619 * c.r + 0.2817188376 * c.g + 0.6299787005 * c.b).cbrt();
238    #[cfg(not(feature = "std"))]
239    let s = cbrtf(0.0883024619 * c.r + 0.2817188376 * c.g + 0.6299787005 * c.b);
240
241    Oklab32 {
242        l: 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
243        a: 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
244        b: 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s,
245    }
246}
247
248/// Converts from [`Oklab32`] to [`LinearSrgb32`] color spaces.
249#[cfg(any(feature = "std", feature = "no_std"))]
250fn oklab32_to_linear_srgb32(c: Oklab32) -> LinearSrgb32 {
251    let _l = c.l + 0.3963377774 * c.a + 0.2158037573 * c.b;
252    let _m = c.l - 0.1055613458 * c.a - 0.0638541728 * c.b;
253    let _s = c.l - 0.0894841775 * c.a - 1.2914855480 * c.b;
254
255    let l = _l * _l * _l;
256    let m = _m * _m * _m;
257    let s = _s * _s * _s;
258
259    LinearSrgb32 {
260        r: 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
261        g: -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
262        b: -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
263    }
264}
265
266/// # Direct conversions
267impl Oklab32 {
268    // [] ()
269
270    /// Direct conversion from an array.
271    #[inline]
272    pub fn from_array(c: [f32; 3]) -> Oklab32 {
273        Oklab32 {
274            l: c[0],
275            a: c[1],
276            b: c[2],
277        }
278    }
279    /// Direct conversion to an array.
280    #[inline]
281    pub fn to_array(c: Oklab32) -> [f32; 3] {
282        [c.l, c.a, c.b]
283    }
284
285    /// Direct conversion from a tuple.
286    #[inline]
287    pub fn from_tuple(c: (f32, f32, f32)) -> Oklab32 {
288        Oklab32 {
289            l: c.0,
290            a: c.1,
291            b: c.2,
292        }
293    }
294    /// Direct conversion to a tuple.
295    #[inline]
296    pub fn to_tuple(c: Oklab32) -> (f32, f32, f32) {
297        (c.l, c.a, c.b)
298    }
299}
300
301#[cfg(any(feature = "std", feature = "no_std"))]
302#[cfg_attr(
303    feature = "nightly",
304    doc(cfg(any(feature = "std", feature = "no_std")))
305)]
306impl Oklab32 {
307    // LinearSrgb32
308
309    /// Direct conversion from [`LinearSrgb32`].
310    #[inline]
311    pub fn from_linear_srgb32(c: LinearSrgb32) -> Oklab32 {
312        linear_srgb32_to_oklab32(c)
313    }
314
315    /// Direct conversion to [`LinearSrgb32`].
316    #[inline]
317    pub fn to_linear_srgb32(&self) -> LinearSrgb32 {
318        oklab32_to_linear_srgb32(*self)
319    }
320
321    // LinearSrgba32
322
323    /// Direct conversion from [`LinearSrgba32`].
324    ///
325    /// Loses the alpha channel.
326    #[inline]
327    pub fn from_linear_srgba32(c: LinearSrgba32) -> Oklab32 {
328        c.to_linear_srgb32().to_oklab32()
329    }
330
331    /// Direct conversion to [`LinearSrgba32`].
332    ///
333    /// Adds the `alpha` channel.
334    #[inline]
335    pub fn to_linear_srgba32(&self, alpha: f32) -> LinearSrgba32 {
336        oklab32_to_linear_srgb32(*self).to_linear_srgba32(alpha)
337    }
338
339    // Oklch32
340
341    /// Direct conversion to [`Oklch32`].
342    #[inline]
343    pub fn to_oklch32(&self) -> Oklch32 {
344        oklab32_to_oklch32(*self)
345    }
346
347    /// Direct conversion from [`Oklch32`].
348    #[inline]
349    pub fn from_oklch32(c: Oklch32) -> Oklab32 {
350        oklch32_to_oklab32(c)
351    }
352}
353
354/// # Indirect conversions
355#[cfg(any(feature = "std", feature = "no_std"))]
356#[cfg_attr(
357    feature = "nightly",
358    doc(cfg(any(feature = "std", feature = "no_std")))
359)]
360impl Oklab32 {
361    // Srgb8
362
363    /// Indirect conversion from [`Srgb8`].
364    #[inline]
365    pub fn from_srgb8(c: Srgb8) -> Oklab32 {
366        c.to_oklab32()
367    }
368
369    /// Indirect conversion to [`Srgb8`].
370    #[inline]
371    pub fn to_srgb8(&self) -> Srgb8 {
372        self.to_linear_srgb32().to_srgb32().to_srgb8()
373    }
374
375    // Srgb8
376
377    /// Indirect conversion from [`Srgba8`].
378    ///
379    /// Loses the alpha channel.
380    #[inline]
381    pub fn from_srgba8(c: Srgba8) -> Oklab32 {
382        c.to_oklab32()
383    }
384
385    /// Indirect conversion to [`Srgba8`].
386    ///
387    /// Adds the `alpha` channel.
388    #[inline]
389    pub fn to_srgba8(&self, alpha: u8) -> Srgba8 {
390        self.to_linear_srgb32().to_srgb32().to_srgba8(alpha)
391    }
392
393    // Srgb32
394
395    /// Indirect conversion from [`Srgb32`].
396    #[inline]
397    pub fn from_srgb32(c: Srgb32) -> Oklab32 {
398        c.to_oklab32()
399    }
400
401    /// Indirect conversion to [`Srgb32`].
402    #[inline]
403    pub fn to_srgb32(&self) -> Srgb32 {
404        self.to_linear_srgb32().to_srgb32()
405    }
406
407    // Srgba32
408
409    /// Indirect conversion from [`Srgba32`].
410    ///
411    /// Loses the alpha channel.
412    #[inline]
413    pub fn from_srgba32(c: Srgba32) -> Oklab32 {
414        c.to_oklab32()
415    }
416
417    /// Indirect conversion to [`Srgba32`].
418    ///
419    /// Adds the `alpha` channel.
420    #[inline]
421    pub fn to_srgba32(&self, alpha: f32) -> Srgba32 {
422        self.to_linear_srgb32().to_srgba32(alpha)
423    }
424}
425
426/// # Direct conversions
427impl Oklch32 {
428    // [] ()
429
430    /// Direct conversion from an array.
431    #[inline]
432    pub fn from_array(c: [f32; 3]) -> Oklch32 {
433        Oklch32 {
434            l: c[0],
435            c: c[1],
436            h: c[2],
437        }
438    }
439    /// Direct conversion to an array.
440    #[inline]
441    pub fn to_array(c: Oklch32) -> [f32; 3] {
442        [c.l, c.c, c.h]
443    }
444
445    /// Direct conversion from a tuple.
446    #[inline]
447    pub fn from_tuple(c: (f32, f32, f32)) -> Oklch32 {
448        Oklch32 {
449            l: c.0,
450            c: c.1,
451            h: c.2,
452        }
453    }
454    /// Direct conversion to a tuple.
455    #[inline]
456    pub fn to_tuple(c: Oklch32) -> (f32, f32, f32) {
457        (c.l, c.c, c.h)
458    }
459}
460
461#[cfg(any(feature = "std", feature = "no_std"))]
462#[cfg_attr(
463    feature = "nightly",
464    doc(cfg(any(feature = "std", feature = "no_std")))
465)]
466impl Oklch32 {
467    // Oklab32
468
469    /// Direct conversion from [`Oklab32`].
470    #[inline]
471    pub fn from_oklab32(c: Oklab32) -> Oklch32 {
472        oklab32_to_oklch32(c)
473    }
474
475    /// Direct conversion to [`Oklab32`].
476    #[inline]
477    pub fn to_oklab32(&self) -> Oklab32 {
478        oklch32_to_oklab32(*self)
479    }
480
481    // Srgb8
482
483    /// Indirect conversion from [`Srgb8`].
484    #[inline]
485    pub fn from_srgb8(c: Srgb8) -> Oklch32 {
486        c.to_oklch32()
487    }
488
489    /// Indirect conversion to [`Srgb8`].
490    #[inline]
491    pub fn to_srgb8(&self) -> Srgb8 {
492        self.to_oklab32().to_linear_srgb32().to_srgb32().to_srgb8()
493    }
494
495    // Srgb8
496
497    /// Indirect conversion from [`Srgba8`].
498    ///
499    /// Loses the alpha channel.
500    #[inline]
501    pub fn from_srgba8(c: Srgba8) -> Oklch32 {
502        c.to_oklch32()
503    }
504
505    /// Indirect conversion to [`Srgba8`].
506    ///
507    /// Adds the `alpha` channel.
508    #[inline]
509    pub fn to_srgba8(&self, alpha: u8) -> Srgba8 {
510        self.to_oklab32()
511            .to_linear_srgb32()
512            .to_srgb32()
513            .to_srgba8(alpha)
514    }
515
516    // Srgb32
517
518    /// Indirect conversion from [`Srgb32`].
519    #[inline]
520    pub fn from_srgb32(c: Srgb32) -> Oklch32 {
521        c.to_oklch32()
522    }
523    /// Indirect conversion to [`Srgb32`].
524    #[inline]
525    pub fn to_srgb32(&self) -> Srgb32 {
526        self.to_oklab32().to_linear_srgb32().to_srgb32()
527    }
528
529    // Srgba32
530
531    /// Indirect conversion from [`Srgba32`].
532    ///
533    /// Loses the alpha channel.
534    #[inline]
535    pub fn from_srgba32(c: Srgba32) -> Oklch32 {
536        c.to_oklch32()
537    }
538
539    /// Indirect conversion to [`Srgba32`].
540    ///
541    /// Adds the `alpha` channel.
542    #[inline]
543    pub fn to_srgba32(&self, alpha: f32) -> Srgba32 {
544        self.to_oklab32().to_linear_srgb32().to_srgba32(alpha)
545    }
546
547    // LinearSrgb32
548
549    /// Indirect conversion from [`LinearSrgb32`].
550    #[inline]
551    pub fn from_linear_srgb32(c: LinearSrgb32) -> Oklch32 {
552        c.to_oklch32()
553    }
554
555    /// Indirect conversion to [`LinearSrgba32`].
556    #[inline]
557    pub fn to_linear_srgb32(&self) -> LinearSrgb32 {
558        self.to_oklab32().to_linear_srgb32()
559    }
560
561    // LinearSrgba32
562
563    /// Indirect conversion from [`LinearSrgba32`].
564    ///
565    /// Loses the alpha channel.
566    #[inline]
567    pub fn from_linear_srgba32(c: LinearSrgba32) -> Oklch32 {
568        c.to_oklch32()
569    }
570
571    /// Indirect conversion to [`LinearSrgba32`].
572    ///
573    /// Adds the `alpha` channel.
574    #[inline]
575    pub fn to_linear_srgba32(&self, alpha: f32) -> LinearSrgba32 {
576        self.to_oklab32().to_linear_srgba32(alpha)
577    }
578}
579
580mod impl_from {
581    use super::*;
582
583    /* From/Into arrays and tuples */
584
585    impl From<[f32; 3]> for Oklab32 {
586        #[inline]
587        fn from(c: [f32; 3]) -> Oklab32 {
588            Oklab32::new(c[0], c[1], c[2])
589        }
590    }
591
592    impl From<(f32, f32, f32)> for Oklab32 {
593        #[inline]
594        fn from(c: (f32, f32, f32)) -> Oklab32 {
595            Oklab32::new(c.0, c.1, c.2)
596        }
597    }
598
599    impl From<[f32; 3]> for Oklch32 {
600        #[inline]
601        fn from(c: [f32; 3]) -> Oklch32 {
602            Oklch32::new(c[0], c[1], c[2])
603        }
604    }
605
606    impl From<(f32, f32, f32)> for Oklch32 {
607        #[inline]
608        fn from(c: (f32, f32, f32)) -> Oklch32 {
609            Oklch32::new(c.0, c.1, c.2)
610        }
611    }
612
613    /* From Oklab32 */
614
615    impl From<Oklab32> for [f32; 3] {
616        #[inline]
617        fn from(c: Oklab32) -> [f32; 3] {
618            [c.l, c.a, c.b]
619        }
620    }
621
622    impl From<Oklab32> for (f32, f32, f32) {
623        #[inline]
624        fn from(c: Oklab32) -> (f32, f32, f32) {
625            (c.l, c.a, c.b)
626        }
627    }
628}
629
630#[cfg(any(feature = "std", feature = "no_std"))]
631#[cfg_attr(
632    feature = "nightly",
633    doc(cfg(any(feature = "std", feature = "no_std")))
634)]
635mod impl_from_std {
636    use super::*;
637
638    impl From<Oklab32> for Oklch32 {
639        #[inline]
640        fn from(c: Oklab32) -> Oklch32 {
641            c.to_oklch32()
642        }
643    }
644
645    impl From<Oklab32> for Srgb8 {
646        #[inline]
647        fn from(c: Oklab32) -> Srgb8 {
648            c.to_linear_srgb32().to_srgb32().to_srgb8()
649        }
650    }
651
652    impl From<Oklab32> for Srgba8 {
653        /// Automatically adds alpha at max opacity.
654        #[inline]
655        fn from(c: Oklab32) -> Srgba8 {
656            c.to_srgba8(u8::MAX)
657        }
658    }
659
660    impl From<Oklab32> for Srgb32 {
661        #[inline]
662        fn from(c: Oklab32) -> Srgb32 {
663            c.to_srgb32()
664        }
665    }
666
667    impl From<Oklab32> for Srgba32 {
668        /// Automatically adds alpha at max opacity.
669        #[inline]
670        fn from(c: Oklab32) -> Srgba32 {
671            c.to_srgba32(1.)
672        }
673    }
674
675    impl From<Oklab32> for LinearSrgb32 {
676        #[inline]
677        fn from(c: Oklab32) -> LinearSrgb32 {
678            c.to_linear_srgb32()
679        }
680    }
681
682    impl From<Oklab32> for LinearSrgba32 {
683        /// Automatically adds alpha at max opacity.
684        #[inline]
685        fn from(c: Oklab32) -> LinearSrgba32 {
686            c.to_linear_srgba32(1.)
687        }
688    }
689
690    /* From Oklch32 */
691
692    impl From<Oklch32> for [f32; 3] {
693        #[inline]
694        fn from(c: Oklch32) -> [f32; 3] {
695            [c.l, c.c, c.h]
696        }
697    }
698
699    impl From<Oklch32> for (f32, f32, f32) {
700        #[inline]
701        fn from(c: Oklch32) -> (f32, f32, f32) {
702            (c.l, c.c, c.h)
703        }
704    }
705
706    impl From<Oklch32> for Oklab32 {
707        #[inline]
708        fn from(c: Oklch32) -> Oklab32 {
709            c.to_oklab32()
710        }
711    }
712}