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    /// * `alpha` - Alpha channel. [0.0, 1.0]
64    pub const fn lch(lightness: f32, chroma: f32, hue: f32) -> Self {
65        Self::new(lightness, chroma, hue, 1.0)
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    /// Return a copy of this color with the 'chroma' channel set to the given value.
74    pub const fn with_chroma(self, chroma: f32) -> Self {
75        Self { chroma, ..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::Oklcha;
87    /// // Unique color for an entity
88    /// # let entity_index = 123;
89    /// // let entity_index = entity.index();
90    /// let color = Oklcha::sequential_dispersed(entity_index);
91    ///
92    /// // Palette with 5 distinct hues
93    /// let palette = (0..5).map(Oklcha::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::lch(0.75, 0.1, hue)
105    }
106}
107
108impl Default for Oklcha {
109    fn default() -> Self {
110        Self::new(1., 0., 0., 1.)
111    }
112}
113
114impl Mix for Oklcha {
115    #[inline]
116    fn mix(&self, other: &Self, factor: f32) -> Self {
117        let n_factor = 1.0 - factor;
118        Self {
119            lightness: self.lightness * n_factor + other.lightness * factor,
120            chroma: self.chroma * n_factor + other.chroma * factor,
121            hue: crate::color_ops::lerp_hue(self.hue, other.hue, factor),
122            alpha: self.alpha * n_factor + other.alpha * factor,
123        }
124    }
125}
126
127impl Gray for Oklcha {
128    const BLACK: Self = Self::new(0., 0., 0., 1.);
129    const WHITE: Self = Self::new(1.0, 0.000000059604645, 90.0, 1.0);
130}
131
132impl Alpha for Oklcha {
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 Oklcha {
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 Luminance for Oklcha {
167    #[inline]
168    fn with_luminance(&self, lightness: f32) -> Self {
169        Self { lightness, ..*self }
170    }
171
172    fn luminance(&self) -> f32 {
173        self.lightness
174    }
175
176    fn darker(&self, amount: f32) -> Self {
177        Self::new(
178            (self.lightness - amount).max(0.),
179            self.chroma,
180            self.hue,
181            self.alpha,
182        )
183    }
184
185    fn lighter(&self, amount: f32) -> Self {
186        Self::new(
187            (self.lightness + amount).min(1.),
188            self.chroma,
189            self.hue,
190            self.alpha,
191        )
192    }
193}
194
195impl EuclideanDistance for Oklcha {
196    #[inline]
197    fn distance_squared(&self, other: &Self) -> f32 {
198        (self.lightness - other.lightness).squared()
199            + (self.chroma - other.chroma).squared()
200            + (self.hue - other.hue).squared()
201    }
202}
203
204impl ColorToComponents for Oklcha {
205    fn to_f32_array(self) -> [f32; 4] {
206        [self.lightness, self.chroma, self.hue, self.alpha]
207    }
208
209    fn to_f32_array_no_alpha(self) -> [f32; 3] {
210        [self.lightness, self.chroma, self.hue]
211    }
212
213    fn to_vec4(self) -> Vec4 {
214        Vec4::new(self.lightness, self.chroma, self.hue, self.alpha)
215    }
216
217    fn to_vec3(self) -> Vec3 {
218        Vec3::new(self.lightness, self.chroma, self.hue)
219    }
220
221    fn from_f32_array(color: [f32; 4]) -> Self {
222        Self {
223            lightness: color[0],
224            chroma: color[1],
225            hue: color[2],
226            alpha: color[3],
227        }
228    }
229
230    fn from_f32_array_no_alpha(color: [f32; 3]) -> Self {
231        Self {
232            lightness: color[0],
233            chroma: color[1],
234            hue: color[2],
235            alpha: 1.0,
236        }
237    }
238
239    fn from_vec4(color: Vec4) -> Self {
240        Self {
241            lightness: color[0],
242            chroma: color[1],
243            hue: color[2],
244            alpha: color[3],
245        }
246    }
247
248    fn from_vec3(color: Vec3) -> Self {
249        Self {
250            lightness: color[0],
251            chroma: color[1],
252            hue: color[2],
253            alpha: 1.0,
254        }
255    }
256}
257
258impl From<Oklaba> for Oklcha {
259    fn from(
260        Oklaba {
261            lightness,
262            a,
263            b,
264            alpha,
265        }: Oklaba,
266    ) -> Self {
267        let chroma = ops::hypot(a, b);
268        let hue = ops::atan2(b, a).to_degrees();
269
270        let hue = if hue < 0.0 { hue + 360.0 } else { hue };
271
272        Oklcha::new(lightness, chroma, hue, alpha)
273    }
274}
275
276impl From<Oklcha> for Oklaba {
277    fn from(
278        Oklcha {
279            lightness,
280            chroma,
281            hue,
282            alpha,
283        }: Oklcha,
284    ) -> Self {
285        let l = lightness;
286        let (sin, cos) = ops::sin_cos(hue.to_radians());
287        let a = chroma * cos;
288        let b = chroma * sin;
289
290        Oklaba::new(l, a, b, alpha)
291    }
292}
293
294// Derived Conversions
295
296impl From<Hsla> for Oklcha {
297    fn from(value: Hsla) -> Self {
298        Oklaba::from(value).into()
299    }
300}
301
302impl From<Oklcha> for Hsla {
303    fn from(value: Oklcha) -> Self {
304        Oklaba::from(value).into()
305    }
306}
307
308impl From<Hsva> for Oklcha {
309    fn from(value: Hsva) -> Self {
310        Oklaba::from(value).into()
311    }
312}
313
314impl From<Oklcha> for Hsva {
315    fn from(value: Oklcha) -> Self {
316        Oklaba::from(value).into()
317    }
318}
319
320impl From<Hwba> for Oklcha {
321    fn from(value: Hwba) -> Self {
322        Oklaba::from(value).into()
323    }
324}
325
326impl From<Oklcha> for Hwba {
327    fn from(value: Oklcha) -> Self {
328        Oklaba::from(value).into()
329    }
330}
331
332impl From<Laba> for Oklcha {
333    fn from(value: Laba) -> Self {
334        Oklaba::from(value).into()
335    }
336}
337
338impl From<Oklcha> for Laba {
339    fn from(value: Oklcha) -> Self {
340        Oklaba::from(value).into()
341    }
342}
343
344impl From<Lcha> for Oklcha {
345    fn from(value: Lcha) -> Self {
346        Oklaba::from(value).into()
347    }
348}
349
350impl From<Oklcha> for Lcha {
351    fn from(value: Oklcha) -> Self {
352        Oklaba::from(value).into()
353    }
354}
355
356impl From<LinearRgba> for Oklcha {
357    fn from(value: LinearRgba) -> Self {
358        Oklaba::from(value).into()
359    }
360}
361
362impl From<Oklcha> for LinearRgba {
363    fn from(value: Oklcha) -> Self {
364        Oklaba::from(value).into()
365    }
366}
367
368impl From<Srgba> for Oklcha {
369    fn from(value: Srgba) -> Self {
370        Oklaba::from(value).into()
371    }
372}
373
374impl From<Oklcha> for Srgba {
375    fn from(value: Oklcha) -> Self {
376        Oklaba::from(value).into()
377    }
378}
379
380impl From<Xyza> for Oklcha {
381    fn from(value: Xyza) -> Self {
382        Oklaba::from(value).into()
383    }
384}
385
386impl From<Oklcha> for Xyza {
387    fn from(value: Oklcha) -> Self {
388        Oklaba::from(value).into()
389    }
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395    use crate::{test_colors::TEST_COLORS, testing::assert_approx_eq};
396
397    #[test]
398    fn test_to_from_srgba() {
399        let oklcha = Oklcha::new(0.5, 0.5, 180.0, 1.0);
400        let srgba: Srgba = oklcha.into();
401        let oklcha2: Oklcha = srgba.into();
402        assert_approx_eq!(oklcha.lightness, oklcha2.lightness, 0.001);
403        assert_approx_eq!(oklcha.chroma, oklcha2.chroma, 0.001);
404        assert_approx_eq!(oklcha.hue, oklcha2.hue, 0.001);
405        assert_approx_eq!(oklcha.alpha, oklcha2.alpha, 0.001);
406    }
407
408    #[test]
409    fn test_to_from_srgba_2() {
410        for color in TEST_COLORS.iter() {
411            let rgb2: Srgba = (color.oklch).into();
412            let oklch: Oklcha = (color.rgb).into();
413            assert!(
414                color.rgb.distance(&rgb2) < 0.0001,
415                "{}: {:?} != {:?}",
416                color.name,
417                color.rgb,
418                rgb2
419            );
420            assert!(
421                color.oklch.distance(&oklch) < 0.0001,
422                "{}: {:?} != {:?}",
423                color.name,
424                color.oklch,
425                oklch
426            );
427        }
428    }
429
430    #[test]
431    fn test_to_from_linear() {
432        let oklcha = Oklcha::new(0.5, 0.5, 0.5, 1.0);
433        let linear: LinearRgba = oklcha.into();
434        let oklcha2: Oklcha = linear.into();
435        assert_approx_eq!(oklcha.lightness, oklcha2.lightness, 0.001);
436        assert_approx_eq!(oklcha.chroma, oklcha2.chroma, 0.001);
437        assert_approx_eq!(oklcha.hue, oklcha2.hue, 0.001);
438        assert_approx_eq!(oklcha.alpha, oklcha2.alpha, 0.001);
439    }
440}