bevy_color/
laba.rs

1use crate::{
2    impl_componentwise_vector_space, Alpha, ColorToComponents, Gray, Hsla, Hsva, Hwba, LinearRgba,
3    Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza,
4};
5use bevy_math::{Vec3, Vec4};
6use bevy_reflect::prelude::*;
7
8/// Color in LAB color space, with alpha
9#[doc = include_str!("../docs/conversion.md")]
10/// <div>
11#[doc = include_str!("../docs/diagrams/model_graph.svg")]
12/// </div>
13#[derive(Debug, Clone, Copy, PartialEq, Reflect)]
14#[reflect(PartialEq, Default)]
15#[cfg_attr(
16    feature = "serialize",
17    derive(serde::Serialize, serde::Deserialize),
18    reflect(Serialize, Deserialize)
19)]
20pub struct Laba {
21    /// The lightness channel. [0.0, 1.5]
22    pub lightness: f32,
23    /// The a axis. [-1.5, 1.5]
24    pub a: f32,
25    /// The b axis. [-1.5, 1.5]
26    pub b: f32,
27    /// The alpha channel. [0.0, 1.0]
28    pub alpha: f32,
29}
30
31impl StandardColor for Laba {}
32
33impl_componentwise_vector_space!(Laba, [lightness, a, b, alpha]);
34
35impl Laba {
36    /// Construct a new [`Laba`] color from components.
37    ///
38    /// # Arguments
39    ///
40    /// * `lightness` - Lightness channel. [0.0, 1.5]
41    /// * `a` - a axis. [-1.5, 1.5]
42    /// * `b` - b axis. [-1.5, 1.5]
43    /// * `alpha` - Alpha channel. [0.0, 1.0]
44    pub const fn new(lightness: f32, a: f32, b: f32, alpha: f32) -> Self {
45        Self {
46            lightness,
47            a,
48            b,
49            alpha,
50        }
51    }
52
53    /// Construct a new [`Laba`] color from (l, a, b) components, with the default alpha (1.0).
54    ///
55    /// # Arguments
56    ///
57    /// * `lightness` - Lightness channel. [0.0, 1.5]
58    /// * `a` - a axis. [-1.5, 1.5]
59    /// * `b` - b axis. [-1.5, 1.5]
60    pub const fn lab(lightness: f32, a: f32, b: f32) -> Self {
61        Self {
62            lightness,
63            a,
64            b,
65            alpha: 1.0,
66        }
67    }
68
69    /// Return a copy of this color with the lightness channel set to the given value.
70    pub const fn with_lightness(self, lightness: f32) -> Self {
71        Self { lightness, ..self }
72    }
73
74    /// CIE Epsilon Constant
75    ///
76    /// See [Continuity (16) (17)](http://brucelindbloom.com/index.html?LContinuity.html)
77    pub const CIE_EPSILON: f32 = 216.0 / 24389.0;
78
79    /// CIE Kappa Constant
80    ///
81    /// See [Continuity (16) (17)](http://brucelindbloom.com/index.html?LContinuity.html)
82    pub const CIE_KAPPA: f32 = 24389.0 / 27.0;
83}
84
85impl Default for Laba {
86    fn default() -> Self {
87        Self::new(1., 0., 0., 1.)
88    }
89}
90
91impl Mix for Laba {
92    #[inline]
93    fn mix(&self, other: &Self, factor: f32) -> Self {
94        let n_factor = 1.0 - factor;
95        Self {
96            lightness: self.lightness * n_factor + other.lightness * factor,
97            a: self.a * n_factor + other.a * factor,
98            b: self.b * n_factor + other.b * factor,
99            alpha: self.alpha * n_factor + other.alpha * factor,
100        }
101    }
102}
103
104impl Gray for Laba {
105    const BLACK: Self = Self::new(0., 0., 0., 1.);
106    const WHITE: Self = Self::new(1., 0., 0., 1.);
107}
108
109impl Alpha for Laba {
110    #[inline]
111    fn with_alpha(&self, alpha: f32) -> Self {
112        Self { alpha, ..*self }
113    }
114
115    #[inline]
116    fn alpha(&self) -> f32 {
117        self.alpha
118    }
119
120    #[inline]
121    fn set_alpha(&mut self, alpha: f32) {
122        self.alpha = alpha;
123    }
124}
125
126impl Luminance for Laba {
127    #[inline]
128    fn with_luminance(&self, lightness: f32) -> Self {
129        Self { lightness, ..*self }
130    }
131
132    fn luminance(&self) -> f32 {
133        self.lightness
134    }
135
136    fn darker(&self, amount: f32) -> Self {
137        Self::new(
138            (self.lightness - amount).max(0.),
139            self.a,
140            self.b,
141            self.alpha,
142        )
143    }
144
145    fn lighter(&self, amount: f32) -> Self {
146        Self::new(
147            (self.lightness + amount).min(1.),
148            self.a,
149            self.b,
150            self.alpha,
151        )
152    }
153}
154
155impl ColorToComponents for Laba {
156    fn to_f32_array(self) -> [f32; 4] {
157        [self.lightness, self.a, self.b, self.alpha]
158    }
159
160    fn to_f32_array_no_alpha(self) -> [f32; 3] {
161        [self.lightness, self.a, self.b]
162    }
163
164    fn to_vec4(self) -> Vec4 {
165        Vec4::new(self.lightness, self.a, self.b, self.alpha)
166    }
167
168    fn to_vec3(self) -> Vec3 {
169        Vec3::new(self.lightness, self.a, self.b)
170    }
171
172    fn from_f32_array(color: [f32; 4]) -> Self {
173        Self {
174            lightness: color[0],
175            a: color[1],
176            b: color[2],
177            alpha: color[3],
178        }
179    }
180
181    fn from_f32_array_no_alpha(color: [f32; 3]) -> Self {
182        Self {
183            lightness: color[0],
184            a: color[1],
185            b: color[2],
186            alpha: 1.0,
187        }
188    }
189
190    fn from_vec4(color: Vec4) -> Self {
191        Self {
192            lightness: color[0],
193            a: color[1],
194            b: color[2],
195            alpha: color[3],
196        }
197    }
198
199    fn from_vec3(color: Vec3) -> Self {
200        Self {
201            lightness: color[0],
202            a: color[1],
203            b: color[2],
204            alpha: 1.0,
205        }
206    }
207}
208
209impl From<Laba> for Xyza {
210    fn from(
211        Laba {
212            lightness,
213            a,
214            b,
215            alpha,
216        }: Laba,
217    ) -> Self {
218        // Based on http://www.brucelindbloom.com/index.html?Eqn_Lab_to_XYZ.html
219        let l = 100. * lightness;
220        let a = 100. * a;
221        let b = 100. * b;
222
223        let fy = (l + 16.0) / 116.0;
224        let fx = a / 500.0 + fy;
225        let fz = fy - b / 200.0;
226        let xr = {
227            let fx3 = fx.powf(3.0);
228
229            if fx3 > Laba::CIE_EPSILON {
230                fx3
231            } else {
232                (116.0 * fx - 16.0) / Laba::CIE_KAPPA
233            }
234        };
235        let yr = if l > Laba::CIE_EPSILON * Laba::CIE_KAPPA {
236            ((l + 16.0) / 116.0).powf(3.0)
237        } else {
238            l / Laba::CIE_KAPPA
239        };
240        let zr = {
241            let fz3 = fz.powf(3.0);
242
243            if fz3 > Laba::CIE_EPSILON {
244                fz3
245            } else {
246                (116.0 * fz - 16.0) / Laba::CIE_KAPPA
247            }
248        };
249        let x = xr * Xyza::D65_WHITE.x;
250        let y = yr * Xyza::D65_WHITE.y;
251        let z = zr * Xyza::D65_WHITE.z;
252
253        Xyza::new(x, y, z, alpha)
254    }
255}
256
257impl From<Xyza> for Laba {
258    fn from(Xyza { x, y, z, alpha }: Xyza) -> Self {
259        // Based on http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html
260        let xr = x / Xyza::D65_WHITE.x;
261        let yr = y / Xyza::D65_WHITE.y;
262        let zr = z / Xyza::D65_WHITE.z;
263        let fx = if xr > Laba::CIE_EPSILON {
264            xr.cbrt()
265        } else {
266            (Laba::CIE_KAPPA * xr + 16.0) / 116.0
267        };
268        let fy = if yr > Laba::CIE_EPSILON {
269            yr.cbrt()
270        } else {
271            (Laba::CIE_KAPPA * yr + 16.0) / 116.0
272        };
273        let fz = if yr > Laba::CIE_EPSILON {
274            zr.cbrt()
275        } else {
276            (Laba::CIE_KAPPA * zr + 16.0) / 116.0
277        };
278        let l = 1.16 * fy - 0.16;
279        let a = 5.00 * (fx - fy);
280        let b = 2.00 * (fy - fz);
281
282        Laba::new(l, a, b, alpha)
283    }
284}
285
286// Derived Conversions
287
288impl From<Srgba> for Laba {
289    fn from(value: Srgba) -> Self {
290        Xyza::from(value).into()
291    }
292}
293
294impl From<Laba> for Srgba {
295    fn from(value: Laba) -> Self {
296        Xyza::from(value).into()
297    }
298}
299
300impl From<LinearRgba> for Laba {
301    fn from(value: LinearRgba) -> Self {
302        Xyza::from(value).into()
303    }
304}
305
306impl From<Laba> for LinearRgba {
307    fn from(value: Laba) -> Self {
308        Xyza::from(value).into()
309    }
310}
311
312impl From<Hsla> for Laba {
313    fn from(value: Hsla) -> Self {
314        Xyza::from(value).into()
315    }
316}
317
318impl From<Laba> for Hsla {
319    fn from(value: Laba) -> Self {
320        Xyza::from(value).into()
321    }
322}
323
324impl From<Hsva> for Laba {
325    fn from(value: Hsva) -> Self {
326        Xyza::from(value).into()
327    }
328}
329
330impl From<Laba> for Hsva {
331    fn from(value: Laba) -> Self {
332        Xyza::from(value).into()
333    }
334}
335
336impl From<Hwba> for Laba {
337    fn from(value: Hwba) -> Self {
338        Xyza::from(value).into()
339    }
340}
341
342impl From<Laba> for Hwba {
343    fn from(value: Laba) -> Self {
344        Xyza::from(value).into()
345    }
346}
347
348impl From<Oklaba> for Laba {
349    fn from(value: Oklaba) -> Self {
350        Xyza::from(value).into()
351    }
352}
353
354impl From<Laba> for Oklaba {
355    fn from(value: Laba) -> Self {
356        Xyza::from(value).into()
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363    use crate::{
364        color_difference::EuclideanDistance, test_colors::TEST_COLORS, testing::assert_approx_eq,
365    };
366
367    #[test]
368    fn test_to_from_srgba() {
369        for color in TEST_COLORS.iter() {
370            let rgb2: Srgba = (color.lab).into();
371            let laba: Laba = (color.rgb).into();
372            assert!(
373                color.rgb.distance(&rgb2) < 0.0001,
374                "{}: {:?} != {:?}",
375                color.name,
376                color.rgb,
377                rgb2
378            );
379            assert_approx_eq!(color.lab.lightness, laba.lightness, 0.001);
380            if laba.lightness > 0.01 {
381                assert_approx_eq!(color.lab.a, laba.a, 0.1);
382            }
383            if laba.lightness > 0.01 && laba.a > 0.01 {
384                assert!(
385                    (color.lab.b - laba.b).abs() < 1.7,
386                    "{:?} != {:?}",
387                    color.lab,
388                    laba
389                );
390            }
391            assert_approx_eq!(color.lab.alpha, laba.alpha, 0.001);
392        }
393    }
394
395    #[test]
396    fn test_to_from_linear() {
397        for color in TEST_COLORS.iter() {
398            let rgb2: LinearRgba = (color.lab).into();
399            let laba: Laba = (color.linear_rgb).into();
400            assert!(
401                color.linear_rgb.distance(&rgb2) < 0.0001,
402                "{}: {:?} != {:?}",
403                color.name,
404                color.linear_rgb,
405                rgb2
406            );
407            assert_approx_eq!(color.lab.lightness, laba.lightness, 0.001);
408            if laba.lightness > 0.01 {
409                assert_approx_eq!(color.lab.a, laba.a, 0.1);
410            }
411            if laba.lightness > 0.01 && laba.a > 0.01 {
412                assert!(
413                    (color.lab.b - laba.b).abs() < 1.7,
414                    "{:?} != {:?}",
415                    color.lab,
416                    laba
417                );
418            }
419            assert_approx_eq!(color.lab.alpha, laba.alpha, 0.001);
420        }
421    }
422}