Skip to main content

bevy_color/
okhsla.rs

1use crate::{
2    impl_componentwise_vector_space,
3    okcolor_convert::{okhsl_to_oklab, oklab_to_okhsl},
4    Alpha, ColorToComponents, Gray, Hsla, Hsva, Hue, Hwba, Laba, Lcha, LinearRgba, Luminance, Mix,
5    Oklaba, Oklcha, Saturation, Srgba, StandardColor, Xyza,
6};
7use bevy_math::{Vec3, Vec4};
8#[cfg(feature = "bevy_reflect")]
9use bevy_reflect::prelude::*;
10
11/// Color in Okhsl color space with alpha.
12/// Further information on this color model can be found on <https://bottosson.github.io/posts/colorpicker>.
13///
14/// Okhsl is defined relative to the sRGB (Rec. 709) gamut. Converting a wide-gamut or
15/// HDR color clamps the lightness to `1.0` but can push saturation outside `[0.0, 1.0]`.
16#[doc = include_str!("../docs/conversion.md")]
17/// <div>
18#[doc = include_str!("../docs/diagrams/model_graph.svg")]
19/// </div>
20#[derive(Debug, Clone, Copy, PartialEq)]
21#[cfg_attr(
22    feature = "bevy_reflect",
23    derive(Reflect),
24    reflect(Clone, PartialEq, Default)
25)]
26#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
27#[cfg_attr(
28    all(feature = "serialize", feature = "bevy_reflect"),
29    reflect(Serialize, Deserialize)
30)]
31pub struct Okhsla {
32    /// The hue channel. [0.0, 360.0]
33    pub hue: f32,
34    /// The saturation channel. [0.0, 1.0]
35    pub saturation: f32,
36    /// The lightness channel. [0.0, 1.0]
37    pub lightness: f32,
38    /// The alpha channel. [0.0, 1.0]
39    pub alpha: f32,
40}
41
42impl StandardColor for Okhsla {}
43
44impl_componentwise_vector_space!(Okhsla, [hue, saturation, lightness, alpha]);
45
46impl Okhsla {
47    /// Construct a new [`Okhsla`] color from components.
48    ///
49    /// # Arguments
50    ///
51    /// * `hue` - Hue channel. [0.0, 360.0]
52    /// * `saturation` - Saturation channel. [0.0, 1.0]
53    /// * `lightness` - Lightness channel. [0.0, 1.0]
54    /// * `alpha` - Alpha channel. [0.0, 1.0]
55    pub const fn new(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Self {
56        Self {
57            hue,
58            saturation,
59            lightness,
60            alpha,
61        }
62    }
63
64    /// Construct a new [`Okhsla`] color from (h, s, l) components, with the default alpha (1.0).
65    ///
66    /// # Arguments
67    ///
68    /// * `hue` - Hue channel. [0.0, 360.0]
69    /// * `saturation` - Saturation channel. [0.0, 1.0]
70    /// * `lightness` - Lightness channel. [0.0, 1.0]
71    pub const fn hsl(hue: f32, saturation: f32, lightness: f32) -> Self {
72        Self::new(hue, saturation, lightness, 1.0)
73    }
74
75    /// Return a copy of this color with the saturation channel set to the given value.
76    pub const fn with_saturation(self, saturation: f32) -> Self {
77        Self { saturation, ..self }
78    }
79
80    /// Return a copy of this color with the lightness channel set to the given value.
81    pub const fn with_lightness(self, lightness: f32) -> Self {
82        Self { lightness, ..self }
83    }
84
85    /// Generate a deterministic but [quasi-randomly distributed](https://en.wikipedia.org/wiki/Low-discrepancy_sequence)
86    /// color from a provided `index`.
87    ///
88    /// This can be helpful for generating debug colors.
89    ///
90    /// # Examples
91    ///
92    /// ```rust
93    /// # use bevy_color::Okhsla;
94    /// // Unique color for an entity
95    /// # let entity_index = 123;
96    /// // let entity_index = entity.index();
97    /// let color = Okhsla::sequential_dispersed(entity_index);
98    ///
99    /// // Palette with 5 distinct hues
100    /// let palette = (0..5).map(Okhsla::sequential_dispersed).collect::<Vec<_>>();
101    /// ```
102    pub const fn sequential_dispersed(index: u32) -> Self {
103        const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up
104        const RATIO_360: f32 = 360.0 / u32::MAX as f32;
105
106        // from https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/
107        //
108        // Map a sequence of integers (eg: 154, 155, 156, 157, 158) into the [0.0..1.0] range,
109        // so that the closer the numbers are, the larger the difference of their image.
110        let hue = index.wrapping_mul(FRAC_U32MAX_GOLDEN_RATIO) as f32 * RATIO_360;
111        Self::hsl(hue, 1., 0.5)
112    }
113}
114
115impl Default for Okhsla {
116    fn default() -> Self {
117        Self::new(0., 0., 1., 1.)
118    }
119}
120
121impl Mix for Okhsla {
122    #[inline]
123    fn mix(&self, other: &Self, factor: f32) -> Self {
124        let n_factor = 1.0 - factor;
125        Self {
126            hue: crate::color_ops::lerp_hue(self.hue, other.hue, factor),
127            saturation: self.saturation * n_factor + other.saturation * factor,
128            lightness: self.lightness * n_factor + other.lightness * factor,
129            alpha: self.alpha * n_factor + other.alpha * factor,
130        }
131    }
132}
133
134impl Gray for Okhsla {
135    const BLACK: Self = Self::new(0., 0., 0., 1.);
136    const WHITE: Self = Self::new(0., 0., 1., 1.);
137}
138
139impl Alpha for Okhsla {
140    #[inline]
141    fn with_alpha(&self, alpha: f32) -> Self {
142        Self { alpha, ..*self }
143    }
144
145    #[inline]
146    fn alpha(&self) -> f32 {
147        self.alpha
148    }
149
150    #[inline]
151    fn set_alpha(&mut self, alpha: f32) {
152        self.alpha = alpha;
153    }
154}
155
156impl Hue for Okhsla {
157    #[inline]
158    fn with_hue(&self, hue: f32) -> Self {
159        Self { hue, ..*self }
160    }
161
162    #[inline]
163    fn hue(&self) -> f32 {
164        self.hue
165    }
166
167    #[inline]
168    fn set_hue(&mut self, hue: f32) {
169        self.hue = hue;
170    }
171}
172
173impl Saturation for Okhsla {
174    #[inline]
175    fn with_saturation(&self, saturation: f32) -> Self {
176        Self {
177            saturation,
178            ..*self
179        }
180    }
181
182    #[inline]
183    fn saturation(&self) -> f32 {
184        self.saturation
185    }
186
187    #[inline]
188    fn set_saturation(&mut self, saturation: f32) {
189        self.saturation = saturation;
190    }
191}
192
193impl Luminance for Okhsla {
194    #[inline]
195    fn with_luminance(&self, lightness: f32) -> Self {
196        Self { lightness, ..*self }
197    }
198
199    fn luminance(&self) -> f32 {
200        self.lightness
201    }
202
203    fn darker(&self, amount: f32) -> Self {
204        Self {
205            lightness: (self.lightness - amount).clamp(0., 1.),
206            ..*self
207        }
208    }
209
210    fn lighter(&self, amount: f32) -> Self {
211        Self {
212            lightness: (self.lightness + amount).min(1.),
213            ..*self
214        }
215    }
216}
217
218impl ColorToComponents for Okhsla {
219    fn to_f32_array(self) -> [f32; 4] {
220        [self.hue, self.saturation, self.lightness, self.alpha]
221    }
222
223    fn to_f32_array_no_alpha(self) -> [f32; 3] {
224        [self.hue, self.saturation, self.lightness]
225    }
226
227    fn to_vec4(self) -> Vec4 {
228        Vec4::new(self.hue, self.saturation, self.lightness, self.alpha)
229    }
230
231    fn to_vec3(self) -> Vec3 {
232        Vec3::new(self.hue, self.saturation, self.lightness)
233    }
234
235    fn from_f32_array(color: [f32; 4]) -> Self {
236        Self {
237            hue: color[0],
238            saturation: color[1],
239            lightness: color[2],
240            alpha: color[3],
241        }
242    }
243
244    fn from_f32_array_no_alpha(color: [f32; 3]) -> Self {
245        Self {
246            hue: color[0],
247            saturation: color[1],
248            lightness: color[2],
249            alpha: 1.0,
250        }
251    }
252
253    fn from_vec4(color: Vec4) -> Self {
254        Self {
255            hue: color[0],
256            saturation: color[1],
257            lightness: color[2],
258            alpha: color[3],
259        }
260    }
261
262    fn from_vec3(color: Vec3) -> Self {
263        Self {
264            hue: color[0],
265            saturation: color[1],
266            lightness: color[2],
267            alpha: 1.0,
268        }
269    }
270}
271
272#[cfg(feature = "wgpu-types")]
273impl From<Okhsla> for wgpu_types::Color {
274    fn from(color: Okhsla) -> Self {
275        wgpu_types::Color {
276            r: color.hue as f64,
277            g: color.saturation as f64,
278            b: color.lightness as f64,
279            a: color.alpha as f64,
280        }
281    }
282}
283
284impl From<Oklaba> for Okhsla {
285    fn from(value: Oklaba) -> Self {
286        oklab_to_okhsl(value)
287    }
288}
289
290impl From<Okhsla> for Oklaba {
291    fn from(value: Okhsla) -> Self {
292        okhsl_to_oklab(value)
293    }
294}
295
296// Derived Conversions
297
298impl From<LinearRgba> for Okhsla {
299    fn from(value: LinearRgba) -> Self {
300        Oklaba::from(value).into()
301    }
302}
303
304impl From<Okhsla> for LinearRgba {
305    fn from(value: Okhsla) -> Self {
306        Oklaba::from(value).into()
307    }
308}
309
310impl From<Hsla> for Okhsla {
311    fn from(value: Hsla) -> Self {
312        Oklaba::from(value).into()
313    }
314}
315
316impl From<Okhsla> for Hsla {
317    fn from(value: Okhsla) -> Self {
318        Oklaba::from(value).into()
319    }
320}
321
322impl From<Hsva> for Okhsla {
323    fn from(value: Hsva) -> Self {
324        Oklaba::from(value).into()
325    }
326}
327
328impl From<Okhsla> for Hsva {
329    fn from(value: Okhsla) -> Self {
330        Oklaba::from(value).into()
331    }
332}
333
334impl From<Hwba> for Okhsla {
335    fn from(value: Hwba) -> Self {
336        Oklaba::from(value).into()
337    }
338}
339
340impl From<Okhsla> for Hwba {
341    fn from(value: Okhsla) -> Self {
342        Oklaba::from(value).into()
343    }
344}
345
346impl From<Lcha> for Okhsla {
347    fn from(value: Lcha) -> Self {
348        Oklaba::from(value).into()
349    }
350}
351
352impl From<Okhsla> for Lcha {
353    fn from(value: Okhsla) -> Self {
354        Oklaba::from(value).into()
355    }
356}
357
358impl From<Srgba> for Okhsla {
359    fn from(value: Srgba) -> Self {
360        Oklaba::from(value).into()
361    }
362}
363
364impl From<Okhsla> for Srgba {
365    fn from(value: Okhsla) -> Self {
366        Oklaba::from(value).into()
367    }
368}
369
370impl From<Xyza> for Okhsla {
371    fn from(value: Xyza) -> Self {
372        Oklaba::from(value).into()
373    }
374}
375
376impl From<Okhsla> for Xyza {
377    fn from(value: Okhsla) -> Self {
378        Oklaba::from(value).into()
379    }
380}
381
382impl From<Laba> for Okhsla {
383    fn from(value: Laba) -> Self {
384        Oklaba::from(value).into()
385    }
386}
387
388impl From<Okhsla> for Laba {
389    fn from(value: Okhsla) -> Self {
390        Oklaba::from(value).into()
391    }
392}
393
394impl From<Oklcha> for Okhsla {
395    fn from(value: Oklcha) -> Self {
396        Oklaba::from(value).into()
397    }
398}
399
400impl From<Okhsla> for Oklcha {
401    fn from(value: Okhsla) -> Self {
402        Oklaba::from(value).into()
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409    use crate::{
410        color_difference::EuclideanDistance, test_colors::TEST_COLORS, testing::assert_approx_eq,
411    };
412
413    #[test]
414    fn test_from_to_oklaba() {
415        // Test `oklab_l == 0.0`
416        let oklaba = Oklaba::new(0.0, 0.5, 0.5, 1.0);
417        let okhsla: Okhsla = oklaba.into();
418        let oklaba2: Oklaba = okhsla.into();
419        assert_approx_eq!(okhsla.hue, 0.0, 0.001);
420        assert_approx_eq!(okhsla.saturation, 0.0, 0.001);
421        assert_approx_eq!(okhsla.lightness, 0.0, 0.001);
422        assert_approx_eq!(okhsla.alpha, 1.0, 0.001);
423
424        assert_approx_eq!(oklaba.lightness, oklaba2.lightness, 0.001);
425        assert_approx_eq!(0.0, oklaba2.a, 0.001);
426        assert_approx_eq!(0.0, oklaba2.b, 0.001);
427        assert_approx_eq!(oklaba.alpha, oklaba2.alpha, 0.001);
428
429        // Test `oklab_l == 1.0`
430        let oklaba = Oklaba::new(1.0, 0.5, 0.5, 1.0);
431        let okhsla: Okhsla = oklaba.into();
432        let oklaba2: Oklaba = okhsla.into();
433        assert_approx_eq!(okhsla.hue, 0.0, 0.001);
434        assert_approx_eq!(okhsla.saturation, 0.0, 0.001);
435        assert_approx_eq!(okhsla.lightness, 1.0, 0.001);
436        assert_approx_eq!(okhsla.alpha, 1.0, 0.001);
437
438        assert_approx_eq!(oklaba.lightness, oklaba2.lightness, 0.001);
439        assert_approx_eq!(0.0, oklaba2.a, 0.001);
440        assert_approx_eq!(0.0, oklaba2.b, 0.001);
441        assert_approx_eq!(oklaba.alpha, oklaba2.alpha, 0.001);
442
443        // Test `oklab_a == 0.0 && oklab_b ==0.0` (C == 0.0)
444        let oklaba = Oklaba::new(0.5, 0.0, 0.0, 1.0);
445        let okhsla: Okhsla = oklaba.into();
446        let oklaba2: Oklaba = okhsla.into();
447        assert_approx_eq!(okhsla.hue, 0.0, 0.001);
448        assert_approx_eq!(okhsla.saturation, 0.0, 0.001);
449        assert_approx_eq!(okhsla.lightness, 0.42114055, 0.001);
450        assert_approx_eq!(okhsla.alpha, 1.0, 0.001);
451
452        assert_approx_eq!(oklaba.lightness, oklaba2.lightness, 0.001);
453        assert_approx_eq!(0.0, oklaba2.a, 0.001);
454        assert_approx_eq!(0.0, oklaba2.b, 0.001);
455        assert_approx_eq!(oklaba.alpha, oklaba2.alpha, 0.001);
456    }
457
458    #[test]
459    fn test_to_from_srgba() {
460        let okhsla = Okhsla::new(180.0, 0.5, 0.5, 1.0);
461        let srgba: Srgba = okhsla.into();
462        let okhsla2: Okhsla = srgba.into();
463        assert_approx_eq!(okhsla.hue, okhsla2.hue, 0.001);
464        assert_approx_eq!(okhsla.saturation, okhsla2.saturation, 0.001);
465        assert_approx_eq!(okhsla.lightness, okhsla2.lightness, 0.001);
466        assert_approx_eq!(okhsla.alpha, okhsla2.alpha, 0.001);
467    }
468
469    #[test]
470    fn test_to_from_srgba_2() {
471        for color in TEST_COLORS.iter() {
472            let rgb2: Srgba = (color.okhsl).into();
473            let okhsl: Okhsla = (color.rgb).into();
474            assert!(
475                color.rgb.distance(&rgb2) < 0.001,
476                "{}: {:?} != {:?}",
477                color.name,
478                color.rgb,
479                rgb2
480            );
481            let msg = alloc::format!(
482                "{}: expected {:?}, got {:?}",
483                color.name,
484                color.okhsl,
485                okhsl
486            );
487            assert_approx_eq!(color.okhsl.hue, okhsl.hue, 0.001, msg);
488            assert_approx_eq!(color.okhsl.saturation, okhsl.saturation, 0.001, msg);
489            assert_approx_eq!(color.okhsl.lightness, okhsl.lightness, 0.001, msg);
490            assert_approx_eq!(color.okhsl.alpha, okhsl.alpha, 0.001, msg);
491        }
492    }
493
494    #[test]
495    fn test_to_from_linear() {
496        let okhsla = Okhsla::new(180.0, 0.5, 0.5, 1.0);
497        let linear: LinearRgba = okhsla.into();
498        let okhsla2: Okhsla = linear.into();
499        assert_approx_eq!(okhsla.hue, okhsla2.hue, 0.001);
500        assert_approx_eq!(okhsla.saturation, okhsla2.saturation, 0.001);
501        assert_approx_eq!(okhsla.lightness, okhsla2.lightness, 0.001);
502        assert_approx_eq!(okhsla.alpha, okhsla2.alpha, 0.001);
503    }
504}