1use 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#[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 #[palette(unsafe_same_layout_as = "T")]
51 pub hue: OklabHue<T>,
52
53 pub whiteness: T,
56
57 pub blackness: T,
60}
61
62impl<T> Okhwb<T> {
63 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 pub const fn new_const(hue: OklabHue<T>, whiteness: T, blackness: T) -> Self {
76 Self {
77 hue,
78 whiteness,
79 blackness,
80 }
81 }
82 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 pub fn into_components(self) -> (OklabHue<T>, T, T) {
95 (self.hue, self.whiteness, self.blackness)
96 }
97
98 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 pub fn min_whiteness() -> T {
110 T::zero()
111 }
112
113 pub fn max_whiteness() -> T {
115 T::max_intensity()
116 }
117
118 pub fn min_blackness() -> T {
120 T::zero()
121 }
122
123 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 fn from_color_unclamped(hsv: Okhsv<T>) -> Self {
138 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}