1use core::{any::TypeId, marker::PhantomData};
4
5use crate::{
6 angle::{FromAngle, RealAngle},
7 bool_mask::{BitOps, BoolMask, HasBoolMask, LazySelect, Select},
8 convert::FromColorUnclamped,
9 encoding::Srgb,
10 hues::RgbHueIter,
11 num::{Arithmetics, IsValidDivisor, MinMax, One, PartialCmp, Real, Zero},
12 rgb::{Rgb, RgbSpace, RgbStandard},
13 stimulus::{FromStimulus, Stimulus},
14 Alpha, FromColor, Hsl, Hwb, RgbHue, Xyz,
15};
16
17pub type Hsva<S = Srgb, T = f32> = Alpha<Hsv<S, T>, T>;
20
21#[derive(Debug, ArrayCast, FromColorUnclamped, WithAlpha)]
44#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
45#[palette(
46 palette_internal,
47 rgb_standard = "S",
48 component = "T",
49 skip_derives(Rgb, Hsl, Hwb, Hsv)
50)]
51#[repr(C)]
52#[doc(alias = "hsb")]
53pub struct Hsv<S = Srgb, T = f32> {
54 #[palette(unsafe_same_layout_as = "T")]
57 pub hue: RgbHue<T>,
58
59 pub saturation: T,
62
63 pub value: T,
67
68 #[cfg_attr(feature = "serializing", serde(skip))]
71 #[palette(unsafe_zero_sized)]
72 pub standard: PhantomData<S>,
73}
74
75impl<T> Hsv<Srgb, T> {
76 pub fn new_srgb<H: Into<RgbHue<T>>>(hue: H, saturation: T, value: T) -> Self {
79 Self::new_const(hue.into(), saturation, value)
80 }
81
82 pub const fn new_srgb_const(hue: RgbHue<T>, saturation: T, value: T) -> Self {
85 Self::new_const(hue, saturation, value)
86 }
87}
88
89impl<S, T> Hsv<S, T> {
90 pub fn new<H: Into<RgbHue<T>>>(hue: H, saturation: T, value: T) -> Self {
92 Self::new_const(hue.into(), saturation, value)
93 }
94
95 pub const fn new_const(hue: RgbHue<T>, saturation: T, value: T) -> Self {
98 Hsv {
99 hue,
100 saturation,
101 value,
102 standard: PhantomData,
103 }
104 }
105
106 pub fn into_format<U>(self) -> Hsv<S, U>
108 where
109 U: FromStimulus<T> + FromAngle<T>,
110 {
111 Hsv {
112 hue: self.hue.into_format(),
113 saturation: U::from_stimulus(self.saturation),
114 value: U::from_stimulus(self.value),
115 standard: PhantomData,
116 }
117 }
118
119 pub fn from_format<U>(color: Hsv<S, U>) -> Self
121 where
122 T: FromStimulus<U> + FromAngle<U>,
123 {
124 color.into_format()
125 }
126
127 pub fn into_components(self) -> (RgbHue<T>, T, T) {
129 (self.hue, self.saturation, self.value)
130 }
131
132 pub fn from_components<H: Into<RgbHue<T>>>((hue, saturation, value): (H, T, T)) -> Self {
134 Self::new(hue, saturation, value)
135 }
136
137 #[inline]
138 fn reinterpret_as<St>(self) -> Hsv<St, T> {
139 Hsv {
140 hue: self.hue,
141 saturation: self.saturation,
142 value: self.value,
143 standard: PhantomData,
144 }
145 }
146}
147
148impl<S, T> Hsv<S, T>
149where
150 T: Stimulus,
151{
152 pub fn min_saturation() -> T {
154 T::zero()
155 }
156
157 pub fn max_saturation() -> T {
159 T::max_intensity()
160 }
161
162 pub fn min_value() -> T {
164 T::zero()
165 }
166
167 pub fn max_value() -> T {
169 T::max_intensity()
170 }
171}
172
173impl<T, A> Alpha<Hsv<Srgb, T>, A> {
175 pub fn new_srgb<H: Into<RgbHue<T>>>(hue: H, saturation: T, value: T, alpha: A) -> Self {
178 Self::new_const(hue.into(), saturation, value, alpha)
179 }
180
181 pub const fn new_srgb_const(hue: RgbHue<T>, saturation: T, value: T, alpha: A) -> Self {
185 Self::new_const(hue, saturation, value, alpha)
186 }
187}
188
189impl<S, T, A> Alpha<Hsv<S, T>, A> {
191 pub fn new<H: Into<RgbHue<T>>>(hue: H, saturation: T, value: T, alpha: A) -> Self {
193 Self::new_const(hue.into(), saturation, value, alpha)
194 }
195
196 pub const fn new_const(hue: RgbHue<T>, saturation: T, value: T, alpha: A) -> Self {
200 Alpha {
201 color: Hsv::new_const(hue, saturation, value),
202 alpha,
203 }
204 }
205
206 pub fn into_format<U, B>(self) -> Alpha<Hsv<S, U>, B>
208 where
209 U: FromStimulus<T> + FromAngle<T>,
210 B: FromStimulus<A>,
211 {
212 Alpha {
213 color: self.color.into_format(),
214 alpha: B::from_stimulus(self.alpha),
215 }
216 }
217
218 pub fn from_format<U, B>(color: Alpha<Hsv<S, U>, B>) -> Self
220 where
221 T: FromStimulus<U> + FromAngle<U>,
222 A: FromStimulus<B>,
223 {
224 color.into_format()
225 }
226
227 pub fn into_components(self) -> (RgbHue<T>, T, T, A) {
229 (
230 self.color.hue,
231 self.color.saturation,
232 self.color.value,
233 self.alpha,
234 )
235 }
236
237 pub fn from_components<H: Into<RgbHue<T>>>(
239 (hue, saturation, value, alpha): (H, T, T, A),
240 ) -> Self {
241 Self::new(hue, saturation, value, alpha)
242 }
243}
244
245impl_reference_component_methods_hue!(Hsv<S>, [saturation, value], standard);
246impl_struct_of_arrays_methods_hue!(Hsv<S>, [saturation, value], standard);
247
248impl<S1, S2, T> FromColorUnclamped<Hsv<S1, T>> for Hsv<S2, T>
249where
250 S1: RgbStandard + 'static,
251 S2: RgbStandard + 'static,
252 S1::Space: RgbSpace<WhitePoint = <S2::Space as RgbSpace>::WhitePoint>,
253 Rgb<S1, T>: FromColorUnclamped<Hsv<S1, T>>,
254 Rgb<S2, T>: FromColorUnclamped<Rgb<S1, T>>,
255 Self: FromColorUnclamped<Rgb<S2, T>>,
256{
257 #[inline]
258 fn from_color_unclamped(hsv: Hsv<S1, T>) -> Self {
259 if TypeId::of::<S1>() == TypeId::of::<S2>() {
260 hsv.reinterpret_as()
261 } else {
262 let rgb = Rgb::<S1, T>::from_color_unclamped(hsv);
263 let converted_rgb = Rgb::<S2, T>::from_color_unclamped(rgb);
264 Self::from_color_unclamped(converted_rgb)
265 }
266 }
267}
268
269impl<S, T> FromColorUnclamped<Rgb<S, T>> for Hsv<S, T>
270where
271 T: RealAngle + One + Zero + MinMax + Arithmetics + PartialCmp + Clone,
272 T::Mask: BoolMask + BitOps + LazySelect<T> + Clone + 'static,
273{
274 fn from_color_unclamped(rgb: Rgb<S, T>) -> Self {
275 let red = rgb.red.max(T::zero());
277 let green = rgb.green.max(T::zero());
278 let blue = rgb.blue.max(T::zero());
279
280 if TypeId::of::<T::Mask>() == TypeId::of::<bool>() {
282 let (max, min, sep, coeff) = {
283 let (max, min, sep, coeff) = if red.gt(&green).is_true() {
284 (red.clone(), green.clone(), green.clone() - &blue, T::zero())
285 } else {
286 (
287 green.clone(),
288 red.clone(),
289 blue.clone() - &red,
290 T::from_f64(2.0),
291 )
292 };
293 if blue.gt(&max).is_true() {
294 (blue, min, red - green, T::from_f64(4.0))
295 } else {
296 let min_val = if blue.lt(&min).is_true() { blue } else { min };
297 (max, min_val, sep, coeff)
298 }
299 };
300
301 let (h, s) = if max.neq(&min).is_true() {
302 let d = max.clone() - min;
303 let h = ((sep / &d) + coeff) * T::from_f64(60.0);
304 let s = d / &max;
305
306 (h, s)
307 } else {
308 (T::zero(), T::zero())
309 };
310 let v = max;
311
312 Hsv {
313 hue: h.into(),
314 saturation: s,
315 value: v,
316 standard: PhantomData,
317 }
318 } else {
319 let six = T::from_f64(6.0);
327
328 let value = red.clone().max(green.clone()).max(blue.clone());
329 let min = red.clone().min(green.clone()).min(blue.clone());
330
331 let chroma = value.clone() - min;
332 let saturation = chroma
333 .eq(&T::zero())
334 .lazy_select(|| T::zero(), || chroma.clone() / &value);
335
336 let x = value.neq(&red);
340 let y = value.eq(&red) | value.neq(&green);
341 let z = value.eq(&red) | value.eq(&green);
342
343 let hue_base = x.clone().select(
346 z.clone().select(T::from_f64(-4.0), T::from_f64(4.0)),
347 T::zero(),
348 ) + &six;
349
350 let red_m = lazy_select! {
355 if x => y.clone().select(red.clone(), -red),
356 else => T::zero(),
357 };
358 let green_m = lazy_select! {
359 if y.clone() => z.clone().select(green.clone(), -green),
360 else => T::zero(),
361 };
362 let blue_m = lazy_select! {
363 if z => y.select(-blue.clone(), blue),
364 else => T::zero(),
365 };
366
367 let hue = lazy_select! {
374 if chroma.eq(&T::zero()) => T::zero(),
375 else => hue_base + (red_m + green_m + blue_m) / &chroma,
376 };
377
378 let hue_sub = hue.gt_eq(&six).select(six, T::zero());
382 let hue = hue - hue_sub;
383
384 Hsv {
385 hue: RgbHue::from_degrees(hue * T::from_f64(60.0)),
386 saturation,
387 value,
388 standard: PhantomData,
389 }
390 }
391 }
392}
393
394impl<S, T> FromColorUnclamped<Hsl<S, T>> for Hsv<S, T>
395where
396 T: Real + Zero + One + IsValidDivisor + Arithmetics + PartialCmp + Clone,
397 T::Mask: LazySelect<T>,
398{
399 #[inline]
400 fn from_color_unclamped(hsl: Hsl<S, T>) -> Self {
401 let x = lazy_select! {
402 if hsl.lightness.lt(&T::from_f64(0.5)) => hsl.lightness.clone(),
403 else => T::one() - &hsl.lightness,
404 } * hsl.saturation;
405
406 let value = hsl.lightness + &x;
407
408 let saturation = lazy_select! {
410 if value.is_valid_divisor() => x * T::from_f64(2.0) / &value,
411 else => T::zero(),
412 };
413
414 Hsv {
415 hue: hsl.hue,
416 saturation,
417 value,
418 standard: PhantomData,
419 }
420 }
421}
422
423impl<S, T> FromColorUnclamped<Hwb<S, T>> for Hsv<S, T>
424where
425 T: One + Zero + IsValidDivisor + Arithmetics,
426 T::Mask: LazySelect<T>,
427{
428 #[inline]
429 fn from_color_unclamped(hwb: Hwb<S, T>) -> Self {
430 let Hwb {
431 hue,
432 whiteness,
433 blackness,
434 ..
435 } = hwb;
436
437 let value = T::one() - blackness;
438
439 let saturation = lazy_select! {
441 if value.is_valid_divisor() => T::one() - (whiteness / &value),
442 else => T::zero(),
443 };
444
445 Hsv {
446 hue,
447 saturation,
448 value,
449 standard: PhantomData,
450 }
451 }
452}
453
454impl_tuple_conversion_hue!(Hsv<S> as (H, T, T), RgbHue);
455
456impl_is_within_bounds! {
457 Hsv<S> {
458 saturation => [Self::min_saturation(), Self::max_saturation()],
459 value => [Self::min_value(), Self::max_value()]
460 }
461 where T: Stimulus
462}
463impl_clamp! {
464 Hsv<S> {
465 saturation => [Self::min_saturation(), Self::max_saturation()],
466 value => [Self::min_value(), Self::max_value()]
467 }
468 other {hue, standard}
469 where T: Stimulus
470}
471
472impl_mix_hue!(Hsv<S> {saturation, value} phantom: standard);
473impl_lighten!(Hsv<S> increase {value => [Self::min_value(), Self::max_value()]} other {hue, saturation} phantom: standard where T: Stimulus);
474impl_saturate!(Hsv<S> increase {saturation => [Self::min_saturation(), Self::max_saturation()]} other {hue, value} phantom: standard where T: Stimulus);
475impl_hue_ops!(Hsv<S>, RgbHue);
476
477impl<S, T> HasBoolMask for Hsv<S, T>
478where
479 T: HasBoolMask,
480{
481 type Mask = T::Mask;
482}
483
484impl<S, T> Default for Hsv<S, T>
485where
486 T: Stimulus,
487 RgbHue<T>: Default,
488{
489 fn default() -> Hsv<S, T> {
490 Hsv::new(RgbHue::default(), Self::min_saturation(), Self::min_value())
491 }
492}
493
494impl_color_add!(Hsv<S>, [hue, saturation, value], standard);
495impl_color_sub!(Hsv<S>, [hue, saturation, value], standard);
496
497impl_array_casts!(Hsv<S, T>, [T; 3]);
498impl_simd_array_conversion_hue!(Hsv<S>, [saturation, value], standard);
499impl_struct_of_array_traits_hue!(Hsv<S>, RgbHueIter, [saturation, value], standard);
500
501impl_eq_hue!(Hsv<S>, RgbHue, [hue, saturation, value]);
502impl_copy_clone!(Hsv<S>, [hue, saturation, value], standard);
503
504#[allow(deprecated)]
505impl<S, T> crate::RelativeContrast for Hsv<S, T>
506where
507 T: Real + Arithmetics + PartialCmp,
508 T::Mask: LazySelect<T>,
509 S: RgbStandard,
510 Xyz<<S::Space as RgbSpace>::WhitePoint, T>: FromColor<Self>,
511{
512 type Scalar = T;
513
514 #[inline]
515 fn get_contrast_ratio(self, other: Self) -> T {
516 let xyz1 = Xyz::from_color(self);
517 let xyz2 = Xyz::from_color(other);
518
519 crate::contrast_ratio(xyz1.y, xyz2.y)
520 }
521}
522
523impl_rand_traits_hsv_cone!(
524 UniformHsv,
525 Hsv<S> {
526 hue: UniformRgbHue => RgbHue,
527 height: value,
528 radius: saturation
529 }
530 phantom: standard: PhantomData<S>
531);
532
533#[cfg(feature = "bytemuck")]
534unsafe impl<S, T> bytemuck::Zeroable for Hsv<S, T> where T: bytemuck::Zeroable {}
535
536#[cfg(feature = "bytemuck")]
537unsafe impl<S: 'static, T> bytemuck::Pod for Hsv<S, T> where T: bytemuck::Pod {}
538
539#[cfg(test)]
540mod test {
541 use super::Hsv;
542
543 test_convert_into_from_xyz!(Hsv);
544
545 #[cfg(feature = "approx")]
546 mod conversion {
547 use crate::{FromColor, Hsl, Hsv, Srgb};
548
549 #[test]
550 fn red() {
551 let a = Hsv::from_color(Srgb::new(1.0, 0.0, 0.0));
552 let b = Hsv::new_srgb(0.0, 1.0, 1.0);
553 let c = Hsv::from_color(Hsl::new_srgb(0.0, 1.0, 0.5));
554
555 assert_relative_eq!(a, b);
556 assert_relative_eq!(a, c);
557 }
558
559 #[test]
560 fn orange() {
561 let a = Hsv::from_color(Srgb::new(1.0, 0.5, 0.0));
562 let b = Hsv::new_srgb(30.0, 1.0, 1.0);
563 let c = Hsv::from_color(Hsl::new_srgb(30.0, 1.0, 0.5));
564
565 assert_relative_eq!(a, b);
566 assert_relative_eq!(a, c);
567 }
568
569 #[test]
570 fn green() {
571 let a = Hsv::from_color(Srgb::new(0.0, 1.0, 0.0));
572 let b = Hsv::new_srgb(120.0, 1.0, 1.0);
573 let c = Hsv::from_color(Hsl::new_srgb(120.0, 1.0, 0.5));
574
575 assert_relative_eq!(a, b);
576 assert_relative_eq!(a, c);
577 }
578
579 #[test]
580 fn blue() {
581 let a = Hsv::from_color(Srgb::new(0.0, 0.0, 1.0));
582 let b = Hsv::new_srgb(240.0, 1.0, 1.0);
583 let c = Hsv::from_color(Hsl::new_srgb(240.0, 1.0, 0.5));
584
585 assert_relative_eq!(a, b);
586 assert_relative_eq!(a, c);
587 }
588
589 #[test]
590 fn purple() {
591 let a = Hsv::from_color(Srgb::new(0.5, 0.0, 1.0));
592 let b = Hsv::new_srgb(270.0, 1.0, 1.0);
593 let c = Hsv::from_color(Hsl::new_srgb(270.0, 1.0, 0.5));
594
595 assert_relative_eq!(a, b);
596 assert_relative_eq!(a, c);
597 }
598 }
599
600 #[test]
601 fn ranges() {
602 assert_ranges! {
603 Hsv<crate::encoding::Srgb, f64>;
604 clamped {
605 saturation: 0.0 => 1.0,
606 value: 0.0 => 1.0
607 }
608 clamped_min {}
609 unclamped {
610 hue: -360.0 => 360.0
611 }
612 }
613 }
614
615 raw_pixel_conversion_tests!(Hsv<crate::encoding::Srgb>: hue, saturation, value);
616 raw_pixel_conversion_fail_tests!(Hsv<crate::encoding::Srgb>: hue, saturation, value);
617
618 #[test]
619 fn check_min_max_components() {
620 use crate::encoding::Srgb;
621
622 assert_eq!(Hsv::<Srgb>::min_saturation(), 0.0,);
623 assert_eq!(Hsv::<Srgb>::min_value(), 0.0,);
624 assert_eq!(Hsv::<Srgb>::max_saturation(), 1.0,);
625 assert_eq!(Hsv::<Srgb>::max_value(), 1.0,);
626 }
627
628 struct_of_arrays_tests!(
629 Hsv<crate::encoding::Srgb>[hue, saturation, value] phantom: standard,
630 super::Hsva::new(0.1f32, 0.2, 0.3, 0.4),
631 super::Hsva::new(0.2, 0.3, 0.4, 0.5),
632 super::Hsva::new(0.3, 0.4, 0.5, 0.6)
633 );
634
635 #[cfg(feature = "serializing")]
636 #[test]
637 fn serialize() {
638 let serialized = ::serde_json::to_string(&Hsv::new_srgb(0.3, 0.8, 0.1)).unwrap();
639
640 assert_eq!(serialized, r#"{"hue":0.3,"saturation":0.8,"value":0.1}"#);
641 }
642
643 #[cfg(feature = "serializing")]
644 #[test]
645 fn deserialize() {
646 let deserialized: Hsv =
647 ::serde_json::from_str(r#"{"hue":0.3,"saturation":0.8,"value":0.1}"#).unwrap();
648
649 assert_eq!(deserialized, Hsv::new(0.3, 0.8, 0.1));
650 }
651
652 test_uniform_distribution! {
653 Hsv<crate::encoding::Srgb, f32> as crate::rgb::Rgb {
654 red: (0.0, 1.0),
655 green: (0.0, 1.0),
656 blue: (0.0, 1.0)
657 },
658 min: Hsv::new(0.0f32, 0.0, 0.0),
659 max: Hsv::new(360.0, 1.0, 1.0)
660 }
661}