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