bevy_color/
oklaba.rs

1use crate::{
2    color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ColorToComponents,
3    Gray, Hsla, Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza,
4};
5use bevy_math::{ops, FloatPow, Vec3, Vec4};
6#[cfg(feature = "bevy_reflect")]
7use bevy_reflect::prelude::*;
8
9/// Color in Oklab color space, with alpha
10#[doc = include_str!("../docs/conversion.md")]
11/// <div>
12#[doc = include_str!("../docs/diagrams/model_graph.svg")]
13/// </div>
14#[derive(Debug, Clone, Copy, PartialEq)]
15#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(PartialEq, Default))]
16#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
17#[cfg_attr(
18    all(feature = "serialize", feature = "bevy_reflect"),
19    reflect(Serialize, Deserialize)
20)]
21pub struct Oklaba {
22    /// The 'lightness' channel. [0.0, 1.0]
23    pub lightness: f32,
24    /// The 'a' channel. [-1.0, 1.0]
25    pub a: f32,
26    /// The 'b' channel. [-1.0, 1.0]
27    pub b: f32,
28    /// The alpha channel. [0.0, 1.0]
29    pub alpha: f32,
30}
31
32impl StandardColor for Oklaba {}
33
34impl_componentwise_vector_space!(Oklaba, [lightness, a, b, alpha]);
35
36impl Oklaba {
37    /// Construct a new [`Oklaba`] color from components.
38    ///
39    /// # Arguments
40    ///
41    /// * `lightness` - Lightness channel. [0.0, 1.0]
42    /// * `a` - Green-red channel. [-1.0, 1.0]
43    /// * `b` - Blue-yellow channel. [-1.0, 1.0]
44    /// * `alpha` - Alpha channel. [0.0, 1.0]
45    pub const fn new(lightness: f32, a: f32, b: f32, alpha: f32) -> Self {
46        Self {
47            lightness,
48            a,
49            b,
50            alpha,
51        }
52    }
53
54    /// Construct a new [`Oklaba`] color from (l, a, b) components, with the default alpha (1.0).
55    ///
56    /// # Arguments
57    ///
58    /// * `lightness` - Lightness channel. [0.0, 1.0]
59    /// * `a` - Green-red channel. [-1.0, 1.0]
60    /// * `b` - Blue-yellow channel. [-1.0, 1.0]
61    pub const fn lab(lightness: f32, a: f32, b: f32) -> Self {
62        Self {
63            lightness,
64            a,
65            b,
66            alpha: 1.0,
67        }
68    }
69
70    /// Return a copy of this color with the 'lightness' channel set to the given value.
71    pub const fn with_lightness(self, lightness: f32) -> Self {
72        Self { lightness, ..self }
73    }
74
75    /// Return a copy of this color with the 'a' channel set to the given value.
76    pub const fn with_a(self, a: f32) -> Self {
77        Self { a, ..self }
78    }
79
80    /// Return a copy of this color with the 'b' channel set to the given value.
81    pub const fn with_b(self, b: f32) -> Self {
82        Self { b, ..self }
83    }
84}
85
86impl Default for Oklaba {
87    fn default() -> Self {
88        Self::new(1., 0., 0., 1.)
89    }
90}
91
92impl Mix for Oklaba {
93    #[inline]
94    fn mix(&self, other: &Self, factor: f32) -> Self {
95        let n_factor = 1.0 - factor;
96        Self {
97            lightness: self.lightness * n_factor + other.lightness * factor,
98            a: self.a * n_factor + other.a * factor,
99            b: self.b * n_factor + other.b * factor,
100            alpha: self.alpha * n_factor + other.alpha * factor,
101        }
102    }
103}
104
105impl Gray for Oklaba {
106    const BLACK: Self = Self::new(0., 0., 0., 1.);
107    const WHITE: Self = Self::new(1.0, 0.0, 0.000000059604645, 1.0);
108}
109
110impl Alpha for Oklaba {
111    #[inline]
112    fn with_alpha(&self, alpha: f32) -> Self {
113        Self { alpha, ..*self }
114    }
115
116    #[inline]
117    fn alpha(&self) -> f32 {
118        self.alpha
119    }
120
121    #[inline]
122    fn set_alpha(&mut self, alpha: f32) {
123        self.alpha = alpha;
124    }
125}
126
127impl Luminance for Oklaba {
128    #[inline]
129    fn with_luminance(&self, lightness: f32) -> Self {
130        Self { lightness, ..*self }
131    }
132
133    fn luminance(&self) -> f32 {
134        self.lightness
135    }
136
137    fn darker(&self, amount: f32) -> Self {
138        Self::new(
139            (self.lightness - amount).max(0.),
140            self.a,
141            self.b,
142            self.alpha,
143        )
144    }
145
146    fn lighter(&self, amount: f32) -> Self {
147        Self::new(
148            (self.lightness + amount).min(1.),
149            self.a,
150            self.b,
151            self.alpha,
152        )
153    }
154}
155
156impl EuclideanDistance for Oklaba {
157    #[inline]
158    fn distance_squared(&self, other: &Self) -> f32 {
159        (self.lightness - other.lightness).squared()
160            + (self.a - other.a).squared()
161            + (self.b - other.b).squared()
162    }
163}
164
165impl ColorToComponents for Oklaba {
166    fn to_f32_array(self) -> [f32; 4] {
167        [self.lightness, self.a, self.b, self.alpha]
168    }
169
170    fn to_f32_array_no_alpha(self) -> [f32; 3] {
171        [self.lightness, self.a, self.b]
172    }
173
174    fn to_vec4(self) -> Vec4 {
175        Vec4::new(self.lightness, self.a, self.b, self.alpha)
176    }
177
178    fn to_vec3(self) -> Vec3 {
179        Vec3::new(self.lightness, self.a, self.b)
180    }
181
182    fn from_f32_array(color: [f32; 4]) -> Self {
183        Self {
184            lightness: color[0],
185            a: color[1],
186            b: color[2],
187            alpha: color[3],
188        }
189    }
190
191    fn from_f32_array_no_alpha(color: [f32; 3]) -> Self {
192        Self {
193            lightness: color[0],
194            a: color[1],
195            b: color[2],
196            alpha: 1.0,
197        }
198    }
199
200    fn from_vec4(color: Vec4) -> Self {
201        Self {
202            lightness: color[0],
203            a: color[1],
204            b: color[2],
205            alpha: color[3],
206        }
207    }
208
209    fn from_vec3(color: Vec3) -> Self {
210        Self {
211            lightness: color[0],
212            a: color[1],
213            b: color[2],
214            alpha: 1.0,
215        }
216    }
217}
218
219#[allow(clippy::excessive_precision)]
220impl From<LinearRgba> for Oklaba {
221    fn from(value: LinearRgba) -> Self {
222        let LinearRgba {
223            red,
224            green,
225            blue,
226            alpha,
227        } = value;
228        // From https://github.com/DougLau/pix
229        let l = 0.4122214708 * red + 0.5363325363 * green + 0.0514459929 * blue;
230        let m = 0.2119034982 * red + 0.6806995451 * green + 0.1073969566 * blue;
231        let s = 0.0883024619 * red + 0.2817188376 * green + 0.6299787005 * blue;
232        let l_ = ops::cbrt(l);
233        let m_ = ops::cbrt(m);
234        let s_ = ops::cbrt(s);
235        let l = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_;
236        let a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_;
237        let b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_;
238        Oklaba::new(l, a, b, alpha)
239    }
240}
241
242#[allow(clippy::excessive_precision)]
243impl From<Oklaba> for LinearRgba {
244    fn from(value: Oklaba) -> Self {
245        let Oklaba {
246            lightness,
247            a,
248            b,
249            alpha,
250        } = value;
251
252        // From https://github.com/Ogeon/palette/blob/e75eab2fb21af579353f51f6229a510d0d50a311/palette/src/oklab.rs#L312-L332
253        let l_ = lightness + 0.3963377774 * a + 0.2158037573 * b;
254        let m_ = lightness - 0.1055613458 * a - 0.0638541728 * b;
255        let s_ = lightness - 0.0894841775 * a - 1.2914855480 * b;
256
257        let l = l_ * l_ * l_;
258        let m = m_ * m_ * m_;
259        let s = s_ * s_ * s_;
260
261        let red = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
262        let green = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
263        let blue = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
264
265        Self {
266            red,
267            green,
268            blue,
269            alpha,
270        }
271    }
272}
273
274// Derived Conversions
275
276impl From<Hsla> for Oklaba {
277    fn from(value: Hsla) -> Self {
278        LinearRgba::from(value).into()
279    }
280}
281
282impl From<Oklaba> for Hsla {
283    fn from(value: Oklaba) -> Self {
284        LinearRgba::from(value).into()
285    }
286}
287
288impl From<Hsva> for Oklaba {
289    fn from(value: Hsva) -> Self {
290        LinearRgba::from(value).into()
291    }
292}
293
294impl From<Oklaba> for Hsva {
295    fn from(value: Oklaba) -> Self {
296        LinearRgba::from(value).into()
297    }
298}
299
300impl From<Hwba> for Oklaba {
301    fn from(value: Hwba) -> Self {
302        LinearRgba::from(value).into()
303    }
304}
305
306impl From<Oklaba> for Hwba {
307    fn from(value: Oklaba) -> Self {
308        LinearRgba::from(value).into()
309    }
310}
311
312impl From<Lcha> for Oklaba {
313    fn from(value: Lcha) -> Self {
314        LinearRgba::from(value).into()
315    }
316}
317
318impl From<Oklaba> for Lcha {
319    fn from(value: Oklaba) -> Self {
320        LinearRgba::from(value).into()
321    }
322}
323
324impl From<Srgba> for Oklaba {
325    fn from(value: Srgba) -> Self {
326        LinearRgba::from(value).into()
327    }
328}
329
330impl From<Oklaba> for Srgba {
331    fn from(value: Oklaba) -> Self {
332        LinearRgba::from(value).into()
333    }
334}
335
336impl From<Xyza> for Oklaba {
337    fn from(value: Xyza) -> Self {
338        LinearRgba::from(value).into()
339    }
340}
341
342impl From<Oklaba> for Xyza {
343    fn from(value: Oklaba) -> Self {
344        LinearRgba::from(value).into()
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351    use crate::{test_colors::TEST_COLORS, testing::assert_approx_eq};
352
353    #[test]
354    fn test_to_from_srgba() {
355        let oklaba = Oklaba::new(0.5, 0.5, 0.5, 1.0);
356        let srgba: Srgba = oklaba.into();
357        let oklaba2: Oklaba = srgba.into();
358        assert_approx_eq!(oklaba.lightness, oklaba2.lightness, 0.001);
359        assert_approx_eq!(oklaba.a, oklaba2.a, 0.001);
360        assert_approx_eq!(oklaba.b, oklaba2.b, 0.001);
361        assert_approx_eq!(oklaba.alpha, oklaba2.alpha, 0.001);
362    }
363
364    #[test]
365    fn test_to_from_srgba_2() {
366        for color in TEST_COLORS.iter() {
367            let rgb2: Srgba = (color.oklab).into();
368            let oklab: Oklaba = (color.rgb).into();
369            assert!(
370                color.rgb.distance(&rgb2) < 0.0001,
371                "{}: {:?} != {:?}",
372                color.name,
373                color.rgb,
374                rgb2
375            );
376            assert!(
377                color.oklab.distance(&oklab) < 0.0001,
378                "{}: {:?} != {:?}",
379                color.name,
380                color.oklab,
381                oklab
382            );
383        }
384    }
385
386    #[test]
387    fn test_to_from_linear() {
388        let oklaba = Oklaba::new(0.5, 0.5, 0.5, 1.0);
389        let linear: LinearRgba = oklaba.into();
390        let oklaba2: Oklaba = linear.into();
391        assert_approx_eq!(oklaba.lightness, oklaba2.lightness, 0.001);
392        assert_approx_eq!(oklaba.a, oklaba2.a, 0.001);
393        assert_approx_eq!(oklaba.b, oklaba2.b, 0.001);
394        assert_approx_eq!(oklaba.alpha, oklaba2.alpha, 0.001);
395    }
396}