prisma/
hsv.rs

1//! The HSV device-dependent color model
2
3use crate::channel::cast::ChannelFormatCast;
4use crate::channel::{
5    AngularChannel, AngularChannelScalar, ChannelCast, ColorChannel, PosNormalBoundedChannel,
6    PosNormalChannelScalar,
7};
8use crate::color;
9use crate::color::{Bounded, Color, FromTuple, Invert, Lerp, PolarColor};
10use crate::convert;
11use crate::encoding::EncodableColor;
12use crate::rgb;
13use crate::tags::HsvTag;
14use angle;
15use angle::{Angle, Deg, FromAngle, IntoAngle};
16#[cfg(feature = "approx")]
17use approx;
18use num_traits;
19use num_traits::cast;
20use std::fmt;
21use std::ops;
22
23/// The HSV device-dependent polar color model
24///
25/// ![hsv-diagram](https://upload.wikimedia.org/wikipedia/commons/3/33/HSV_color_solid_cylinder_saturation_gray.png)
26///
27/// HSV is defined by a hue (base color), saturation (color richness) and value (color intensity).
28/// HSV is modeled as a cylinder, however the underlying space is conical. This causes some level of
29/// distortion and a degeneracy at S=0 or V=0. Thus, while easy to reason about, it is not good for
30/// perceptual uniformity. It does an okay job with averaging colors or doing other math, but prefer
31/// the CIE spaces for uniform gradients.
32///
33/// Hsv takes two type parameters: the cartesian channel scalar, and an angular channel scalar.
34///
35/// Hsv is in the same color space and encoding as the parent RGB space, it is merely a geometric
36/// transformation and distortion.
37///
38/// For an undistorted device-dependent polar color model, look at
39/// [Hsi](../hsi/struct.Hsi.html).
40#[repr(C)]
41#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Hash)]
42pub struct Hsv<T, A = Deg<T>> {
43    hue: AngularChannel<A>,
44    saturation: PosNormalBoundedChannel<T>,
45    value: PosNormalBoundedChannel<T>,
46}
47
48impl<T, A> Hsv<T, A>
49where
50    T: PosNormalChannelScalar,
51    A: AngularChannelScalar,
52{
53    /// Construct an Hsv instance from hue, saturation and value
54    pub fn new(hue: A, saturation: T, value: T) -> Self {
55        Hsv {
56            hue: AngularChannel::new(hue),
57            saturation: PosNormalBoundedChannel::new(saturation),
58            value: PosNormalBoundedChannel::new(value),
59        }
60    }
61
62    impl_color_color_cast_angular!(
63        Hsv {
64            hue,
65            saturation,
66            value
67        },
68        chan_traits = { PosNormalChannelScalar }
69    );
70
71    /// Returns the hue scalar
72    pub fn hue(&self) -> A {
73        self.hue.0.clone()
74    }
75    /// Returns the saturation scalar
76    pub fn saturation(&self) -> T {
77        self.saturation.0.clone()
78    }
79    /// Returns the value scalar
80    pub fn value(&self) -> T {
81        self.value.0.clone()
82    }
83    /// Returns a mutable reference to the hue channel scalar
84    pub fn hue_mut(&mut self) -> &mut A {
85        &mut self.hue.0
86    }
87    /// Returns a mutable reference to the saturation channel scalar
88    pub fn saturation_mut(&mut self) -> &mut T {
89        &mut self.saturation.0
90    }
91    /// Returns a mutable reference to the value channel scalar
92    pub fn value_mut(&mut self) -> &mut T {
93        &mut self.value.0
94    }
95    /// Set the hue channel value
96    pub fn set_hue(&mut self, val: A) {
97        self.hue.0 = val;
98    }
99    /// Set the saturation channel value
100    pub fn set_saturation(&mut self, val: T) {
101        self.saturation.0 = val;
102    }
103    /// Set the value channel value
104    pub fn set_value(&mut self, val: T) {
105        self.value.0 = val;
106    }
107}
108
109impl<T, A> PolarColor for Hsv<T, A>
110where
111    T: PosNormalChannelScalar,
112    A: AngularChannelScalar,
113{
114    type Angular = A;
115    type Cartesian = T;
116}
117
118impl<T, A> Color for Hsv<T, A>
119where
120    T: PosNormalChannelScalar,
121    A: AngularChannelScalar,
122{
123    type Tag = HsvTag;
124    type ChannelsTuple = (A, T, T);
125
126    fn num_channels() -> u32 {
127        3
128    }
129    fn to_tuple(self) -> Self::ChannelsTuple {
130        (self.hue.0, self.saturation.0, self.value.0)
131    }
132}
133
134impl<T, A> FromTuple for Hsv<T, A>
135where
136    T: PosNormalChannelScalar,
137    A: AngularChannelScalar,
138{
139    fn from_tuple(values: Self::ChannelsTuple) -> Self {
140        Hsv::new(values.0, values.1, values.2)
141    }
142}
143
144impl<T, A> Invert for Hsv<T, A>
145where
146    T: PosNormalChannelScalar,
147    A: AngularChannelScalar,
148{
149    impl_color_invert!(Hsv {
150        hue,
151        saturation,
152        value
153    });
154}
155
156impl<T, A> Lerp for Hsv<T, A>
157where
158    T: PosNormalChannelScalar + color::Lerp,
159    A: AngularChannelScalar + color::Lerp,
160{
161    type Position = A::Position;
162
163    impl_color_lerp_angular!(Hsv<T> {hue, saturation, value});
164}
165
166impl<T, A> Bounded for Hsv<T, A>
167where
168    T: PosNormalChannelScalar,
169    A: AngularChannelScalar,
170{
171    impl_color_bounded!(Hsv {
172        hue,
173        saturation,
174        value
175    });
176}
177
178impl<T, A> EncodableColor for Hsv<T, A>
179where
180    T: PosNormalChannelScalar + num_traits::Float,
181    A: AngularChannelScalar + Angle<Scalar = T> + FromAngle<angle::Turns<T>>,
182{
183}
184
185#[cfg(feature = "approx")]
186impl<T, A> approx::AbsDiffEq for Hsv<T, A>
187where
188    T: PosNormalChannelScalar + approx::AbsDiffEq<Epsilon = A::Epsilon>,
189    A: AngularChannelScalar + approx::AbsDiffEq,
190    A::Epsilon: Clone + num_traits::Float,
191{
192    impl_abs_diff_eq!({hue, saturation, value});
193}
194#[cfg(feature = "approx")]
195impl<T, A> approx::RelativeEq for Hsv<T, A>
196where
197    T: PosNormalChannelScalar + approx::RelativeEq<Epsilon = A::Epsilon>,
198    A: AngularChannelScalar + approx::RelativeEq,
199    A::Epsilon: Clone + num_traits::Float,
200{
201    impl_rel_eq!({hue, saturation, value});
202}
203#[cfg(feature = "approx")]
204impl<T, A> approx::UlpsEq for Hsv<T, A>
205where
206    T: PosNormalChannelScalar + approx::UlpsEq<Epsilon = A::Epsilon>,
207    A: AngularChannelScalar + approx::UlpsEq,
208    A::Epsilon: Clone + num_traits::Float,
209{
210    impl_ulps_eq!({hue, saturation, value});
211}
212
213impl<T, A> Default for Hsv<T, A>
214where
215    T: PosNormalChannelScalar + num_traits::Zero,
216    A: AngularChannelScalar + num_traits::Zero,
217{
218    impl_color_default!(Hsv {
219        hue: AngularChannel,
220        saturation: PosNormalBoundedChannel,
221        value: PosNormalBoundedChannel
222    });
223}
224
225impl<T, A> fmt::Display for Hsv<T, A>
226where
227    T: PosNormalChannelScalar + fmt::Display,
228    A: AngularChannelScalar + fmt::Display,
229{
230    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
231        write!(f, "Hsv({}, {}, {})", self.hue, self.saturation, self.value)
232    }
233}
234
235impl<T, A> convert::GetChroma for Hsv<T, A>
236where
237    T: PosNormalChannelScalar + ops::Mul<T, Output = T>,
238    A: AngularChannelScalar,
239{
240    type ChromaType = T;
241    fn get_chroma(&self) -> T {
242        self.saturation.0.clone() * self.value.0.clone()
243    }
244}
245impl<T, A> convert::GetHue for Hsv<T, A>
246where
247    T: PosNormalChannelScalar,
248    A: AngularChannelScalar,
249{
250    impl_color_get_hue_angular!(Hsv);
251}
252
253impl<T, A> convert::FromColor<Hsv<T, A>> for rgb::Rgb<T>
254where
255    T: PosNormalChannelScalar + num_traits::Float,
256    A: AngularChannelScalar,
257{
258    fn from_color(from: &Hsv<T, A>) -> Self {
259        let (hue_seg, hue_frac) = convert::decompose_hue_segment(from);
260        let one: T = cast(1.0).unwrap();
261        let hue_frac_t: T = cast(hue_frac).unwrap();
262
263        let channel_min = from.value() * (one - from.saturation());
264        let channel_max = from.value();
265
266        match hue_seg {
267            0 => {
268                let g = from.value() * (one - from.saturation() * (one - hue_frac_t));
269                rgb::Rgb::new(channel_max, g, channel_min)
270            }
271            1 => {
272                let r = from.value() * (one - from.saturation() * hue_frac_t);
273                rgb::Rgb::new(r, channel_max, channel_min)
274            }
275            2 => {
276                let b = from.value() * (one - from.saturation() * (one - hue_frac_t));
277                rgb::Rgb::new(channel_min, channel_max, b)
278            }
279            3 => {
280                let g = from.value() * (one - from.saturation() * hue_frac_t);
281                rgb::Rgb::new(channel_min, g, channel_max)
282            }
283            4 => {
284                let r = from.value() * (one - from.saturation() * (one - hue_frac_t));
285                rgb::Rgb::new(r, channel_min, channel_max)
286            }
287            5 => {
288                let b = from.value() * (one - from.saturation() * hue_frac_t);
289                rgb::Rgb::new(channel_max, channel_min, b)
290            }
291            _ => unreachable!(),
292        }
293    }
294}
295
296#[cfg(test)]
297mod test {
298    use super::*;
299    use crate::convert::*;
300    use crate::rgb;
301    use angle::*;
302    use approx::*;
303    use std::f32::consts;
304
305    use crate::test;
306
307    #[test]
308    fn test_construct() {
309        let c1 = Hsv::new(Deg(50.0), 0.5, 0.3);
310
311        assert_ulps_eq!(c1.hue(), Deg(50.0));
312        assert_ulps_eq!(c1.saturation(), 0.5);
313        assert_ulps_eq!(c1.value(), 0.3);
314
315        let mut c2 = Hsv::new(Turns(0.9), 0.5, 0.75);
316        assert_ulps_eq!(c2.hue(), Turns(0.9));
317        c2.set_saturation(0.33);
318        assert_ulps_eq!(c2, Hsv::new(Turns(0.9), 0.33, 0.75));
319
320        let c3 = Hsv::from_tuple((Deg(50.0), 0.33, 0.66));
321        assert_eq!(c3.to_tuple(), (Deg(50.0), 0.33, 0.66));
322    }
323
324    #[test]
325    fn test_invert() {
326        let c1 = Hsv::new(Deg(30.0), 0.3, 0.6);
327        assert_ulps_eq!(c1.invert(), Hsv::new(Deg(210.0), 0.7, 0.4));
328
329        let c2 = Hsv::new(Deg(320.0), 0.5, 0.0);
330        assert_ulps_eq!(c2.invert(), Hsv::new(Deg(140.0), 0.5, 1.0));
331    }
332
333    #[test]
334    fn test_lerp() {
335        let c1 = Hsv::new(Rad(0.5), 0.0, 0.25);
336        let c2 = Hsv::new(Rad(1.5), 0.5, 0.25);
337        assert_ulps_eq!(c1.lerp(&c2, 0.0), c1);
338        assert_ulps_eq!(c1.lerp(&c2, 1.0), c2);
339        assert_ulps_eq!(c1.lerp(&c2, 0.25), Hsv::new(Rad(0.75), 0.125, 0.25));
340        assert_ulps_eq!(c1.lerp(&c2, 0.75), Hsv::new(Rad(1.25), 0.375, 0.25));
341
342        let c3 = Hsv::new(Deg(320.0), 0.0, 1.0);
343        let c4 = Hsv::new(Deg(100.0), 1.0, 0.0);
344        assert_ulps_eq!(c3.lerp(&c4, 0.0), c3);
345        assert_ulps_eq!(c3.lerp(&c4, 1.0).normalize(), c4);
346        assert_ulps_eq!(c3.lerp(&c4, 0.5).normalize(), Hsv::new(Deg(30.0), 0.5, 0.5));
347    }
348
349    #[test]
350    fn test_normalize() {
351        let c1 = Hsv::new(Deg(-120.0), 0.25, 0.75);
352        assert!(!c1.is_normalized());
353        assert_ulps_eq!(c1.normalize(), Hsv::new(Deg(240.0), 0.25, 0.75));
354
355        let c2 = Hsv::new(Turns(11.25), -1.11, 1.11);
356        assert_ulps_eq!(c2.normalize(), Hsv::new(Turns(0.25), 0.0, 1.0));
357    }
358
359    #[test]
360    fn test_chroma() {
361        let test_data = test::build_hs_test_data();
362
363        for item in test_data.iter() {
364            assert_relative_eq!(item.hsv.get_chroma(), item.chroma, epsilon = 1e-3);
365        }
366
367        let c1 = Hsv::new(Deg(100.0), 0.5, 0.5);
368        assert_ulps_eq!(c1.get_chroma(), 0.25);
369        assert_relative_eq!(
370            Hsv::new(Deg(240.50), 0.316, 0.721).get_chroma(),
371            0.228,
372            epsilon = 1e-3
373        );
374        assert_relative_eq!(
375            Hsv::new(Deg(120.0), 0.0, 0.0).get_chroma(),
376            0.0,
377            epsilon = 1e-3
378        );
379    }
380
381    #[test]
382    fn test_get_hue() {
383        assert_ulps_eq!(Hsv::new(Deg(120.0), 0.25, 0.75).get_hue(), Deg(120.0));
384        assert_ulps_eq!(
385            Hsv::new(Deg(180.0_f32), 0.35, 0.55).get_hue(),
386            Rad(consts::PI)
387        );
388        assert_ulps_eq!(Hsv::new(Turns(0.0), 0.00, 0.00).get_hue(), Rad(0.0));
389    }
390
391    #[test]
392    fn test_rgb_from_hsv() {
393        let test_data = test::build_hs_test_data();
394
395        for item in test_data.iter() {
396            let rgb = rgb::Rgb::from_color(&item.hsv);
397            assert_relative_eq!(rgb, item.rgb, epsilon = 1e-3);
398        }
399    }
400
401    #[test]
402    fn test_cast() {
403        let c1 = Hsv::new(Deg(180.0_f32), 0.5_f32, 0.3);
404        assert_relative_eq!(
405            c1.color_cast(),
406            Hsv::new(Rad(consts::PI), 0.5_f32, 0.3),
407            epsilon = 1e-6
408        );
409
410        let c2 = Hsv::new(Deg(55.0), 0.3, 0.2);
411        assert_relative_eq!(c2.color_cast(), Hsv::new(Deg(55.0_f32), 0.3_f32, 0.2_f32));
412
413        let c3 = Hsv::new(Rad(2.0), 0.88, 0.66);
414        assert_relative_eq!(c3.color_cast(), c3);
415    }
416}