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