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