palette/
hwb.rs

1//! Types for the HWB color space.
2
3use core::{any::TypeId, marker::PhantomData};
4
5#[cfg(feature = "random")]
6use crate::hsv::UniformHsv;
7
8use crate::{
9    angle::FromAngle,
10    bool_mask::{HasBoolMask, LazySelect, Select},
11    convert::FromColorUnclamped,
12    encoding::Srgb,
13    hues::RgbHueIter,
14    num::{Arithmetics, One, PartialCmp, Real},
15    rgb::{RgbSpace, RgbStandard},
16    stimulus::{FromStimulus, Stimulus},
17    Alpha, FromColor, Hsv, RgbHue, Xyz,
18};
19
20/// Linear HWB with an alpha component. See the [`Hwba` implementation in
21/// `Alpha`](crate::Alpha#Hwba).
22pub type Hwba<S = Srgb, T = f32> = Alpha<Hwb<S, T>, T>;
23
24/// HWB color space.
25///
26/// HWB is a cylindrical version of [RGB](crate::rgb::Rgb) and it's very
27/// closely related to [HSV](crate::Hsv). It describes colors with a
28/// starting hue, then a degree of whiteness and blackness to mix into that
29/// base hue.
30///
31/// HWB component values are typically real numbers (such as floats), but may
32/// also be converted to and from `u8` for storage and interoperability
33/// purposes. The hue is then within the range `[0, 255]`.
34///
35/// ```
36/// use approx::assert_relative_eq;
37/// use palette::Hwb;
38///
39/// let hwb_u8 = Hwb::new_srgb(128u8, 85, 51);
40/// let hwb_f32 = hwb_u8.into_format::<f32>();
41///
42/// assert_relative_eq!(hwb_f32, Hwb::new(180.0, 1.0 / 3.0, 0.2));
43/// ```
44///
45/// It is very intuitive for humans to use and many color-pickers are based on
46/// the HWB color system
47#[derive(Debug, ArrayCast, FromColorUnclamped, WithAlpha)]
48#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
49#[palette(
50    palette_internal,
51    rgb_standard = "S",
52    component = "T",
53    skip_derives(Hsv, Hwb)
54)]
55#[repr(C)]
56pub struct Hwb<S = Srgb, T = f32> {
57    /// The hue of the color, in degrees. Decides if it's red, blue, purple,
58    /// etc. Same as the hue for HSL and HSV.
59    #[palette(unsafe_same_layout_as = "T")]
60    pub hue: RgbHue<T>,
61
62    /// The whiteness of the color. It specifies the amount white to mix into
63    /// the hue. It varies from 0 to 1, with 1 being always full white and 0
64    /// always being the color shade (a mixture of a pure hue with black)
65    /// chosen with the other two controls.
66    pub whiteness: T,
67
68    /// The blackness of the color. It specifies the amount black to mix into
69    /// the hue. It varies from 0 to 1, with 1 being always full black and
70    /// 0 always being the color tint (a mixture of a pure hue with white)
71    /// chosen with the other two
72    //controls.
73    pub blackness: T,
74
75    /// The white point and RGB primaries this color is adapted to. The default
76    /// is the sRGB standard.
77    #[cfg_attr(feature = "serializing", serde(skip))]
78    #[palette(unsafe_zero_sized)]
79    pub standard: PhantomData<S>,
80}
81
82impl<T> Hwb<Srgb, T> {
83    /// Create an sRGB HWB color. This method can be used instead of `Hwb::new`
84    /// to help type inference.
85    pub fn new_srgb<H: Into<RgbHue<T>>>(hue: H, whiteness: T, blackness: T) -> Self {
86        Self::new_const(hue.into(), whiteness, blackness)
87    }
88
89    /// Create an sRGB HWB color. This is the same as `Hwb::new_srgb` without the
90    /// generic hue type. It's temporary until `const fn` supports traits.
91    pub const fn new_srgb_const(hue: RgbHue<T>, whiteness: T, blackness: T) -> Self {
92        Self::new_const(hue, whiteness, blackness)
93    }
94}
95
96impl<S, T> Hwb<S, T> {
97    /// Create an HWB color.
98    pub fn new<H: Into<RgbHue<T>>>(hue: H, whiteness: T, blackness: T) -> Self {
99        Self::new_const(hue.into(), whiteness, blackness)
100    }
101
102    /// Create an HWB color. This is the same as `Hwb::new` without the generic
103    /// hue type. It's temporary until `const fn` supports traits.
104    pub const fn new_const(hue: RgbHue<T>, whiteness: T, blackness: T) -> Self {
105        Hwb {
106            hue,
107            whiteness,
108            blackness,
109            standard: PhantomData,
110        }
111    }
112
113    /// Convert into another component type.
114    pub fn into_format<U>(self) -> Hwb<S, U>
115    where
116        U: FromStimulus<T> + FromAngle<T>,
117    {
118        Hwb {
119            hue: self.hue.into_format(),
120            whiteness: U::from_stimulus(self.whiteness),
121            blackness: U::from_stimulus(self.blackness),
122            standard: PhantomData,
123        }
124    }
125
126    /// Convert from another component type.
127    pub fn from_format<U>(color: Hwb<S, U>) -> Self
128    where
129        T: FromStimulus<U> + FromAngle<U>,
130    {
131        color.into_format()
132    }
133
134    /// Convert to a `(hue, whiteness, blackness)` tuple.
135    pub fn into_components(self) -> (RgbHue<T>, T, T) {
136        (self.hue, self.whiteness, self.blackness)
137    }
138
139    /// Convert from a `(hue, whiteness, blackness)` tuple.
140    pub fn from_components<H: Into<RgbHue<T>>>((hue, whiteness, blackness): (H, T, T)) -> Self {
141        Self::new(hue, whiteness, blackness)
142    }
143
144    #[inline]
145    fn reinterpret_as<St>(self) -> Hwb<St, T> {
146        Hwb {
147            hue: self.hue,
148            whiteness: self.whiteness,
149            blackness: self.blackness,
150            standard: PhantomData,
151        }
152    }
153}
154
155impl<S, T> Hwb<S, T>
156where
157    T: Stimulus,
158{
159    /// Return the `whiteness` value minimum.
160    pub fn min_whiteness() -> T {
161        T::zero()
162    }
163
164    /// Return the `whiteness` value maximum.
165    pub fn max_whiteness() -> T {
166        T::max_intensity()
167    }
168
169    /// Return the `blackness` value minimum.
170    pub fn min_blackness() -> T {
171        T::zero()
172    }
173
174    /// Return the `blackness` value maximum.
175    pub fn max_blackness() -> T {
176        T::max_intensity()
177    }
178}
179
180///<span id="Hwba"></span>[`Hwba`](crate::Hwba) implementations.
181impl<T, A> Alpha<Hwb<Srgb, T>, A> {
182    /// Create an sRGB HWB color with transparency. This method can be used
183    /// instead of `Hwba::new` to help type inference.
184    pub fn new_srgb<H: Into<RgbHue<T>>>(hue: H, whiteness: T, blackness: T, alpha: A) -> Self {
185        Self::new_const(hue.into(), whiteness, blackness, alpha)
186    }
187
188    /// Create an sRGB HWB color with transparency. This is the same as
189    /// `Hwba::new_srgb` without the generic hue type. It's temporary until `const
190    /// fn` supports traits.
191    pub const fn new_srgb_const(hue: RgbHue<T>, whiteness: T, blackness: T, alpha: A) -> Self {
192        Self::new_const(hue, whiteness, blackness, alpha)
193    }
194}
195
196///<span id="Hwba"></span>[`Hwba`](crate::Hwba) implementations.
197impl<S, T, A> Alpha<Hwb<S, T>, A> {
198    /// Create an HWB color with transparency.
199    pub fn new<H: Into<RgbHue<T>>>(hue: H, whiteness: T, blackness: T, alpha: A) -> Self {
200        Self::new_const(hue.into(), whiteness, blackness, alpha)
201    }
202
203    /// Create an HWB color with transparency. This is the same as `Hwba::new` without the
204    /// generic hue type. It's temporary until `const fn` supports traits.
205    pub const fn new_const(hue: RgbHue<T>, whiteness: T, blackness: T, alpha: A) -> Self {
206        Alpha {
207            color: Hwb::new_const(hue, whiteness, blackness),
208            alpha,
209        }
210    }
211
212    /// Convert into another component type.
213    pub fn into_format<U, B>(self) -> Alpha<Hwb<S, U>, B>
214    where
215        U: FromStimulus<T> + FromAngle<T>,
216        B: FromStimulus<A>,
217    {
218        Alpha {
219            color: self.color.into_format(),
220            alpha: B::from_stimulus(self.alpha),
221        }
222    }
223
224    /// Convert from another component type.
225    pub fn from_format<U, B>(color: Alpha<Hwb<S, U>, B>) -> Self
226    where
227        T: FromStimulus<U> + FromAngle<U>,
228        A: FromStimulus<B>,
229    {
230        color.into_format()
231    }
232
233    /// Convert to a `(hue, whiteness, blackness, alpha)` tuple.
234    pub fn into_components(self) -> (RgbHue<T>, T, T, A) {
235        (
236            self.color.hue,
237            self.color.whiteness,
238            self.color.blackness,
239            self.alpha,
240        )
241    }
242
243    /// Convert from a `(hue, whiteness, blackness, alpha)` tuple.
244    pub fn from_components<H: Into<RgbHue<T>>>(
245        (hue, whiteness, blackness, alpha): (H, T, T, A),
246    ) -> Self {
247        Self::new(hue, whiteness, blackness, alpha)
248    }
249}
250
251impl_reference_component_methods_hue!(Hwb<S>, [whiteness, blackness], standard);
252impl_struct_of_arrays_methods_hue!(Hwb<S>, [whiteness, blackness], standard);
253
254impl<S1, S2, T> FromColorUnclamped<Hwb<S1, T>> for Hwb<S2, T>
255where
256    S1: RgbStandard + 'static,
257    S2: RgbStandard + 'static,
258    S1::Space: RgbSpace<WhitePoint = <S2::Space as RgbSpace>::WhitePoint>,
259    Hsv<S1, T>: FromColorUnclamped<Hwb<S1, T>>,
260    Hsv<S2, T>: FromColorUnclamped<Hsv<S1, T>>,
261    Self: FromColorUnclamped<Hsv<S2, T>>,
262{
263    #[inline]
264    fn from_color_unclamped(hwb: Hwb<S1, T>) -> Self {
265        if TypeId::of::<S1>() == TypeId::of::<S2>() {
266            hwb.reinterpret_as()
267        } else {
268            let hsv = Hsv::<S1, T>::from_color_unclamped(hwb);
269            let converted_hsv = Hsv::<S2, T>::from_color_unclamped(hsv);
270            Self::from_color_unclamped(converted_hsv)
271        }
272    }
273}
274
275impl<S, T> FromColorUnclamped<Hsv<S, T>> for Hwb<S, T>
276where
277    T: One + Arithmetics,
278{
279    #[inline]
280    fn from_color_unclamped(color: Hsv<S, T>) -> Self {
281        Hwb {
282            hue: color.hue,
283            whiteness: (T::one() - color.saturation) * &color.value,
284            blackness: (T::one() - color.value),
285            standard: PhantomData,
286        }
287    }
288}
289
290impl_tuple_conversion_hue!(Hwb<S> as (H, T, T), RgbHue);
291impl_is_within_bounds_hwb!(Hwb<S> where T: Stimulus);
292impl_clamp_hwb!(Hwb<S> phantom: standard where T: Stimulus);
293
294impl_mix_hue!(Hwb<S> {whiteness, blackness} phantom: standard);
295impl_lighten_hwb!(Hwb<S> phantom: standard where T: Stimulus);
296impl_hue_ops!(Hwb<S>, RgbHue);
297
298impl<S, T> HasBoolMask for Hwb<S, T>
299where
300    T: HasBoolMask,
301{
302    type Mask = T::Mask;
303}
304
305impl<S, T> Default for Hwb<S, T>
306where
307    T: Stimulus,
308    RgbHue<T>: Default,
309{
310    fn default() -> Hwb<S, T> {
311        Hwb::new(
312            RgbHue::default(),
313            Self::min_whiteness(),
314            Self::max_blackness(),
315        )
316    }
317}
318
319impl_color_add!(Hwb<S>, [hue, whiteness, blackness], standard);
320impl_color_sub!(Hwb<S>, [hue, whiteness, blackness], standard);
321
322impl_array_casts!(Hwb<S, T>, [T; 3]);
323impl_simd_array_conversion_hue!(Hwb<S>, [whiteness, blackness], standard);
324impl_struct_of_array_traits_hue!(Hwb<S>, RgbHueIter, [whiteness, blackness], standard);
325
326impl_copy_clone!(Hwb<S>, [hue, whiteness, blackness], standard);
327impl_eq_hue!(Hwb<S>, RgbHue, [hue, whiteness, blackness]);
328
329#[allow(deprecated)]
330impl<S, T> crate::RelativeContrast for Hwb<S, T>
331where
332    T: Real + Arithmetics + PartialCmp,
333    T::Mask: LazySelect<T>,
334    S: RgbStandard,
335    Xyz<<S::Space as RgbSpace>::WhitePoint, T>: FromColor<Self>,
336{
337    type Scalar = T;
338
339    #[inline]
340    fn get_contrast_ratio(self, other: Self) -> T {
341        let xyz1 = Xyz::from_color(self);
342        let xyz2 = Xyz::from_color(other);
343
344        crate::contrast_ratio(xyz1.y, xyz2.y)
345    }
346}
347
348impl_rand_traits_hwb_cone!(
349    UniformHwb,
350    Hwb<S>,
351    UniformHsv,
352    Hsv {
353        height: value,
354        radius: saturation
355    }
356    phantom: standard: PhantomData<S>
357);
358
359#[cfg(feature = "bytemuck")]
360unsafe impl<S, T> bytemuck::Zeroable for Hwb<S, T> where T: bytemuck::Zeroable {}
361
362#[cfg(feature = "bytemuck")]
363unsafe impl<S: 'static, T> bytemuck::Pod for Hwb<S, T> where T: bytemuck::Pod {}
364
365#[cfg(test)]
366mod test {
367    use super::Hwb;
368
369    test_convert_into_from_xyz!(Hwb);
370
371    #[cfg(feature = "approx")]
372    mod conversion {
373        use crate::{FromColor, Hwb, Srgb};
374
375        #[test]
376        fn red() {
377            let a = Hwb::from_color(Srgb::new(1.0, 0.0, 0.0));
378            let b = Hwb::new_srgb(0.0, 0.0, 0.0);
379            assert_relative_eq!(a, b);
380        }
381
382        #[test]
383        fn orange() {
384            let a = Hwb::from_color(Srgb::new(1.0, 0.5, 0.0));
385            let b = Hwb::new_srgb(30.0, 0.0, 0.0);
386            assert_relative_eq!(a, b);
387        }
388
389        #[test]
390        fn green() {
391            let a = Hwb::from_color(Srgb::new(0.0, 1.0, 0.0));
392            let b = Hwb::new_srgb(120.0, 0.0, 0.0);
393            assert_relative_eq!(a, b);
394        }
395
396        #[test]
397        fn blue() {
398            let a = Hwb::from_color(Srgb::new(0.0, 0.0, 1.0));
399            let b = Hwb::new_srgb(240.0, 0.0, 0.0);
400            assert_relative_eq!(a, b);
401        }
402
403        #[test]
404        fn purple() {
405            let a = Hwb::from_color(Srgb::new(0.5, 0.0, 1.0));
406            let b = Hwb::new_srgb(270.0, 0.0, 0.0);
407            assert_relative_eq!(a, b);
408        }
409    }
410
411    #[cfg(feature = "approx")]
412    mod clamp {
413        use crate::{Clamp, Hwb};
414
415        #[test]
416        fn clamp_invalid() {
417            let expected = Hwb::new_srgb(240.0, 0.0, 0.0);
418            let clamped = Hwb::new_srgb(240.0, -3.0, -4.0).clamp();
419            assert_relative_eq!(expected, clamped);
420        }
421
422        #[test]
423        fn clamp_none() {
424            let expected = Hwb::new_srgb(240.0, 0.3, 0.7);
425            let clamped = Hwb::new_srgb(240.0, 0.3, 0.7).clamp();
426            assert_relative_eq!(expected, clamped);
427        }
428        #[test]
429        fn clamp_over_one() {
430            let expected = Hwb::new_srgb(240.0, 0.2, 0.8);
431            let clamped = Hwb::new_srgb(240.0, 5.0, 20.0).clamp();
432            assert_relative_eq!(expected, clamped);
433        }
434        #[test]
435        fn clamp_under_one() {
436            let expected = Hwb::new_srgb(240.0, 0.3, 0.1);
437            let clamped = Hwb::new_srgb(240.0, 0.3, 0.1).clamp();
438            assert_relative_eq!(expected, clamped);
439        }
440    }
441
442    raw_pixel_conversion_tests!(Hwb<crate::encoding::Srgb>: hue, whiteness, blackness);
443    raw_pixel_conversion_fail_tests!(Hwb<crate::encoding::Srgb>: hue, whiteness, blackness);
444
445    #[test]
446    fn check_min_max_components() {
447        use crate::encoding::Srgb;
448
449        assert_eq!(Hwb::<Srgb>::min_whiteness(), 0.0,);
450        assert_eq!(Hwb::<Srgb>::min_blackness(), 0.0,);
451        assert_eq!(Hwb::<Srgb>::max_whiteness(), 1.0,);
452        assert_eq!(Hwb::<Srgb>::max_blackness(), 1.0,);
453    }
454
455    struct_of_arrays_tests!(
456        Hwb<crate::encoding::Srgb>[hue, whiteness, blackness] phantom: standard,
457        super::Hwba::new(0.1f32, 0.2, 0.3, 0.4),
458        super::Hwba::new(0.2, 0.3, 0.4, 0.5),
459        super::Hwba::new(0.3, 0.4, 0.5, 0.6)
460    );
461
462    #[cfg(feature = "serializing")]
463    #[test]
464    fn serialize() {
465        let serialized = ::serde_json::to_string(&Hwb::new_srgb(0.3, 0.8, 0.1)).unwrap();
466
467        assert_eq!(serialized, r#"{"hue":0.3,"whiteness":0.8,"blackness":0.1}"#);
468    }
469
470    #[cfg(feature = "serializing")]
471    #[test]
472    fn deserialize() {
473        let deserialized: Hwb =
474            ::serde_json::from_str(r#"{"hue":0.3,"whiteness":0.8,"blackness":0.1}"#).unwrap();
475
476        assert_eq!(deserialized, Hwb::new(0.3, 0.8, 0.1));
477    }
478
479    test_uniform_distribution! {
480        Hwb<crate::encoding::Srgb, f32> as crate::rgb::Rgb {
481            red: (0.0, 1.0),
482            green: (0.0, 1.0),
483            blue: (0.0, 1.0)
484        },
485        min: Hwb::new(0.0f32, 0.0, 0.0),
486        max: Hwb::new(360.0, 1.0, 1.0)
487    }
488}