palette/
hsv.rs

1//! Types for the HSV color space.
2
3use 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
17/// Linear HSV with an alpha component. See the [`Hsva` implementation in
18/// `Alpha`](crate::Alpha#Hsva).
19pub type Hsva<S = Srgb, T = f32> = Alpha<Hsv<S, T>, T>;
20
21/// HSV color space.
22///
23/// HSV is a cylindrical version of [RGB](crate::rgb::Rgb) and it's very similar
24/// to [HSL](crate::Hsl). The difference is that the `value` component in HSV
25/// determines the _brightness_ of the color, and not the _lightness_. The
26/// difference is that, for example, red (100% R, 0% G, 0% B) and white (100% R,
27/// 100% G, 100% B) has the same brightness (or value), but not the same
28/// lightness.
29///
30/// HSV component values are typically real numbers (such as floats), but may
31/// also be converted to and from `u8` for storage and interoperability
32/// purposes. The hue is then within the range `[0, 255]`.
33///
34/// ```
35/// use approx::assert_relative_eq;
36/// use palette::Hsv;
37///
38/// let hsv_u8 = Hsv::new_srgb(128u8, 85, 51);
39/// let hsv_f32 = hsv_u8.into_format::<f32>();
40///
41/// assert_relative_eq!(hsv_f32, Hsv::new(180.0, 1.0 / 3.0, 0.2));
42/// ```
43#[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    /// The hue of the color, in degrees. Decides if it's red, blue, purple,
55    /// etc.
56    #[palette(unsafe_same_layout_as = "T")]
57    pub hue: RgbHue<T>,
58
59    /// The colorfulness of the color. 0.0 gives gray scale colors and 1.0 will
60    /// give absolutely clear colors.
61    pub saturation: T,
62
63    /// Decides how bright the color will look. 0.0 will be black, and 1.0 will
64    /// give a bright an clear color that goes towards white when `saturation`
65    /// goes towards 0.0.
66    pub value: T,
67
68    /// The white point and RGB primaries this color is adapted to. The default
69    /// is the sRGB standard.
70    #[cfg_attr(feature = "serializing", serde(skip))]
71    #[palette(unsafe_zero_sized)]
72    pub standard: PhantomData<S>,
73}
74
75impl<T> Hsv<Srgb, T> {
76    /// Create an sRGB HSV color. This method can be used instead of `Hsv::new`
77    /// to help type inference.
78    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    /// Create an sRGB HSV color. This is the same as `Hsv::new_srgb` without
83    /// the generic hue type. It's temporary until `const fn` supports traits.
84    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    /// Create an HSV color.
91    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    /// Create an HSV color. This is the same as `Hsv::new` without the generic
96    /// hue type. It's temporary until `const fn` supports traits.
97    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    /// Convert into another component type.
107    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    /// Convert from another component type.
120    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    /// Convert to a `(hue, saturation, value)` tuple.
128    pub fn into_components(self) -> (RgbHue<T>, T, T) {
129        (self.hue, self.saturation, self.value)
130    }
131
132    /// Convert from a `(hue, saturation, value)` tuple.
133    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    /// Return the `saturation` value minimum.
153    pub fn min_saturation() -> T {
154        T::zero()
155    }
156
157    /// Return the `saturation` value maximum.
158    pub fn max_saturation() -> T {
159        T::max_intensity()
160    }
161
162    /// Return the `value` value minimum.
163    pub fn min_value() -> T {
164        T::zero()
165    }
166
167    /// Return the `value` value maximum.
168    pub fn max_value() -> T {
169        T::max_intensity()
170    }
171}
172
173///<span id="Hsva"></span>[`Hsva`](crate::Hsva) implementations.
174impl<T, A> Alpha<Hsv<Srgb, T>, A> {
175    /// Create an sRGB HSV color with transparency. This method can be used
176    /// instead of `Hsva::new` to help type inference.
177    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    /// Create an sRGB HSV color with transparency. This is the same as
182    /// `Hsva::new_srgb` without the generic hue type. It's temporary until
183    /// `const fn` supports traits.
184    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
189///<span id="Hsva"></span>[`Hsva`](crate::Hsva) implementations.
190impl<S, T, A> Alpha<Hsv<S, T>, A> {
191    /// Create an HSV color with transparency.
192    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    /// Create an HSV color with transparency. This is the same as `Hsva::new`
197    /// without the generic hue type. It's temporary until `const fn` supports
198    /// traits.
199    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    /// Convert into another component type.
207    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    /// Convert from another component type.
219    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    /// Convert to a `(hue, saturation, value, alpha)` tuple.
228    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    /// Convert from a `(hue, saturation, value, alpha)` tuple.
238    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        // Avoid negative numbers
276        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        // The SIMD optimized version showed significant slowdown for regular floats.
281        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            // Based on OPTIMIZED RGB TO HSV COLOR CONVERSION USING SSE TECHNOLOGY
320            // by KOBALICEK, Petr & BLIZNAK, Michal
321            //
322            // This implementation assumes less about the underlying mask and number
323            // representation. The hue is also multiplied by 6 to avoid rounding
324            // errors when using degrees.
325
326            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            // Each of these represents an RGB component. The maximum will be false
337            // while the two other will be true. They are later used for determining
338            // which branch in the hue equation we end up in.
339            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            // The hue base is the `1`, `2/6`, `4/6` or 0 part of the hue equation,
344            // except it's multiplied by 6 here.
345            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            // Each of these is a part of `G - B`, `B - R`, `R - G` or 0 from the
351            // hue equation. They become positive, negative or 0, depending on which
352            // branch we should be in. This makes the sum of all three combine as
353            // expected.
354            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            // This is the hue equation parts combined. The hue base is the constant
368            // and the RGB components are masked so up to two of them are non-zero.
369            // Once again, this is multiplied by 6, so the chroma isn't multiplied
370            // before dividing.
371            //
372            // We also avoid dividing by 0 for non-SIMD values.
373            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            // hue will always be within [0, 12) (it's multiplied by 6, compared to
379            // the paper), so we can subtract by 6 instead of using % to get it
380            // within [0, 6).
381            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        // avoid divide by zero
409        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        // avoid divide by zero
440        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}