Skip to main content

bevy_color/
okhwba.rs

1use crate::{
2    okhsva::Okhsva, Alpha, ColorToComponents, Gray, Hsla, Hsva, Hue, Hwba, Laba, Lcha, LinearRgba,
3    Mix, Okhsla, Oklaba, Oklcha, Srgba, StandardColor, Xyza,
4};
5use bevy_math::{Vec3, Vec4};
6#[cfg(feature = "bevy_reflect")]
7use bevy_reflect::prelude::*;
8
9/// Color in Okhwb color space with alpha.
10/// Further information on this color model can be found on <https://bottosson.github.io/posts/colorpicker>.
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 Okhwba {
27    /// The hue channel. [0.0, 360.0]
28    pub hue: f32,
29    /// The whiteness channel. [0.0, 1.0]
30    pub whiteness: f32,
31    /// The blackness channel. [0.0, 1.0]
32    pub blackness: f32,
33    /// The alpha channel. [0.0, 1.0]
34    pub alpha: f32,
35}
36
37impl StandardColor for Okhwba {}
38
39impl Okhwba {
40    /// Construct a new [`Okhwba`] color from components.
41    ///
42    /// # Arguments
43    ///
44    /// * `hue` - Hue channel. [0.0, 360.0]
45    /// * `whiteness` - Whiteness channel. [0.0, 1.0]
46    /// * `blackness` - Blackness channel. [0.0, 1.0]
47    /// * `alpha` - Alpha channel. [0.0, 1.0]
48    pub const fn new(hue: f32, whiteness: f32, blackness: f32, alpha: f32) -> Self {
49        Self {
50            hue,
51            whiteness,
52            blackness,
53            alpha,
54        }
55    }
56
57    /// Construct a new [`Okhwba`] color from (h, w, b) components, with the default alpha (1.0).
58    ///
59    /// # Arguments
60    ///
61    /// * `hue` - Hue channel. [0.0, 360.0]
62    /// * `whiteness` - Whiteness channel. [0.0, 1.0]
63    /// * `blackness` - Blackness channel. [0.0, 1.0]
64    pub const fn hwb(hue: f32, whiteness: f32, blackness: f32) -> Self {
65        Self::new(hue, whiteness, blackness, 1.0)
66    }
67
68    /// Return a copy of this color with the whiteness channel set to the given value.
69    pub const fn with_whiteness(self, whiteness: f32) -> Self {
70        Self { whiteness, ..self }
71    }
72
73    /// Return a copy of this color with the blackness channel set to the given value.
74    pub const fn with_blackness(self, blackness: f32) -> Self {
75        Self { blackness, ..self }
76    }
77}
78
79impl Default for Okhwba {
80    fn default() -> Self {
81        Self::new(0., 0., 1., 1.)
82    }
83}
84
85impl Mix for Okhwba {
86    #[inline]
87    fn mix(&self, other: &Self, factor: f32) -> Self {
88        let n_factor = 1.0 - factor;
89        Self {
90            hue: crate::color_ops::lerp_hue(self.hue, other.hue, factor),
91            whiteness: self.whiteness * n_factor + other.whiteness * factor,
92            blackness: self.blackness * n_factor + other.blackness * factor,
93            alpha: self.alpha * n_factor + other.alpha * factor,
94        }
95    }
96}
97
98impl Gray for Okhwba {
99    const BLACK: Self = Self::new(0., 0., 1., 1.);
100    const WHITE: Self = Self::new(0., 1., 0., 1.);
101}
102
103impl Alpha for Okhwba {
104    #[inline]
105    fn with_alpha(&self, alpha: f32) -> Self {
106        Self { alpha, ..*self }
107    }
108
109    #[inline]
110    fn alpha(&self) -> f32 {
111        self.alpha
112    }
113
114    #[inline]
115    fn set_alpha(&mut self, alpha: f32) {
116        self.alpha = alpha;
117    }
118}
119
120impl Hue for Okhwba {
121    #[inline]
122    fn with_hue(&self, hue: f32) -> Self {
123        Self { hue, ..*self }
124    }
125
126    #[inline]
127    fn hue(&self) -> f32 {
128        self.hue
129    }
130
131    #[inline]
132    fn set_hue(&mut self, hue: f32) {
133        self.hue = hue;
134    }
135}
136
137impl ColorToComponents for Okhwba {
138    fn to_f32_array(self) -> [f32; 4] {
139        [self.hue, self.whiteness, self.blackness, self.alpha]
140    }
141
142    fn to_f32_array_no_alpha(self) -> [f32; 3] {
143        [self.hue, self.whiteness, self.blackness]
144    }
145
146    fn to_vec4(self) -> Vec4 {
147        Vec4::new(self.hue, self.whiteness, self.blackness, self.alpha)
148    }
149
150    fn to_vec3(self) -> Vec3 {
151        Vec3::new(self.hue, self.whiteness, self.blackness)
152    }
153
154    fn from_f32_array(color: [f32; 4]) -> Self {
155        Self {
156            hue: color[0],
157            whiteness: color[1],
158            blackness: color[2],
159            alpha: color[3],
160        }
161    }
162
163    fn from_f32_array_no_alpha(color: [f32; 3]) -> Self {
164        Self {
165            hue: color[0],
166            whiteness: color[1],
167            blackness: color[2],
168            alpha: 1.0,
169        }
170    }
171
172    fn from_vec4(color: Vec4) -> Self {
173        Self {
174            hue: color[0],
175            whiteness: color[1],
176            blackness: color[2],
177            alpha: color[3],
178        }
179    }
180
181    fn from_vec3(color: Vec3) -> Self {
182        Self {
183            hue: color[0],
184            whiteness: color[1],
185            blackness: color[2],
186            alpha: 1.0,
187        }
188    }
189}
190
191impl From<Okhsva> for Okhwba {
192    fn from(
193        Okhsva {
194            hue,
195            saturation,
196            value,
197            alpha,
198        }: Okhsva,
199    ) -> Self {
200        // Based on https://bottosson.github.io/posts/colorpicker/#okhwb
201        let whiteness = (1. - saturation) * value;
202        let blackness = 1. - value;
203
204        Okhwba::new(hue, whiteness, blackness, alpha)
205    }
206}
207
208impl From<Okhwba> for Okhsva {
209    fn from(
210        Okhwba {
211            hue,
212            whiteness,
213            blackness,
214            alpha,
215        }: Okhwba,
216    ) -> Self {
217        // Based on https://bottosson.github.io/posts/colorpicker/#okhwb
218        let value = 1. - blackness;
219        let saturation = if value != 0. {
220            1. - (whiteness / value)
221        } else {
222            0.
223        };
224
225        Okhsva::new(hue, saturation, value, alpha)
226    }
227}
228
229// Derived Conversions
230
231impl From<LinearRgba> for Okhwba {
232    fn from(value: LinearRgba) -> Self {
233        Okhsva::from(value).into()
234    }
235}
236
237impl From<Okhwba> for LinearRgba {
238    fn from(value: Okhwba) -> Self {
239        Okhsva::from(value).into()
240    }
241}
242
243impl From<Srgba> for Okhwba {
244    fn from(value: Srgba) -> Self {
245        Okhsva::from(value).into()
246    }
247}
248
249impl From<Okhwba> for Srgba {
250    fn from(value: Okhwba) -> Self {
251        Okhsva::from(value).into()
252    }
253}
254
255impl From<Hwba> for Okhwba {
256    fn from(value: Hwba) -> Self {
257        Okhsva::from(value).into()
258    }
259}
260
261impl From<Okhwba> for Hwba {
262    fn from(value: Okhwba) -> Self {
263        Okhsva::from(value).into()
264    }
265}
266
267impl From<Lcha> for Okhwba {
268    fn from(value: Lcha) -> Self {
269        Okhsva::from(value).into()
270    }
271}
272
273impl From<Okhwba> for Lcha {
274    fn from(value: Okhwba) -> Self {
275        Okhsva::from(value).into()
276    }
277}
278
279impl From<Xyza> for Okhwba {
280    fn from(value: Xyza) -> Self {
281        Okhsva::from(value).into()
282    }
283}
284
285impl From<Okhwba> for Xyza {
286    fn from(value: Okhwba) -> Self {
287        Okhsva::from(value).into()
288    }
289}
290
291impl From<Okhsla> for Okhwba {
292    fn from(value: Okhsla) -> Self {
293        Okhsva::from(value).into()
294    }
295}
296
297impl From<Okhwba> for Okhsla {
298    fn from(value: Okhwba) -> Self {
299        Okhsva::from(value).into()
300    }
301}
302
303impl From<Hsla> for Okhwba {
304    fn from(value: Hsla) -> Self {
305        Okhsva::from(value).into()
306    }
307}
308
309impl From<Okhwba> for Hsla {
310    fn from(value: Okhwba) -> Self {
311        Okhsva::from(value).into()
312    }
313}
314
315impl From<Hsva> for Okhwba {
316    fn from(value: Hsva) -> Self {
317        Okhsva::from(value).into()
318    }
319}
320
321impl From<Okhwba> for Hsva {
322    fn from(value: Okhwba) -> Self {
323        Okhsva::from(value).into()
324    }
325}
326
327impl From<Laba> for Okhwba {
328    fn from(value: Laba) -> Self {
329        Okhsva::from(value).into()
330    }
331}
332
333impl From<Okhwba> for Laba {
334    fn from(value: Okhwba) -> Self {
335        Okhsva::from(value).into()
336    }
337}
338
339impl From<Oklaba> for Okhwba {
340    fn from(value: Oklaba) -> Self {
341        Okhsva::from(value).into()
342    }
343}
344
345impl From<Okhwba> for Oklaba {
346    fn from(value: Okhwba) -> Self {
347        Okhsva::from(value).into()
348    }
349}
350
351impl From<Oklcha> for Okhwba {
352    fn from(value: Oklcha) -> Self {
353        Okhsva::from(value).into()
354    }
355}
356
357impl From<Okhwba> for Oklcha {
358    fn from(value: Okhwba) -> Self {
359        Okhsva::from(value).into()
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366    use crate::{
367        color_difference::EuclideanDistance, test_colors::TEST_COLORS, testing::assert_approx_eq,
368    };
369
370    #[test]
371    fn test_from_okhsva() {
372        // Test `saturation == 0.0`
373        let okhsva = Okhsva::new(90.0, 0.0, 0.4, 1.0);
374        let okhwba: Okhwba = okhsva.into();
375        let okhsva2: Okhsva = okhwba.into();
376        assert_approx_eq!(okhwba.hue, 90.0, 0.001);
377        assert_approx_eq!(okhwba.whiteness, 0.4, 0.001);
378        assert_approx_eq!(okhwba.blackness, 0.6, 0.001);
379        assert_approx_eq!(okhwba.alpha, 1.0, 0.001);
380
381        assert_approx_eq!(okhsva.hue, okhsva2.hue, 0.001);
382        assert_approx_eq!(okhsva.saturation, okhsva2.saturation, 0.001);
383        assert_approx_eq!(okhsva.value, okhsva2.value, 0.001);
384        assert_approx_eq!(okhsva.alpha, okhsva2.alpha, 0.001);
385
386        // Test `saturation == 1.0 && value == 1.0`
387        let okhsva = Okhsva::new(270.0, 1.0, 1.0, 1.0);
388        let okhwba: Okhwba = okhsva.into();
389        let okhsva2: Okhsva = okhwba.into();
390        assert_approx_eq!(okhwba.hue, 270.0, 0.001);
391        assert_approx_eq!(okhwba.whiteness, 0.0, 0.001);
392        assert_approx_eq!(okhwba.blackness, 0.0, 0.001);
393        assert_approx_eq!(okhwba.alpha, 1.0, 0.001);
394
395        assert_approx_eq!(okhsva.hue, okhsva2.hue, 0.001);
396        assert_approx_eq!(okhsva.saturation, okhsva2.saturation, 0.001);
397        assert_approx_eq!(okhsva.value, okhsva2.value, 0.001);
398        assert_approx_eq!(okhsva.alpha, okhsva2.alpha, 0.001);
399
400        // Test `saturation == 0.0 && value == 1.0` (white)
401        let okhsva = Okhsva::new(0.0, 0.0, 1.0, 1.0);
402        let okhwba: Okhwba = okhsva.into();
403        let okhsva2: Okhsva = okhwba.into();
404        assert_approx_eq!(okhwba.hue, 0.0, 0.001);
405        assert_approx_eq!(okhwba.whiteness, 1.0, 0.001);
406        assert_approx_eq!(okhwba.blackness, 0.0, 0.001);
407        assert_approx_eq!(okhwba.alpha, 1.0, 0.001);
408
409        assert_approx_eq!(okhsva.hue, okhsva2.hue, 0.001);
410        assert_approx_eq!(okhsva.saturation, okhsva2.saturation, 0.001);
411        assert_approx_eq!(okhsva.value, okhsva2.value, 0.001);
412        assert_approx_eq!(okhsva.alpha, okhsva2.alpha, 0.001);
413
414        // Test `value == 0.0` (black)
415        let okhsva = Okhsva::new(0.0, 0.8, 0.0, 1.0);
416        let okhwba: Okhwba = okhsva.into();
417        let okhsva2: Okhsva = okhwba.into();
418        assert_approx_eq!(okhwba.hue, 0.0, 0.001);
419        assert_approx_eq!(okhwba.whiteness, 0.0, 0.001);
420        assert_approx_eq!(okhwba.blackness, 1.0, 0.001);
421        assert_approx_eq!(okhwba.alpha, 1.0, 0.001);
422
423        assert_approx_eq!(okhsva.hue, okhsva2.hue, 0.001);
424        assert_approx_eq!(0.0, okhsva2.saturation, 0.001);
425        assert_approx_eq!(okhsva.value, okhsva2.value, 0.001);
426        assert_approx_eq!(okhsva.alpha, okhsva2.alpha, 0.001);
427    }
428
429    #[test]
430    fn test_from_oklaba() {
431        // Test `oklab_l == 0.0`
432        let oklaba = Oklaba::new(0.0, 0.5, 0.5, 1.0);
433        let okhwba: Okhwba = oklaba.into();
434        let oklaba2: Oklaba = okhwba.into();
435        assert_approx_eq!(okhwba.hue, 0.0, 0.001);
436        assert_approx_eq!(okhwba.whiteness, 0.0, 0.001);
437        assert_approx_eq!(okhwba.blackness, 1.0, 0.001);
438        assert_approx_eq!(okhwba.alpha, 1.0, 0.001);
439
440        assert_approx_eq!(oklaba.lightness, oklaba2.lightness, 0.001);
441        assert_approx_eq!(0.0, oklaba2.a, 0.001);
442        assert_approx_eq!(0.0, oklaba2.b, 0.001);
443        assert_approx_eq!(oklaba.alpha, oklaba2.alpha, 0.001);
444
445        // Test `oklab_l == 1.0`
446        let oklaba = Oklaba::new(1.0, 0.5, 0.5, 1.0);
447        let okhwba: Okhwba = oklaba.into();
448        let oklaba2: Oklaba = okhwba.into();
449        assert_approx_eq!(okhwba.hue, 0.0, 0.001);
450        assert_approx_eq!(okhwba.whiteness, 1.0, 0.001);
451        assert_approx_eq!(okhwba.blackness, 0.0, 0.001);
452        assert_approx_eq!(okhwba.alpha, 1.0, 0.001);
453
454        assert_approx_eq!(oklaba.lightness, oklaba2.lightness, 0.001);
455        assert_approx_eq!(0.0, oklaba2.a, 0.001);
456        assert_approx_eq!(0.0, oklaba2.b, 0.001);
457        assert_approx_eq!(oklaba.alpha, oklaba2.alpha, 0.001);
458
459        // Test `oklab_a == 0.0 && oklab_b ==0.0` (C == 0.0)
460        let oklaba = Oklaba::new(0.5, 0.0, 0.0, 1.0);
461        let okhwba: Okhwba = oklaba.into();
462        let oklaba2: Oklaba = okhwba.into();
463        assert_approx_eq!(okhwba.hue, 0.0, 0.001);
464        assert_approx_eq!(okhwba.whiteness, 0.42114055, 0.001);
465        assert_approx_eq!(okhwba.blackness, 0.57885945, 0.001);
466        assert_approx_eq!(okhwba.alpha, 1.0, 0.001);
467
468        assert_approx_eq!(oklaba.lightness, oklaba2.lightness, 0.001);
469        assert_approx_eq!(0.0, oklaba2.a, 0.001);
470        assert_approx_eq!(0.0, oklaba2.b, 0.001);
471        assert_approx_eq!(oklaba.alpha, oklaba2.alpha, 0.001);
472    }
473
474    #[test]
475    fn test_to_from_srgba() {
476        let okhwba = Okhwba::new(180.0, 0.5, 0.5, 1.0);
477        let srgba: Srgba = okhwba.into();
478        let okhwba2: Okhwba = srgba.into();
479        assert_approx_eq!(okhwba.hue, okhwba2.hue, 0.001);
480        assert_approx_eq!(okhwba.whiteness, okhwba2.whiteness, 0.001);
481        assert_approx_eq!(okhwba.blackness, okhwba2.blackness, 0.001);
482        assert_approx_eq!(okhwba.alpha, okhwba2.alpha, 0.001);
483    }
484
485    #[test]
486    fn test_to_from_linear() {
487        let okhwba = Okhwba::new(180.0, 0.5, 0.5, 1.0);
488        let linear: LinearRgba = okhwba.into();
489        let okhwba2: Okhwba = linear.into();
490        assert_approx_eq!(okhwba.hue, okhwba2.hue, 0.001);
491        assert_approx_eq!(okhwba.whiteness, okhwba2.whiteness, 0.001);
492        assert_approx_eq!(okhwba.blackness, okhwba2.blackness, 0.001);
493        assert_approx_eq!(okhwba.alpha, okhwba2.alpha, 0.001);
494    }
495
496    #[test]
497    fn test_to_from_srgba_2() {
498        for color in TEST_COLORS.iter() {
499            let rgb2: Srgba = (color.okhwb).into();
500            let okhwb: Okhwba = (color.rgb).into();
501            assert!(
502                color.rgb.distance(&rgb2) < 0.001,
503                "{}: {:?} != {:?}",
504                color.name,
505                color.rgb,
506                rgb2,
507            );
508            let msg = alloc::format!(
509                "{}: expected {:?}, got {:?}",
510                color.name,
511                color.okhwb,
512                okhwb
513            );
514            assert_approx_eq!(color.okhwb.hue, okhwb.hue, 0.001, msg);
515            assert_approx_eq!(color.okhwb.whiteness, okhwb.whiteness, 0.001, msg);
516            assert_approx_eq!(color.okhwb.blackness, okhwb.blackness, 0.001, msg);
517            assert_approx_eq!(color.okhwb.alpha, okhwb.alpha, 0.001, msg);
518        }
519    }
520}