1use 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
20pub type Hwba<S = Srgb, T = f32> = Alpha<Hwb<S, T>, T>;
23
24#[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 #[palette(unsafe_same_layout_as = "T")]
60 pub hue: RgbHue<T>,
61
62 pub whiteness: T,
67
68 pub blackness: T,
74
75 #[cfg_attr(feature = "serializing", serde(skip))]
78 #[palette(unsafe_zero_sized)]
79 pub standard: PhantomData<S>,
80}
81
82impl<T> Hwb<Srgb, T> {
83 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 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 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 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 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 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 pub fn into_components(self) -> (RgbHue<T>, T, T) {
136 (self.hue, self.whiteness, self.blackness)
137 }
138
139 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 pub fn min_whiteness() -> T {
161 T::zero()
162 }
163
164 pub fn max_whiteness() -> T {
166 T::max_intensity()
167 }
168
169 pub fn min_blackness() -> T {
171 T::zero()
172 }
173
174 pub fn max_blackness() -> T {
176 T::max_intensity()
177 }
178}
179
180impl<T, A> Alpha<Hwb<Srgb, T>, A> {
182 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 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
196impl<S, T, A> Alpha<Hwb<S, T>, A> {
198 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 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 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 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 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 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}