bevy_color/
hsla.rs

1use crate::{
2    Alpha, ColorToComponents, Gray, Hsva, Hue, Hwba, Lcha, LinearRgba, Luminance, Mix, Saturation,
3    Srgba, 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(
17    feature = "bevy_reflect",
18    derive(Reflect),
19    reflect(Clone, PartialEq, Default)
20)]
21#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
22#[cfg_attr(
23    all(feature = "serialize", feature = "bevy_reflect"),
24    reflect(Serialize, Deserialize)
25)]
26pub struct Hsla {
27    /// The hue channel. [0.0, 360.0]
28    pub hue: f32,
29    /// The saturation channel. [0.0, 1.0]
30    pub saturation: f32,
31    /// The lightness channel. [0.0, 1.0]
32    pub lightness: f32,
33    /// The alpha channel. [0.0, 1.0]
34    pub alpha: f32,
35}
36
37impl StandardColor for Hsla {}
38
39impl Hsla {
40    /// Construct a new [`Hsla`] color from components.
41    ///
42    /// # Arguments
43    ///
44    /// * `hue` - Hue channel. [0.0, 360.0]
45    /// * `saturation` - Saturation channel. [0.0, 1.0]
46    /// * `lightness` - Lightness channel. [0.0, 1.0]
47    /// * `alpha` - Alpha channel. [0.0, 1.0]
48    pub const fn new(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Self {
49        Self {
50            hue,
51            saturation,
52            lightness,
53            alpha,
54        }
55    }
56
57    /// Construct a new [`Hsla`] color from (h, s, l) components, with the default alpha (1.0).
58    ///
59    /// # Arguments
60    ///
61    /// * `hue` - Hue channel. [0.0, 360.0]
62    /// * `saturation` - Saturation channel. [0.0, 1.0]
63    /// * `lightness` - Lightness channel. [0.0, 1.0]
64    pub const fn hsl(hue: f32, saturation: f32, lightness: f32) -> Self {
65        Self::new(hue, saturation, lightness, 1.0)
66    }
67
68    /// Return a copy of this color with the saturation channel set to the given value.
69    pub const fn with_saturation(self, saturation: f32) -> Self {
70        Self { saturation, ..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::Hsla;
87    /// // Unique color for an entity
88    /// # let entity_index = 123;
89    /// // let entity_index = entity.index();
90    /// let color = Hsla::sequential_dispersed(entity_index);
91    ///
92    /// // Palette with 5 distinct hues
93    /// let palette = (0..5).map(Hsla::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::hsl(hue, 1., 0.5)
105    }
106}
107
108impl Default for Hsla {
109    fn default() -> Self {
110        Self::new(0., 0., 1., 1.)
111    }
112}
113
114impl Mix for Hsla {
115    #[inline]
116    fn mix(&self, other: &Self, factor: f32) -> Self {
117        let n_factor = 1.0 - factor;
118        Self {
119            hue: crate::color_ops::lerp_hue(self.hue, other.hue, factor),
120            saturation: self.saturation * n_factor + other.saturation * factor,
121            lightness: self.lightness * n_factor + other.lightness * factor,
122            alpha: self.alpha * n_factor + other.alpha * factor,
123        }
124    }
125}
126
127impl Gray for Hsla {
128    const BLACK: Self = Self::new(0., 0., 0., 1.);
129    const WHITE: Self = Self::new(0., 0., 1., 1.);
130}
131
132impl Alpha for Hsla {
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 Hsla {
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 Saturation for Hsla {
167    #[inline]
168    fn with_saturation(&self, saturation: f32) -> Self {
169        Self {
170            saturation,
171            ..*self
172        }
173    }
174
175    #[inline]
176    fn saturation(&self) -> f32 {
177        self.saturation
178    }
179
180    #[inline]
181    fn set_saturation(&mut self, saturation: f32) {
182        self.saturation = saturation;
183    }
184}
185
186impl Luminance for Hsla {
187    #[inline]
188    fn with_luminance(&self, lightness: f32) -> Self {
189        Self { lightness, ..*self }
190    }
191
192    fn luminance(&self) -> f32 {
193        self.lightness
194    }
195
196    fn darker(&self, amount: f32) -> Self {
197        Self {
198            lightness: (self.lightness - amount).clamp(0., 1.),
199            ..*self
200        }
201    }
202
203    fn lighter(&self, amount: f32) -> Self {
204        Self {
205            lightness: (self.lightness + amount).min(1.),
206            ..*self
207        }
208    }
209}
210
211impl ColorToComponents for Hsla {
212    fn to_f32_array(self) -> [f32; 4] {
213        [self.hue, self.saturation, self.lightness, self.alpha]
214    }
215
216    fn to_f32_array_no_alpha(self) -> [f32; 3] {
217        [self.hue, self.saturation, self.lightness]
218    }
219
220    fn to_vec4(self) -> Vec4 {
221        Vec4::new(self.hue, self.saturation, self.lightness, self.alpha)
222    }
223
224    fn to_vec3(self) -> Vec3 {
225        Vec3::new(self.hue, self.saturation, self.lightness)
226    }
227
228    fn from_f32_array(color: [f32; 4]) -> Self {
229        Self {
230            hue: color[0],
231            saturation: color[1],
232            lightness: color[2],
233            alpha: color[3],
234        }
235    }
236
237    fn from_f32_array_no_alpha(color: [f32; 3]) -> Self {
238        Self {
239            hue: color[0],
240            saturation: color[1],
241            lightness: color[2],
242            alpha: 1.0,
243        }
244    }
245
246    fn from_vec4(color: Vec4) -> Self {
247        Self {
248            hue: color[0],
249            saturation: color[1],
250            lightness: color[2],
251            alpha: color[3],
252        }
253    }
254
255    fn from_vec3(color: Vec3) -> Self {
256        Self {
257            hue: color[0],
258            saturation: color[1],
259            lightness: color[2],
260            alpha: 1.0,
261        }
262    }
263}
264
265impl From<Hsla> for Hsva {
266    fn from(
267        Hsla {
268            hue,
269            saturation,
270            lightness,
271            alpha,
272        }: Hsla,
273    ) -> Self {
274        // Based on https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_HSV
275        let value = lightness + saturation * lightness.min(1. - lightness);
276        let saturation = if value == 0. {
277            0.
278        } else {
279            2. * (1. - (lightness / value))
280        };
281
282        Hsva::new(hue, saturation, value, alpha)
283    }
284}
285
286impl From<Hsva> for Hsla {
287    fn from(
288        Hsva {
289            hue,
290            saturation,
291            value,
292            alpha,
293        }: Hsva,
294    ) -> Self {
295        // Based on https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_HSL
296        let lightness = value * (1. - saturation / 2.);
297        let saturation = if lightness == 0. || lightness == 1. {
298            0.
299        } else {
300            (value - lightness) / lightness.min(1. - lightness)
301        };
302
303        Hsla::new(hue, saturation, lightness, alpha)
304    }
305}
306
307// Derived Conversions
308
309impl From<Hwba> for Hsla {
310    fn from(value: Hwba) -> Self {
311        Hsva::from(value).into()
312    }
313}
314
315impl From<Hsla> for Hwba {
316    fn from(value: Hsla) -> Self {
317        Hsva::from(value).into()
318    }
319}
320
321impl From<Srgba> for Hsla {
322    fn from(value: Srgba) -> Self {
323        Hsva::from(value).into()
324    }
325}
326
327impl From<Hsla> for Srgba {
328    fn from(value: Hsla) -> Self {
329        Hsva::from(value).into()
330    }
331}
332
333impl From<LinearRgba> for Hsla {
334    fn from(value: LinearRgba) -> Self {
335        Hsva::from(value).into()
336    }
337}
338
339impl From<Hsla> for LinearRgba {
340    fn from(value: Hsla) -> Self {
341        Hsva::from(value).into()
342    }
343}
344
345impl From<Lcha> for Hsla {
346    fn from(value: Lcha) -> Self {
347        Hsva::from(value).into()
348    }
349}
350
351impl From<Hsla> for Lcha {
352    fn from(value: Hsla) -> Self {
353        Hsva::from(value).into()
354    }
355}
356
357impl From<Xyza> for Hsla {
358    fn from(value: Xyza) -> Self {
359        Hsva::from(value).into()
360    }
361}
362
363impl From<Hsla> for Xyza {
364    fn from(value: Hsla) -> Self {
365        Hsva::from(value).into()
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372    use crate::{
373        color_difference::EuclideanDistance, test_colors::TEST_COLORS, testing::assert_approx_eq,
374    };
375
376    #[test]
377    fn test_to_from_srgba() {
378        let hsla = Hsla::new(0.5, 0.5, 0.5, 1.0);
379        let srgba: Srgba = hsla.into();
380        let hsla2: Hsla = srgba.into();
381        assert_approx_eq!(hsla.hue, hsla2.hue, 0.001);
382        assert_approx_eq!(hsla.saturation, hsla2.saturation, 0.001);
383        assert_approx_eq!(hsla.lightness, hsla2.lightness, 0.001);
384        assert_approx_eq!(hsla.alpha, hsla2.alpha, 0.001);
385    }
386
387    #[test]
388    fn test_to_from_srgba_2() {
389        for color in TEST_COLORS.iter() {
390            let rgb2: Srgba = (color.hsl).into();
391            let hsl2: Hsla = (color.rgb).into();
392            assert!(
393                color.rgb.distance(&rgb2) < 0.000001,
394                "{}: {:?} != {:?}",
395                color.name,
396                color.rgb,
397                rgb2
398            );
399            assert_approx_eq!(color.hsl.hue, hsl2.hue, 0.001);
400            assert_approx_eq!(color.hsl.saturation, hsl2.saturation, 0.001);
401            assert_approx_eq!(color.hsl.lightness, hsl2.lightness, 0.001);
402            assert_approx_eq!(color.hsl.alpha, hsl2.alpha, 0.001);
403        }
404    }
405
406    #[test]
407    fn test_to_from_linear() {
408        let hsla = Hsla::new(0.5, 0.5, 0.5, 1.0);
409        let linear: LinearRgba = hsla.into();
410        let hsla2: Hsla = linear.into();
411        assert_approx_eq!(hsla.hue, hsla2.hue, 0.001);
412        assert_approx_eq!(hsla.saturation, hsla2.saturation, 0.001);
413        assert_approx_eq!(hsla.lightness, hsla2.lightness, 0.001);
414        assert_approx_eq!(hsla.alpha, hsla2.alpha, 0.001);
415    }
416
417    #[test]
418    fn test_mix_wrap() {
419        let hsla0 = Hsla::new(10., 0.5, 0.5, 1.0);
420        let hsla1 = Hsla::new(20., 0.5, 0.5, 1.0);
421        let hsla2 = Hsla::new(350., 0.5, 0.5, 1.0);
422        assert_approx_eq!(hsla0.mix(&hsla1, 0.25).hue, 12.5, 0.001);
423        assert_approx_eq!(hsla0.mix(&hsla1, 0.5).hue, 15., 0.001);
424        assert_approx_eq!(hsla0.mix(&hsla1, 0.75).hue, 17.5, 0.001);
425
426        assert_approx_eq!(hsla1.mix(&hsla0, 0.25).hue, 17.5, 0.001);
427        assert_approx_eq!(hsla1.mix(&hsla0, 0.5).hue, 15., 0.001);
428        assert_approx_eq!(hsla1.mix(&hsla0, 0.75).hue, 12.5, 0.001);
429
430        assert_approx_eq!(hsla0.mix(&hsla2, 0.25).hue, 5., 0.001);
431        assert_approx_eq!(hsla0.mix(&hsla2, 0.5).hue, 0., 0.001);
432        assert_approx_eq!(hsla0.mix(&hsla2, 0.75).hue, 355., 0.001);
433
434        assert_approx_eq!(hsla2.mix(&hsla0, 0.25).hue, 355., 0.001);
435        assert_approx_eq!(hsla2.mix(&hsla0, 0.5).hue, 0., 0.001);
436        assert_approx_eq!(hsla2.mix(&hsla0, 0.75).hue, 5., 0.001);
437    }
438
439    #[test]
440    fn test_from_index() {
441        let references = [
442            Hsla::hsl(0.0, 1., 0.5),
443            Hsla::hsl(222.49225, 1., 0.5),
444            Hsla::hsl(84.984474, 1., 0.5),
445            Hsla::hsl(307.4767, 1., 0.5),
446            Hsla::hsl(169.96895, 1., 0.5),
447        ];
448
449        for (index, reference) in references.into_iter().enumerate() {
450            let color = Hsla::sequential_dispersed(index as u32);
451
452            assert_approx_eq!(color.hue, reference.hue, 0.001);
453        }
454    }
455}