bevy_color/
oklcha.rs

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