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