palette/
okhwb.rs

1//! Types for the Okhwb color space.
2
3use core::fmt::Debug;
4
5pub use alpha::Okhwba;
6
7use crate::{
8    angle::FromAngle,
9    convert::FromColorUnclamped,
10    num::{Arithmetics, One},
11    stimulus::{FromStimulus, Stimulus},
12    white_point::D65,
13    HasBoolMask, Okhsv, OklabHue,
14};
15
16pub use self::properties::Iter;
17
18#[cfg(feature = "random")]
19pub use self::random::UniformOkhwb;
20
21mod alpha;
22mod properties;
23#[cfg(feature = "random")]
24mod random;
25#[cfg(test)]
26#[cfg(feature = "approx")]
27mod visual_eq;
28
29/// A Hue/Whiteness/Blackness representation of [`Oklab`][crate::Oklab] in the
30/// `sRGB` color space, similar to [`Hwb`][crate::Okhwb].
31#[derive(Debug, Copy, Clone, ArrayCast, FromColorUnclamped, WithAlpha)]
32#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
33#[palette(
34    palette_internal,
35    white_point = "D65",
36    component = "T",
37    skip_derives(Okhwb, Okhsv)
38)]
39#[repr(C)]
40pub struct Okhwb<T = f32> {
41    /// The hue of the color, in degrees of a circle.
42    ///
43    /// For fully saturated, bright colors
44    /// * 0° corresponds to a kind of magenta-pink (RBG #ff0188),
45    /// * 90° to a kind of yellow (RBG RGB #ffcb00)
46    /// * 180° to a kind of cyan (RBG #00ffe1) and
47    /// * 240° to a kind of blue (RBG #00aefe).
48    ///
49    /// For s == 0 or v == 0, the hue is irrelevant.
50    #[palette(unsafe_same_layout_as = "T")]
51    pub hue: OklabHue<T>,
52
53    /// The amount of white, mixed in the pure hue, ranging from `0.0` to `1.0`.
54    /// `0.0` produces pure, possibly black color. `1.0` a white or grey.
55    pub whiteness: T,
56
57    /// The amount of black, mixed in the pure hue, ranging from `0.0` to `1.0`.
58    /// `0.0` produces a pure bright or whitened color. `1.0` a black or grey.
59    pub blackness: T,
60}
61
62impl<T> Okhwb<T> {
63    /// Create an `Okhwb` color.
64    pub fn new<H: Into<OklabHue<T>>>(hue: H, whiteness: T, blackness: T) -> Self {
65        let hue = hue.into();
66        Self {
67            hue,
68            whiteness,
69            blackness,
70        }
71    }
72
73    /// Create an `Okhwb` color. This is the same as `Okhwb::new` without the
74    /// generic hue type. It's temporary until `const fn` supports traits.
75    pub const fn new_const(hue: OklabHue<T>, whiteness: T, blackness: T) -> Self {
76        Self {
77            hue,
78            whiteness,
79            blackness,
80        }
81    }
82    /// Convert into another component type.
83    pub fn into_format<U>(self) -> Okhwb<U>
84    where
85        U: FromStimulus<T> + FromAngle<T>,
86    {
87        Okhwb {
88            hue: self.hue.into_format(),
89            whiteness: U::from_stimulus(self.whiteness),
90            blackness: U::from_stimulus(self.blackness),
91        }
92    }
93    /// Convert to a `(h, w, b)` tuple.
94    pub fn into_components(self) -> (OklabHue<T>, T, T) {
95        (self.hue, self.whiteness, self.blackness)
96    }
97
98    /// Convert from a `(h, w, b)` tuple.
99    pub fn from_components<H: Into<OklabHue<T>>>((hue, whiteness, blackness): (H, T, T)) -> Self {
100        Self::new(hue, whiteness, blackness)
101    }
102}
103
104impl<T> Okhwb<T>
105where
106    T: Stimulus,
107{
108    /// Return the `whiteness` value minimum.
109    pub fn min_whiteness() -> T {
110        T::zero()
111    }
112
113    /// Return the `whiteness` value maximum.
114    pub fn max_whiteness() -> T {
115        T::max_intensity()
116    }
117
118    /// Return the `blackness` value minimum.
119    pub fn min_blackness() -> T {
120        T::zero()
121    }
122
123    /// Return the `blackness` value maximum.
124    pub fn max_blackness() -> T {
125        T::max_intensity()
126    }
127}
128
129impl_reference_component_methods_hue!(Okhwb, [whiteness, blackness]);
130impl_struct_of_arrays_methods_hue!(Okhwb, [whiteness, blackness]);
131
132impl<T> FromColorUnclamped<Okhsv<T>> for Okhwb<T>
133where
134    T: One + Arithmetics,
135{
136    /// Converts `lab` to `Okhwb` in the bounds of sRGB.
137    fn from_color_unclamped(hsv: Okhsv<T>) -> Self {
138        // See <https://bottosson.github.io/posts/colorpicker/#okhwb>.
139        Self {
140            hue: hsv.hue,
141            whiteness: (T::one() - hsv.saturation) * &hsv.value,
142            blackness: T::one() - hsv.value,
143        }
144    }
145}
146
147impl<T> HasBoolMask for Okhwb<T>
148where
149    T: HasBoolMask,
150{
151    type Mask = T::Mask;
152}
153
154impl<T> Default for Okhwb<T>
155where
156    T: Stimulus,
157    OklabHue<T>: Default,
158{
159    fn default() -> Okhwb<T> {
160        Okhwb::new(
161            OklabHue::default(),
162            Self::min_whiteness(),
163            Self::max_blackness(),
164        )
165    }
166}
167
168#[cfg(feature = "bytemuck")]
169unsafe impl<T> bytemuck::Zeroable for Okhwb<T> where T: bytemuck::Zeroable {}
170
171#[cfg(feature = "bytemuck")]
172unsafe impl<T> bytemuck::Pod for Okhwb<T> where T: bytemuck::Pod {}
173
174#[cfg(test)]
175mod tests {
176    use crate::Okhwb;
177
178    test_convert_into_from_xyz!(Okhwb);
179
180    #[cfg(feature = "approx")]
181    mod conversion {
182        use crate::{
183            convert::FromColorUnclamped, encoding, rgb::Rgb, visual::VisuallyEqual, LinSrgb, Okhsv,
184            Okhwb, Oklab,
185        };
186
187        #[cfg_attr(miri, ignore)]
188        #[test]
189        fn test_roundtrip_okhwb_oklab_is_original() {
190            let colors = [
191                (
192                    "red",
193                    Oklab::from_color_unclamped(LinSrgb::new(1.0, 0.0, 0.0)),
194                ),
195                (
196                    "green",
197                    Oklab::from_color_unclamped(LinSrgb::new(0.0, 1.0, 0.0)),
198                ),
199                (
200                    "cyan",
201                    Oklab::from_color_unclamped(LinSrgb::new(0.0, 1.0, 1.0)),
202                ),
203                (
204                    "magenta",
205                    Oklab::from_color_unclamped(LinSrgb::new(1.0, 0.0, 1.0)),
206                ),
207                (
208                    "white",
209                    Oklab::from_color_unclamped(LinSrgb::new(1.0, 1.0, 1.0)),
210                ),
211                (
212                    "black",
213                    Oklab::from_color_unclamped(LinSrgb::new(0.0, 0.0, 0.0)),
214                ),
215                (
216                    "grey",
217                    Oklab::from_color_unclamped(LinSrgb::new(0.5, 0.5, 0.5)),
218                ),
219                (
220                    "yellow",
221                    Oklab::from_color_unclamped(LinSrgb::new(1.0, 1.0, 0.0)),
222                ),
223                (
224                    "blue",
225                    Oklab::from_color_unclamped(LinSrgb::new(0.0, 0.0, 1.0)),
226                ),
227            ];
228
229            const EPSILON: f64 = 1e-14;
230
231            for (name, color) in colors {
232                let rgb: Rgb<encoding::Srgb, u8> =
233                    crate::Srgb::<f64>::from_color_unclamped(color).into_format();
234                println!(
235                    "\n\
236                    roundtrip of {} (#{:x} / {:?})\n\
237                    =================================================",
238                    name, rgb, color
239                );
240
241                let okhsv = Okhsv::from_color_unclamped(color);
242                println!("Okhsv: {:?}", okhsv);
243                let okhwb_from_okhsv = Okhwb::from_color_unclamped(okhsv);
244                let okhwb = Okhwb::from_color_unclamped(color);
245                println!("Okhwb: {:?}", okhwb);
246                assert!(
247                Okhwb::visually_eq(okhwb, okhwb_from_okhsv, EPSILON),
248                "Okhwb \n{:?} is not visually equal to Okhwb from Okhsv \n{:?}\nwithin EPSILON {}",
249                okhwb,
250                okhwb_from_okhsv,
251                EPSILON
252            );
253                let okhsv_from_okhwb = Okhsv::from_color_unclamped(okhwb);
254                assert!(
255                Okhsv::visually_eq(okhsv, okhsv_from_okhwb, EPSILON),
256                "Okhsv \n{:?} is not visually equal to Okhsv from Okhsv from Okhwb \n{:?}\nwithin EPSILON {}",
257                okhsv,
258                okhsv_from_okhwb, EPSILON
259            );
260
261                let roundtrip_color = Oklab::from_color_unclamped(okhwb);
262                let oklab_from_okhsv = Oklab::from_color_unclamped(okhsv);
263                assert!(
264                    Oklab::visually_eq(roundtrip_color, oklab_from_okhsv, EPSILON),
265                    "roundtrip color \n{:?} does not match \n{:?}\nwithin EPSILON {}",
266                    roundtrip_color,
267                    oklab_from_okhsv,
268                    EPSILON
269                );
270                assert!(
271                    Oklab::visually_eq(roundtrip_color, color, EPSILON),
272                    "'{}' failed.\n\
273                {:?}\n\
274                !=\n\
275                \n{:?}\n",
276                    name,
277                    roundtrip_color,
278                    color
279                );
280            }
281        }
282    }
283
284    struct_of_arrays_tests!(
285        Okhwb[hue, whiteness, blackness],
286        super::Okhwba::new(0.1f32, 0.2, 0.3, 0.4),
287        super::Okhwba::new(0.2, 0.3, 0.4, 0.5),
288        super::Okhwba::new(0.3, 0.4, 0.5, 0.6)
289    );
290}