bevy_color/
oklaba.rs

1use crate::{
2    color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ColorToComponents,
3    Gray, Hsla, Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza,
4};
5use bevy_math::{ops, FloatPow, Vec3, Vec4};
6#[cfg(feature = "bevy_reflect")]
7use bevy_reflect::prelude::*;
8
9/// Color in Oklab 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 Oklaba {
26    /// The 'lightness' channel. [0.0, 1.0]
27    pub lightness: f32,
28    /// The 'a' channel. [-1.0, 1.0]
29    pub a: f32,
30    /// The 'b' channel. [-1.0, 1.0]
31    pub b: f32,
32    /// The alpha channel. [0.0, 1.0]
33    pub alpha: f32,
34}
35
36impl StandardColor for Oklaba {}
37
38impl_componentwise_vector_space!(Oklaba, [lightness, a, b, alpha]);
39
40impl Oklaba {
41    /// Construct a new [`Oklaba`] color from components.
42    ///
43    /// # Arguments
44    ///
45    /// * `lightness` - Lightness channel. [0.0, 1.0]
46    /// * `a` - Green-red channel. [-1.0, 1.0]
47    /// * `b` - Blue-yellow channel. [-1.0, 1.0]
48    /// * `alpha` - Alpha channel. [0.0, 1.0]
49    pub const fn new(lightness: f32, a: f32, b: f32, alpha: f32) -> Self {
50        Self {
51            lightness,
52            a,
53            b,
54            alpha,
55        }
56    }
57
58    /// Construct a new [`Oklaba`] color from (l, a, b) components, with the default alpha (1.0).
59    ///
60    /// # Arguments
61    ///
62    /// * `lightness` - Lightness channel. [0.0, 1.0]
63    /// * `a` - Green-red channel. [-1.0, 1.0]
64    /// * `b` - Blue-yellow channel. [-1.0, 1.0]
65    pub const fn lab(lightness: f32, a: f32, b: f32) -> Self {
66        Self {
67            lightness,
68            a,
69            b,
70            alpha: 1.0,
71        }
72    }
73
74    /// Return a copy of this color with the 'lightness' channel set to the given value.
75    pub const fn with_lightness(self, lightness: f32) -> Self {
76        Self { lightness, ..self }
77    }
78
79    /// Return a copy of this color with the 'a' channel set to the given value.
80    pub const fn with_a(self, a: f32) -> Self {
81        Self { a, ..self }
82    }
83
84    /// Return a copy of this color with the 'b' channel set to the given value.
85    pub const fn with_b(self, b: f32) -> Self {
86        Self { b, ..self }
87    }
88}
89
90impl Default for Oklaba {
91    fn default() -> Self {
92        Self::new(1., 0., 0., 1.)
93    }
94}
95
96impl Mix for Oklaba {
97    #[inline]
98    fn mix(&self, other: &Self, factor: f32) -> Self {
99        let n_factor = 1.0 - factor;
100        Self {
101            lightness: self.lightness * n_factor + other.lightness * factor,
102            a: self.a * n_factor + other.a * factor,
103            b: self.b * n_factor + other.b * factor,
104            alpha: self.alpha * n_factor + other.alpha * factor,
105        }
106    }
107}
108
109impl Gray for Oklaba {
110    const BLACK: Self = Self::new(0., 0., 0., 1.);
111    const WHITE: Self = Self::new(1.0, 0.0, 0.000000059604645, 1.0);
112}
113
114impl Alpha for Oklaba {
115    #[inline]
116    fn with_alpha(&self, alpha: f32) -> Self {
117        Self { alpha, ..*self }
118    }
119
120    #[inline]
121    fn alpha(&self) -> f32 {
122        self.alpha
123    }
124
125    #[inline]
126    fn set_alpha(&mut self, alpha: f32) {
127        self.alpha = alpha;
128    }
129}
130
131impl Luminance for Oklaba {
132    #[inline]
133    fn with_luminance(&self, lightness: f32) -> Self {
134        Self { lightness, ..*self }
135    }
136
137    fn luminance(&self) -> f32 {
138        self.lightness
139    }
140
141    fn darker(&self, amount: f32) -> Self {
142        Self::new(
143            (self.lightness - amount).max(0.),
144            self.a,
145            self.b,
146            self.alpha,
147        )
148    }
149
150    fn lighter(&self, amount: f32) -> Self {
151        Self::new(
152            (self.lightness + amount).min(1.),
153            self.a,
154            self.b,
155            self.alpha,
156        )
157    }
158}
159
160impl EuclideanDistance for Oklaba {
161    #[inline]
162    fn distance_squared(&self, other: &Self) -> f32 {
163        (self.lightness - other.lightness).squared()
164            + (self.a - other.a).squared()
165            + (self.b - other.b).squared()
166    }
167}
168
169impl ColorToComponents for Oklaba {
170    fn to_f32_array(self) -> [f32; 4] {
171        [self.lightness, self.a, self.b, self.alpha]
172    }
173
174    fn to_f32_array_no_alpha(self) -> [f32; 3] {
175        [self.lightness, self.a, self.b]
176    }
177
178    fn to_vec4(self) -> Vec4 {
179        Vec4::new(self.lightness, self.a, self.b, self.alpha)
180    }
181
182    fn to_vec3(self) -> Vec3 {
183        Vec3::new(self.lightness, self.a, self.b)
184    }
185
186    fn from_f32_array(color: [f32; 4]) -> Self {
187        Self {
188            lightness: color[0],
189            a: color[1],
190            b: color[2],
191            alpha: color[3],
192        }
193    }
194
195    fn from_f32_array_no_alpha(color: [f32; 3]) -> Self {
196        Self {
197            lightness: color[0],
198            a: color[1],
199            b: color[2],
200            alpha: 1.0,
201        }
202    }
203
204    fn from_vec4(color: Vec4) -> Self {
205        Self {
206            lightness: color[0],
207            a: color[1],
208            b: color[2],
209            alpha: color[3],
210        }
211    }
212
213    fn from_vec3(color: Vec3) -> Self {
214        Self {
215            lightness: color[0],
216            a: color[1],
217            b: color[2],
218            alpha: 1.0,
219        }
220    }
221}
222
223impl From<LinearRgba> for Oklaba {
224    fn from(value: LinearRgba) -> Self {
225        let LinearRgba {
226            red,
227            green,
228            blue,
229            alpha,
230        } = value;
231        // From https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
232        // Float literals are truncated to avoid excessive precision.
233        let l = 0.41222146 * red + 0.53633255 * green + 0.051445995 * blue;
234        let m = 0.2119035 * red + 0.6806995 * green + 0.10739696 * blue;
235        let s = 0.08830246 * red + 0.28171885 * green + 0.6299787 * blue;
236        let l_ = ops::cbrt(l);
237        let m_ = ops::cbrt(m);
238        let s_ = ops::cbrt(s);
239        let l = 0.21045426 * l_ + 0.7936178 * m_ - 0.004072047 * s_;
240        let a = 1.9779985 * l_ - 2.4285922 * m_ + 0.4505937 * s_;
241        let b = 0.025904037 * l_ + 0.78277177 * m_ - 0.80867577 * s_;
242        Oklaba::new(l, a, b, alpha)
243    }
244}
245
246impl From<Oklaba> for LinearRgba {
247    fn from(value: Oklaba) -> Self {
248        let Oklaba {
249            lightness,
250            a,
251            b,
252            alpha,
253        } = value;
254
255        // From https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
256        // Float literals are truncated to avoid excessive precision.
257        let l_ = lightness + 0.39633778 * a + 0.21580376 * b;
258        let m_ = lightness - 0.105561346 * a - 0.06385417 * b;
259        let s_ = lightness - 0.08948418 * a - 1.2914855 * b;
260
261        let l = l_ * l_ * l_;
262        let m = m_ * m_ * m_;
263        let s = s_ * s_ * s_;
264
265        let red = 4.0767417 * l - 3.3077116 * m + 0.23096994 * s;
266        let green = -1.268438 * l + 2.6097574 * m - 0.34131938 * s;
267        let blue = -0.0041960863 * l - 0.7034186 * m + 1.7076147 * s;
268
269        Self {
270            red,
271            green,
272            blue,
273            alpha,
274        }
275    }
276}
277
278// Derived Conversions
279
280impl From<Hsla> for Oklaba {
281    fn from(value: Hsla) -> Self {
282        LinearRgba::from(value).into()
283    }
284}
285
286impl From<Oklaba> for Hsla {
287    fn from(value: Oklaba) -> Self {
288        LinearRgba::from(value).into()
289    }
290}
291
292impl From<Hsva> for Oklaba {
293    fn from(value: Hsva) -> Self {
294        LinearRgba::from(value).into()
295    }
296}
297
298impl From<Oklaba> for Hsva {
299    fn from(value: Oklaba) -> Self {
300        LinearRgba::from(value).into()
301    }
302}
303
304impl From<Hwba> for Oklaba {
305    fn from(value: Hwba) -> Self {
306        LinearRgba::from(value).into()
307    }
308}
309
310impl From<Oklaba> for Hwba {
311    fn from(value: Oklaba) -> Self {
312        LinearRgba::from(value).into()
313    }
314}
315
316impl From<Lcha> for Oklaba {
317    fn from(value: Lcha) -> Self {
318        LinearRgba::from(value).into()
319    }
320}
321
322impl From<Oklaba> for Lcha {
323    fn from(value: Oklaba) -> Self {
324        LinearRgba::from(value).into()
325    }
326}
327
328impl From<Srgba> for Oklaba {
329    fn from(value: Srgba) -> Self {
330        LinearRgba::from(value).into()
331    }
332}
333
334impl From<Oklaba> for Srgba {
335    fn from(value: Oklaba) -> Self {
336        LinearRgba::from(value).into()
337    }
338}
339
340impl From<Xyza> for Oklaba {
341    fn from(value: Xyza) -> Self {
342        LinearRgba::from(value).into()
343    }
344}
345
346impl From<Oklaba> for Xyza {
347    fn from(value: Oklaba) -> Self {
348        LinearRgba::from(value).into()
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355    use crate::{test_colors::TEST_COLORS, testing::assert_approx_eq};
356
357    #[test]
358    fn test_to_from_srgba() {
359        let oklaba = Oklaba::new(0.5, 0.5, 0.5, 1.0);
360        let srgba: Srgba = oklaba.into();
361        let oklaba2: Oklaba = srgba.into();
362        assert_approx_eq!(oklaba.lightness, oklaba2.lightness, 0.001);
363        assert_approx_eq!(oklaba.a, oklaba2.a, 0.001);
364        assert_approx_eq!(oklaba.b, oklaba2.b, 0.001);
365        assert_approx_eq!(oklaba.alpha, oklaba2.alpha, 0.001);
366    }
367
368    #[test]
369    fn test_to_from_srgba_2() {
370        for color in TEST_COLORS.iter() {
371            let rgb2: Srgba = (color.oklab).into();
372            let oklab: Oklaba = (color.rgb).into();
373            assert!(
374                color.rgb.distance(&rgb2) < 0.0001,
375                "{}: {:?} != {:?}",
376                color.name,
377                color.rgb,
378                rgb2
379            );
380            assert!(
381                color.oklab.distance(&oklab) < 0.0001,
382                "{}: {:?} != {:?}",
383                color.name,
384                color.oklab,
385                oklab
386            );
387        }
388    }
389
390    #[test]
391    fn test_to_from_linear() {
392        let oklaba = Oklaba::new(0.5, 0.5, 0.5, 1.0);
393        let linear: LinearRgba = oklaba.into();
394        let oklaba2: Oklaba = linear.into();
395        assert_approx_eq!(oklaba.lightness, oklaba2.lightness, 0.001);
396        assert_approx_eq!(oklaba.a, oklaba2.a, 0.001);
397        assert_approx_eq!(oklaba.b, oklaba2.b, 0.001);
398        assert_approx_eq!(oklaba.alpha, oklaba2.alpha, 0.001);
399    }
400}