bevy_color/
hsla.rs

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