bevy_color/
hwba.rs

1//! Implementation of the Hue-Whiteness-Blackness (HWB) color model as described
2//! in [_HWB - A More Intuitive Hue-Based Color Model_] by _Smith et al_.
3//!
4//! [_HWB - A More Intuitive Hue-Based Color Model_]: https://web.archive.org/web/20240226005220/http://alvyray.com/Papers/CG/HWB_JGTv208.pdf
5use crate::{
6    Alpha, ColorToComponents, Gray, Hue, Lcha, LinearRgba, Mix, Srgba, StandardColor, Xyza,
7};
8use bevy_math::{ops, Vec3, Vec4};
9#[cfg(feature = "bevy_reflect")]
10use bevy_reflect::prelude::*;
11
12/// Color in Hue-Whiteness-Blackness (HWB) color space with alpha.
13/// Further information on this color model can be found on [Wikipedia](https://en.wikipedia.org/wiki/HWB_color_model).
14#[doc = include_str!("../docs/conversion.md")]
15/// <div>
16#[doc = include_str!("../docs/diagrams/model_graph.svg")]
17/// </div>
18#[derive(Debug, Clone, Copy, PartialEq)]
19#[cfg_attr(
20    feature = "bevy_reflect",
21    derive(Reflect),
22    reflect(Clone, PartialEq, Default)
23)]
24#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
25#[cfg_attr(
26    all(feature = "serialize", feature = "bevy_reflect"),
27    reflect(Serialize, Deserialize)
28)]
29pub struct Hwba {
30    /// The hue channel. [0.0, 360.0]
31    pub hue: f32,
32    /// The whiteness channel. [0.0, 1.0]
33    pub whiteness: f32,
34    /// The blackness channel. [0.0, 1.0]
35    pub blackness: f32,
36    /// The alpha channel. [0.0, 1.0]
37    pub alpha: f32,
38}
39
40impl StandardColor for Hwba {}
41
42impl Hwba {
43    /// Construct a new [`Hwba`] color from components.
44    ///
45    /// # Arguments
46    ///
47    /// * `hue` - Hue channel. [0.0, 360.0]
48    /// * `whiteness` - Whiteness channel. [0.0, 1.0]
49    /// * `blackness` - Blackness channel. [0.0, 1.0]
50    /// * `alpha` - Alpha channel. [0.0, 1.0]
51    pub const fn new(hue: f32, whiteness: f32, blackness: f32, alpha: f32) -> Self {
52        Self {
53            hue,
54            whiteness,
55            blackness,
56            alpha,
57        }
58    }
59
60    /// Construct a new [`Hwba`] color from (h, s, l) components, with the default alpha (1.0).
61    ///
62    /// # Arguments
63    ///
64    /// * `hue` - Hue channel. [0.0, 360.0]
65    /// * `whiteness` - Whiteness channel. [0.0, 1.0]
66    /// * `blackness` - Blackness channel. [0.0, 1.0]
67    pub const fn hwb(hue: f32, whiteness: f32, blackness: f32) -> Self {
68        Self::new(hue, whiteness, blackness, 1.0)
69    }
70
71    /// Return a copy of this color with the whiteness channel set to the given value.
72    pub const fn with_whiteness(self, whiteness: f32) -> Self {
73        Self { whiteness, ..self }
74    }
75
76    /// Return a copy of this color with the blackness channel set to the given value.
77    pub const fn with_blackness(self, blackness: f32) -> Self {
78        Self { blackness, ..self }
79    }
80}
81
82impl Default for Hwba {
83    fn default() -> Self {
84        Self::new(0., 0., 1., 1.)
85    }
86}
87
88impl Mix for Hwba {
89    #[inline]
90    fn mix(&self, other: &Self, factor: f32) -> Self {
91        let n_factor = 1.0 - factor;
92        Self {
93            hue: crate::color_ops::lerp_hue(self.hue, other.hue, factor),
94            whiteness: self.whiteness * n_factor + other.whiteness * factor,
95            blackness: self.blackness * n_factor + other.blackness * factor,
96            alpha: self.alpha * n_factor + other.alpha * factor,
97        }
98    }
99}
100
101impl Gray for Hwba {
102    const BLACK: Self = Self::new(0., 0., 1., 1.);
103    const WHITE: Self = Self::new(0., 1., 0., 1.);
104}
105
106impl Alpha for Hwba {
107    #[inline]
108    fn with_alpha(&self, alpha: f32) -> Self {
109        Self { alpha, ..*self }
110    }
111
112    #[inline]
113    fn alpha(&self) -> f32 {
114        self.alpha
115    }
116
117    #[inline]
118    fn set_alpha(&mut self, alpha: f32) {
119        self.alpha = alpha;
120    }
121}
122
123impl Hue for Hwba {
124    #[inline]
125    fn with_hue(&self, hue: f32) -> Self {
126        Self { hue, ..*self }
127    }
128
129    #[inline]
130    fn hue(&self) -> f32 {
131        self.hue
132    }
133
134    #[inline]
135    fn set_hue(&mut self, hue: f32) {
136        self.hue = hue;
137    }
138}
139
140impl ColorToComponents for Hwba {
141    fn to_f32_array(self) -> [f32; 4] {
142        [self.hue, self.whiteness, self.blackness, self.alpha]
143    }
144
145    fn to_f32_array_no_alpha(self) -> [f32; 3] {
146        [self.hue, self.whiteness, self.blackness]
147    }
148
149    fn to_vec4(self) -> Vec4 {
150        Vec4::new(self.hue, self.whiteness, self.blackness, self.alpha)
151    }
152
153    fn to_vec3(self) -> Vec3 {
154        Vec3::new(self.hue, self.whiteness, self.blackness)
155    }
156
157    fn from_f32_array(color: [f32; 4]) -> Self {
158        Self {
159            hue: color[0],
160            whiteness: color[1],
161            blackness: color[2],
162            alpha: color[3],
163        }
164    }
165
166    fn from_f32_array_no_alpha(color: [f32; 3]) -> Self {
167        Self {
168            hue: color[0],
169            whiteness: color[1],
170            blackness: color[2],
171            alpha: 1.0,
172        }
173    }
174
175    fn from_vec4(color: Vec4) -> Self {
176        Self {
177            hue: color[0],
178            whiteness: color[1],
179            blackness: color[2],
180            alpha: color[3],
181        }
182    }
183
184    fn from_vec3(color: Vec3) -> Self {
185        Self {
186            hue: color[0],
187            whiteness: color[1],
188            blackness: color[2],
189            alpha: 1.0,
190        }
191    }
192}
193
194impl From<Srgba> for Hwba {
195    fn from(
196        Srgba {
197            red,
198            green,
199            blue,
200            alpha,
201        }: Srgba,
202    ) -> Self {
203        // Based on "HWB - A More Intuitive Hue-Based Color Model" Appendix B
204        let x_max = 0f32.max(red).max(green).max(blue);
205        let x_min = 1f32.min(red).min(green).min(blue);
206
207        let chroma = x_max - x_min;
208
209        let hue = if chroma == 0.0 {
210            0.0
211        } else if red == x_max {
212            60.0 * (green - blue) / chroma
213        } else if green == x_max {
214            60.0 * (2.0 + (blue - red) / chroma)
215        } else {
216            60.0 * (4.0 + (red - green) / chroma)
217        };
218        let hue = if hue < 0.0 { 360.0 + hue } else { hue };
219
220        let whiteness = x_min;
221        let blackness = 1.0 - x_max;
222
223        Hwba {
224            hue,
225            whiteness,
226            blackness,
227            alpha,
228        }
229    }
230}
231
232impl From<Hwba> for Srgba {
233    fn from(
234        Hwba {
235            hue,
236            whiteness,
237            blackness,
238            alpha,
239        }: Hwba,
240    ) -> Self {
241        // Based on "HWB - A More Intuitive Hue-Based Color Model" Appendix B
242        let w = whiteness;
243        let v = 1. - blackness;
244
245        let h = (hue % 360.) / 60.;
246        let i = ops::floor(h);
247        let f = h - i;
248
249        let i = i as u8;
250
251        let f = if i % 2 == 0 { f } else { 1. - f };
252
253        let n = w + f * (v - w);
254
255        let (red, green, blue) = match i {
256            0 => (v, n, w),
257            1 => (n, v, w),
258            2 => (w, v, n),
259            3 => (w, n, v),
260            4 => (n, w, v),
261            5 => (v, w, n),
262            _ => unreachable!("i is bounded in [0, 6)"),
263        };
264
265        Srgba::new(red, green, blue, alpha)
266    }
267}
268
269// Derived Conversions
270
271impl From<LinearRgba> for Hwba {
272    fn from(value: LinearRgba) -> Self {
273        Srgba::from(value).into()
274    }
275}
276
277impl From<Hwba> for LinearRgba {
278    fn from(value: Hwba) -> Self {
279        Srgba::from(value).into()
280    }
281}
282
283impl From<Lcha> for Hwba {
284    fn from(value: Lcha) -> Self {
285        Srgba::from(value).into()
286    }
287}
288
289impl From<Hwba> for Lcha {
290    fn from(value: Hwba) -> Self {
291        Srgba::from(value).into()
292    }
293}
294
295impl From<Xyza> for Hwba {
296    fn from(value: Xyza) -> Self {
297        Srgba::from(value).into()
298    }
299}
300
301impl From<Hwba> for Xyza {
302    fn from(value: Hwba) -> Self {
303        Srgba::from(value).into()
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310    use crate::{
311        color_difference::EuclideanDistance, test_colors::TEST_COLORS, testing::assert_approx_eq,
312    };
313
314    #[test]
315    fn test_to_from_srgba() {
316        let hwba = Hwba::new(0.0, 0.5, 0.5, 1.0);
317        let srgba: Srgba = hwba.into();
318        let hwba2: Hwba = srgba.into();
319        assert_approx_eq!(hwba.hue, hwba2.hue, 0.001);
320        assert_approx_eq!(hwba.whiteness, hwba2.whiteness, 0.001);
321        assert_approx_eq!(hwba.blackness, hwba2.blackness, 0.001);
322        assert_approx_eq!(hwba.alpha, hwba2.alpha, 0.001);
323    }
324
325    #[test]
326    fn test_to_from_srgba_2() {
327        for color in TEST_COLORS.iter() {
328            let rgb2: Srgba = (color.hwb).into();
329            let hwb2: Hwba = (color.rgb).into();
330            assert!(
331                color.rgb.distance(&rgb2) < 0.00001,
332                "{}: {:?} != {:?}",
333                color.name,
334                color.rgb,
335                rgb2
336            );
337            assert_approx_eq!(color.hwb.hue, hwb2.hue, 0.001);
338            assert_approx_eq!(color.hwb.whiteness, hwb2.whiteness, 0.001);
339            assert_approx_eq!(color.hwb.blackness, hwb2.blackness, 0.001);
340            assert_approx_eq!(color.hwb.alpha, hwb2.alpha, 0.001);
341        }
342    }
343}