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::{ops, Vec3, Vec4};
6#[cfg(feature = "bevy_reflect")]
7use bevy_reflect::prelude::*;
8
9/// Color in LAB 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 Laba {
22    /// The lightness channel. [0.0, 1.5]
23    pub lightness: f32,
24    /// The a axis. [-1.5, 1.5]
25    pub a: f32,
26    /// The b axis. [-1.5, 1.5]
27    pub b: f32,
28    /// The alpha channel. [0.0, 1.0]
29    pub alpha: f32,
30}
31
32impl StandardColor for Laba {}
33
34impl_componentwise_vector_space!(Laba, [lightness, a, b, alpha]);
35
36impl Laba {
37    /// Construct a new [`Laba`] color from components.
38    ///
39    /// # Arguments
40    ///
41    /// * `lightness` - Lightness channel. [0.0, 1.5]
42    /// * `a` - a axis. [-1.5, 1.5]
43    /// * `b` - b axis. [-1.5, 1.5]
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 [`Laba`] color from (l, a, b) components, with the default alpha (1.0).
55    ///
56    /// # Arguments
57    ///
58    /// * `lightness` - Lightness channel. [0.0, 1.5]
59    /// * `a` - a axis. [-1.5, 1.5]
60    /// * `b` - b axis. [-1.5, 1.5]
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    /// CIE Epsilon Constant
76    ///
77    /// See [Continuity (16) (17)](http://brucelindbloom.com/index.html?LContinuity.html)
78    pub const CIE_EPSILON: f32 = 216.0 / 24389.0;
79
80    /// CIE Kappa Constant
81    ///
82    /// See [Continuity (16) (17)](http://brucelindbloom.com/index.html?LContinuity.html)
83    pub const CIE_KAPPA: f32 = 24389.0 / 27.0;
84}
85
86impl Default for Laba {
87    fn default() -> Self {
88        Self::new(1., 0., 0., 1.)
89    }
90}
91
92impl Mix for Laba {
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 Laba {
106    const BLACK: Self = Self::new(0., 0., 0., 1.);
107    const WHITE: Self = Self::new(1., 0., 0., 1.);
108}
109
110impl Alpha for Laba {
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 Laba {
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 ColorToComponents for Laba {
157    fn to_f32_array(self) -> [f32; 4] {
158        [self.lightness, self.a, self.b, self.alpha]
159    }
160
161    fn to_f32_array_no_alpha(self) -> [f32; 3] {
162        [self.lightness, self.a, self.b]
163    }
164
165    fn to_vec4(self) -> Vec4 {
166        Vec4::new(self.lightness, self.a, self.b, self.alpha)
167    }
168
169    fn to_vec3(self) -> Vec3 {
170        Vec3::new(self.lightness, self.a, self.b)
171    }
172
173    fn from_f32_array(color: [f32; 4]) -> Self {
174        Self {
175            lightness: color[0],
176            a: color[1],
177            b: color[2],
178            alpha: color[3],
179        }
180    }
181
182    fn from_f32_array_no_alpha(color: [f32; 3]) -> Self {
183        Self {
184            lightness: color[0],
185            a: color[1],
186            b: color[2],
187            alpha: 1.0,
188        }
189    }
190
191    fn from_vec4(color: Vec4) -> Self {
192        Self {
193            lightness: color[0],
194            a: color[1],
195            b: color[2],
196            alpha: color[3],
197        }
198    }
199
200    fn from_vec3(color: Vec3) -> Self {
201        Self {
202            lightness: color[0],
203            a: color[1],
204            b: color[2],
205            alpha: 1.0,
206        }
207    }
208}
209
210impl From<Laba> for Xyza {
211    fn from(
212        Laba {
213            lightness,
214            a,
215            b,
216            alpha,
217        }: Laba,
218    ) -> Self {
219        // Based on http://www.brucelindbloom.com/index.html?Eqn_Lab_to_XYZ.html
220        let l = 100. * lightness;
221        let a = 100. * a;
222        let b = 100. * b;
223
224        let fy = (l + 16.0) / 116.0;
225        let fx = a / 500.0 + fy;
226        let fz = fy - b / 200.0;
227        let xr = {
228            let fx3 = ops::powf(fx, 3.0);
229
230            if fx3 > Laba::CIE_EPSILON {
231                fx3
232            } else {
233                (116.0 * fx - 16.0) / Laba::CIE_KAPPA
234            }
235        };
236        let yr = if l > Laba::CIE_EPSILON * Laba::CIE_KAPPA {
237            ops::powf((l + 16.0) / 116.0, 3.0)
238        } else {
239            l / Laba::CIE_KAPPA
240        };
241        let zr = {
242            let fz3 = ops::powf(fz, 3.0);
243
244            if fz3 > Laba::CIE_EPSILON {
245                fz3
246            } else {
247                (116.0 * fz - 16.0) / Laba::CIE_KAPPA
248            }
249        };
250        let x = xr * Xyza::D65_WHITE.x;
251        let y = yr * Xyza::D65_WHITE.y;
252        let z = zr * Xyza::D65_WHITE.z;
253
254        Xyza::new(x, y, z, alpha)
255    }
256}
257
258impl From<Xyza> for Laba {
259    fn from(Xyza { x, y, z, alpha }: Xyza) -> Self {
260        // Based on http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html
261        let xr = x / Xyza::D65_WHITE.x;
262        let yr = y / Xyza::D65_WHITE.y;
263        let zr = z / Xyza::D65_WHITE.z;
264        let fx = if xr > Laba::CIE_EPSILON {
265            ops::cbrt(xr)
266        } else {
267            (Laba::CIE_KAPPA * xr + 16.0) / 116.0
268        };
269        let fy = if yr > Laba::CIE_EPSILON {
270            ops::cbrt(yr)
271        } else {
272            (Laba::CIE_KAPPA * yr + 16.0) / 116.0
273        };
274        let fz = if yr > Laba::CIE_EPSILON {
275            ops::cbrt(zr)
276        } else {
277            (Laba::CIE_KAPPA * zr + 16.0) / 116.0
278        };
279        let l = 1.16 * fy - 0.16;
280        let a = 5.00 * (fx - fy);
281        let b = 2.00 * (fy - fz);
282
283        Laba::new(l, a, b, alpha)
284    }
285}
286
287// Derived Conversions
288
289impl From<Srgba> for Laba {
290    fn from(value: Srgba) -> Self {
291        Xyza::from(value).into()
292    }
293}
294
295impl From<Laba> for Srgba {
296    fn from(value: Laba) -> Self {
297        Xyza::from(value).into()
298    }
299}
300
301impl From<LinearRgba> for Laba {
302    fn from(value: LinearRgba) -> Self {
303        Xyza::from(value).into()
304    }
305}
306
307impl From<Laba> for LinearRgba {
308    fn from(value: Laba) -> Self {
309        Xyza::from(value).into()
310    }
311}
312
313impl From<Hsla> for Laba {
314    fn from(value: Hsla) -> Self {
315        Xyza::from(value).into()
316    }
317}
318
319impl From<Laba> for Hsla {
320    fn from(value: Laba) -> Self {
321        Xyza::from(value).into()
322    }
323}
324
325impl From<Hsva> for Laba {
326    fn from(value: Hsva) -> Self {
327        Xyza::from(value).into()
328    }
329}
330
331impl From<Laba> for Hsva {
332    fn from(value: Laba) -> Self {
333        Xyza::from(value).into()
334    }
335}
336
337impl From<Hwba> for Laba {
338    fn from(value: Hwba) -> Self {
339        Xyza::from(value).into()
340    }
341}
342
343impl From<Laba> for Hwba {
344    fn from(value: Laba) -> Self {
345        Xyza::from(value).into()
346    }
347}
348
349impl From<Oklaba> for Laba {
350    fn from(value: Oklaba) -> Self {
351        Xyza::from(value).into()
352    }
353}
354
355impl From<Laba> for Oklaba {
356    fn from(value: Laba) -> Self {
357        Xyza::from(value).into()
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364    use crate::{
365        color_difference::EuclideanDistance, test_colors::TEST_COLORS, testing::assert_approx_eq,
366    };
367
368    #[test]
369    fn test_to_from_srgba() {
370        for color in TEST_COLORS.iter() {
371            let rgb2: Srgba = (color.lab).into();
372            let laba: Laba = (color.rgb).into();
373            assert!(
374                color.rgb.distance(&rgb2) < 0.0001,
375                "{}: {:?} != {:?}",
376                color.name,
377                color.rgb,
378                rgb2
379            );
380            assert_approx_eq!(color.lab.lightness, laba.lightness, 0.001);
381            if laba.lightness > 0.01 {
382                assert_approx_eq!(color.lab.a, laba.a, 0.1);
383            }
384            if laba.lightness > 0.01 && laba.a > 0.01 {
385                assert!(
386                    (color.lab.b - laba.b).abs() < 1.7,
387                    "{:?} != {:?}",
388                    color.lab,
389                    laba
390                );
391            }
392            assert_approx_eq!(color.lab.alpha, laba.alpha, 0.001);
393        }
394    }
395
396    #[test]
397    fn test_to_from_linear() {
398        for color in TEST_COLORS.iter() {
399            let rgb2: LinearRgba = (color.lab).into();
400            let laba: Laba = (color.linear_rgb).into();
401            assert!(
402                color.linear_rgb.distance(&rgb2) < 0.0001,
403                "{}: {:?} != {:?}",
404                color.name,
405                color.linear_rgb,
406                rgb2
407            );
408            assert_approx_eq!(color.lab.lightness, laba.lightness, 0.001);
409            if laba.lightness > 0.01 {
410                assert_approx_eq!(color.lab.a, laba.a, 0.1);
411            }
412            if laba.lightness > 0.01 && laba.a > 0.01 {
413                assert!(
414                    (color.lab.b - laba.b).abs() < 1.7,
415                    "{:?} != {:?}",
416                    color.lab,
417                    laba
418                );
419            }
420            assert_approx_eq!(color.lab.alpha, laba.alpha, 0.001);
421        }
422    }
423}