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