Skip to main content

bevy_color/
oklaba.rs

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