bevy_color/
lcha.rs

1use crate::{
2    Alpha, ColorToComponents, Gray, Hue, Laba, LinearRgba, Luminance, Mix, Srgba, StandardColor,
3    Xyza,
4};
5use bevy_math::{Vec3, Vec4};
6use bevy_reflect::prelude::*;
7
8/// Color in LCH 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 Lcha {
21    /// The lightness channel. [0.0, 1.5]
22    pub lightness: f32,
23    /// The chroma channel. [0.0, 1.5]
24    pub chroma: f32,
25    /// The hue channel. [0.0, 360.0]
26    pub hue: f32,
27    /// The alpha channel. [0.0, 1.0]
28    pub alpha: f32,
29}
30
31impl StandardColor for Lcha {}
32
33impl Lcha {
34    /// Construct a new [`Lcha`] color from components.
35    ///
36    /// # Arguments
37    ///
38    /// * `lightness` - Lightness channel. [0.0, 1.5]
39    /// * `chroma` - Chroma channel. [0.0, 1.5]
40    /// * `hue` - Hue channel. [0.0, 360.0]
41    /// * `alpha` - Alpha channel. [0.0, 1.0]
42    pub const fn new(lightness: f32, chroma: f32, hue: f32, alpha: f32) -> Self {
43        Self {
44            lightness,
45            chroma,
46            hue,
47            alpha,
48        }
49    }
50
51    /// Construct a new [`Lcha`] color from (h, s, l) components, with the default alpha (1.0).
52    ///
53    /// # Arguments
54    ///
55    /// * `lightness` - Lightness channel. [0.0, 1.5]
56    /// * `chroma` - Chroma channel. [0.0, 1.5]
57    /// * `hue` - Hue channel. [0.0, 360.0]
58    pub const fn lch(lightness: f32, chroma: f32, hue: f32) -> Self {
59        Self {
60            lightness,
61            chroma,
62            hue,
63            alpha: 1.0,
64        }
65    }
66
67    /// Return a copy of this color with the chroma channel set to the given value.
68    pub const fn with_chroma(self, chroma: f32) -> Self {
69        Self { chroma, ..self }
70    }
71
72    /// Return a copy of this color with the lightness channel set to the given value.
73    pub const fn with_lightness(self, lightness: f32) -> Self {
74        Self { lightness, ..self }
75    }
76
77    /// Generate a deterministic but [quasi-randomly distributed](https://en.wikipedia.org/wiki/Low-discrepancy_sequence)
78    /// color from a provided `index`.
79    ///
80    /// This can be helpful for generating debug colors.
81    ///
82    /// # Examples
83    ///
84    /// ```rust
85    /// # use bevy_color::Lcha;
86    /// // Unique color for an entity
87    /// # let entity_index = 123;
88    /// // let entity_index = entity.index();
89    /// let color = Lcha::sequential_dispersed(entity_index);
90    ///
91    /// // Palette with 5 distinct hues
92    /// let palette = (0..5).map(Lcha::sequential_dispersed).collect::<Vec<_>>();
93    /// ```
94    pub fn sequential_dispersed(index: u32) -> Self {
95        const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up
96        const RATIO_360: f32 = 360.0 / u32::MAX as f32;
97
98        // from https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/
99        //
100        // Map a sequence of integers (eg: 154, 155, 156, 157, 158) into the [0.0..1.0] range,
101        // so that the closer the numbers are, the larger the difference of their image.
102        let hue = index.wrapping_mul(FRAC_U32MAX_GOLDEN_RATIO) as f32 * RATIO_360;
103        Self::lch(0.75, 0.35, hue)
104    }
105}
106
107impl Default for Lcha {
108    fn default() -> Self {
109        Self::new(1., 0., 0., 1.)
110    }
111}
112
113impl Mix for Lcha {
114    #[inline]
115    fn mix(&self, other: &Self, factor: f32) -> Self {
116        let n_factor = 1.0 - factor;
117        Self {
118            lightness: self.lightness * n_factor + other.lightness * factor,
119            chroma: self.chroma * n_factor + other.chroma * factor,
120            hue: crate::color_ops::lerp_hue(self.hue, other.hue, factor),
121            alpha: self.alpha * n_factor + other.alpha * factor,
122        }
123    }
124}
125
126impl Gray for Lcha {
127    const BLACK: Self = Self::new(0.0, 0.0, 0.0000136603785, 1.0);
128    const WHITE: Self = Self::new(1.0, 0.0, 0.0000136603785, 1.0);
129}
130
131impl Alpha for Lcha {
132    #[inline]
133    fn with_alpha(&self, alpha: f32) -> Self {
134        Self { alpha, ..*self }
135    }
136
137    #[inline]
138    fn alpha(&self) -> f32 {
139        self.alpha
140    }
141
142    #[inline]
143    fn set_alpha(&mut self, alpha: f32) {
144        self.alpha = alpha;
145    }
146}
147
148impl Hue for Lcha {
149    #[inline]
150    fn with_hue(&self, hue: f32) -> Self {
151        Self { hue, ..*self }
152    }
153
154    #[inline]
155    fn hue(&self) -> f32 {
156        self.hue
157    }
158
159    #[inline]
160    fn set_hue(&mut self, hue: f32) {
161        self.hue = hue;
162    }
163}
164
165impl Luminance for Lcha {
166    #[inline]
167    fn with_luminance(&self, lightness: f32) -> Self {
168        Self { lightness, ..*self }
169    }
170
171    fn luminance(&self) -> f32 {
172        self.lightness
173    }
174
175    fn darker(&self, amount: f32) -> Self {
176        Self::new(
177            (self.lightness - amount).max(0.),
178            self.chroma,
179            self.hue,
180            self.alpha,
181        )
182    }
183
184    fn lighter(&self, amount: f32) -> Self {
185        Self::new(
186            (self.lightness + amount).min(1.),
187            self.chroma,
188            self.hue,
189            self.alpha,
190        )
191    }
192}
193
194impl ColorToComponents for Lcha {
195    fn to_f32_array(self) -> [f32; 4] {
196        [self.lightness, self.chroma, self.hue, self.alpha]
197    }
198
199    fn to_f32_array_no_alpha(self) -> [f32; 3] {
200        [self.lightness, self.chroma, self.hue]
201    }
202
203    fn to_vec4(self) -> Vec4 {
204        Vec4::new(self.lightness, self.chroma, self.hue, self.alpha)
205    }
206
207    fn to_vec3(self) -> Vec3 {
208        Vec3::new(self.lightness, self.chroma, self.hue)
209    }
210
211    fn from_f32_array(color: [f32; 4]) -> Self {
212        Self {
213            lightness: color[0],
214            chroma: color[1],
215            hue: color[2],
216            alpha: color[3],
217        }
218    }
219
220    fn from_f32_array_no_alpha(color: [f32; 3]) -> Self {
221        Self {
222            lightness: color[0],
223            chroma: color[1],
224            hue: color[2],
225            alpha: 1.0,
226        }
227    }
228
229    fn from_vec4(color: Vec4) -> Self {
230        Self {
231            lightness: color[0],
232            chroma: color[1],
233            hue: color[2],
234            alpha: color[3],
235        }
236    }
237
238    fn from_vec3(color: Vec3) -> Self {
239        Self {
240            lightness: color[0],
241            chroma: color[1],
242            hue: color[2],
243            alpha: 1.0,
244        }
245    }
246}
247
248impl From<Lcha> for Laba {
249    fn from(
250        Lcha {
251            lightness,
252            chroma,
253            hue,
254            alpha,
255        }: Lcha,
256    ) -> Self {
257        // Based on http://www.brucelindbloom.com/index.html?Eqn_LCH_to_Lab.html
258        let l = lightness;
259        let a = chroma * hue.to_radians().cos();
260        let b = chroma * hue.to_radians().sin();
261
262        Laba::new(l, a, b, alpha)
263    }
264}
265
266impl From<Laba> for Lcha {
267    fn from(
268        Laba {
269            lightness,
270            a,
271            b,
272            alpha,
273        }: Laba,
274    ) -> Self {
275        // Based on http://www.brucelindbloom.com/index.html?Eqn_Lab_to_LCH.html
276        let c = (a.powf(2.0) + b.powf(2.0)).sqrt();
277        let h = {
278            let h = b.to_radians().atan2(a.to_radians()).to_degrees();
279
280            if h < 0.0 {
281                h + 360.0
282            } else {
283                h
284            }
285        };
286
287        let chroma = c.clamp(0.0, 1.5);
288        let hue = h;
289
290        Lcha::new(lightness, chroma, hue, alpha)
291    }
292}
293
294// Derived Conversions
295
296impl From<Srgba> for Lcha {
297    fn from(value: Srgba) -> Self {
298        Laba::from(value).into()
299    }
300}
301
302impl From<Lcha> for Srgba {
303    fn from(value: Lcha) -> Self {
304        Laba::from(value).into()
305    }
306}
307
308impl From<LinearRgba> for Lcha {
309    fn from(value: LinearRgba) -> Self {
310        Laba::from(value).into()
311    }
312}
313
314impl From<Lcha> for LinearRgba {
315    fn from(value: Lcha) -> Self {
316        Laba::from(value).into()
317    }
318}
319
320impl From<Xyza> for Lcha {
321    fn from(value: Xyza) -> Self {
322        Laba::from(value).into()
323    }
324}
325
326impl From<Lcha> for Xyza {
327    fn from(value: Lcha) -> Self {
328        Laba::from(value).into()
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335    use crate::{
336        color_difference::EuclideanDistance, test_colors::TEST_COLORS, testing::assert_approx_eq,
337    };
338
339    #[test]
340    fn test_to_from_srgba() {
341        for color in TEST_COLORS.iter() {
342            let rgb2: Srgba = (color.lch).into();
343            let lcha: Lcha = (color.rgb).into();
344            assert!(
345                color.rgb.distance(&rgb2) < 0.0001,
346                "{}: {:?} != {:?}",
347                color.name,
348                color.rgb,
349                rgb2
350            );
351            assert_approx_eq!(color.lch.lightness, lcha.lightness, 0.001);
352            if lcha.lightness > 0.01 {
353                assert_approx_eq!(color.lch.chroma, lcha.chroma, 0.1);
354            }
355            if lcha.lightness > 0.01 && lcha.chroma > 0.01 {
356                assert!(
357                    (color.lch.hue - lcha.hue).abs() < 1.7,
358                    "{:?} != {:?}",
359                    color.lch,
360                    lcha
361                );
362            }
363            assert_approx_eq!(color.lch.alpha, lcha.alpha, 0.001);
364        }
365    }
366
367    #[test]
368    fn test_to_from_linear() {
369        for color in TEST_COLORS.iter() {
370            let rgb2: LinearRgba = (color.lch).into();
371            let lcha: Lcha = (color.linear_rgb).into();
372            assert!(
373                color.linear_rgb.distance(&rgb2) < 0.0001,
374                "{}: {:?} != {:?}",
375                color.name,
376                color.linear_rgb,
377                rgb2
378            );
379            assert_approx_eq!(color.lch.lightness, lcha.lightness, 0.001);
380            if lcha.lightness > 0.01 {
381                assert_approx_eq!(color.lch.chroma, lcha.chroma, 0.1);
382            }
383            if lcha.lightness > 0.01 && lcha.chroma > 0.01 {
384                assert!(
385                    (color.lch.hue - lcha.hue).abs() < 1.7,
386                    "{:?} != {:?}",
387                    color.lch,
388                    lcha
389                );
390            }
391            assert_approx_eq!(color.lch.alpha, lcha.alpha, 0.001);
392        }
393    }
394}