bevy_color/
lcha.rs

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