Skip to main content

primitives/foundation/colorspace/
hsv.rs

1use super::{utils, Color, ColorError, Float};
2use std::fmt;
3use utils::{hue_bound, percentage_to_fraction};
4
5/// Hsv color representation
6#[derive(Clone, Copy, PartialEq, Debug)]
7pub struct HsvColor {
8    /// Hue component
9    pub hue: Float,
10    /// Saturation component
11    pub saturation: Float,
12    /// Value component
13    pub value: Float,
14}
15
16impl HsvColor {
17    /// Create new Hsv color with parameters
18    pub fn new(hue: Float, saturation: Float, value: Float) -> Self {
19        Self {
20            hue: hue_bound(hue),
21            saturation: if saturation > 100. {
22                100.
23            } else if saturation < 0. {
24                0.
25            } else {
26                saturation
27            },
28            value: if saturation > 100. {
29                100.
30            } else if saturation < 0. {
31                0.
32            } else {
33                value
34            },
35        }
36    }
37}
38
39impl fmt::Display for HsvColor {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        write!(f, "hsv({}, {}, {})", self.hue, self.saturation, self.value)
42    }
43}
44
45// HSV -> RGB
46impl From<HsvColor> for Color {
47    fn from(hsv: HsvColor) -> Self {
48        //Err(ColorError::Unimplemented)
49        let hue = hue_bound(hsv.hue);
50        let value = percentage_to_fraction(hsv.value);
51        let saturation = percentage_to_fraction(hsv.saturation);
52
53        // https://ru.wikipedia.org/wiki/HSV_(%D1%86%D0%B2%D0%B5%D1%82%D0%BE%D0%B2%D0%B0%D1%8F_%D0%BC%D0%BE%D0%B4%D0%B5%D0%BB%D1%8C)#HSV_%E2%86%92_RGB
54        let min = (1. - saturation) * value;
55        let a = (value - min) * ((hue % 60.) / 60.);
56
57        if (0.0..60.0).contains(&hue) {
58            Color::new(value, min + a, min, 1.)
59        } else if (60.0..120.0).contains(&hue) {
60            Color::new(value - a, value, min, 1.)
61        } else if (120.0..180.0).contains(&hue) {
62            Color::new(min, value, min + a, 1.)
63        } else if (180.0..240.0).contains(&hue) {
64            Color::new(min, value - a, value, 1.)
65        } else if (240.0..300.0).contains(&hue) {
66            Color::new(min + a, min, value, 1.)
67        } else if (300.0..360.0).contains(&hue) {
68            Color::new(value, min, value - a, 1.)
69        } else {
70            unreachable!("HSV -> RGB: {}", ColorError::DegreeOverflow);
71        }
72    }
73}
74
75// RGB -> HSV
76impl From<Color> for HsvColor {
77    fn from(rgb: Color) -> Self {
78        // https://ru.wikipedia.org/wiki/HSV_(%D1%86%D0%B2%D0%B5%D1%82%D0%BE%D0%B2%D0%B0%D1%8F_%D0%BC%D0%BE%D0%B4%D0%B5%D0%BB%D1%8C)#RGB_%E2%86%92_HSV
79        let Color {
80            red,
81            green,
82            blue,
83            alpha: _,
84        } = rgb;
85        let (min, max) = utils::min_max_tuple([red, green, blue].iter());
86        let hue = if (max - red).abs() < Float::MIN_POSITIVE {
87            //normalize_hue(60. * (green - blue) / delta - 30.)
88            if green >= blue {
89                60. * (green as Float - blue as Float) / (max - min) as Float
90            } else {
91                360. - (green as Float - blue as Float) / (max - min) as Float * 60.
92            }
93        } else if (max - green).abs() < Float::MIN_POSITIVE {
94            60. * (blue as Float - red as Float) / (max - min) as Float + 120.
95        } else if (max - blue).abs() < Float::MIN_POSITIVE {
96            60. * (red as Float - green as Float) / (max - min) as Float + 240.
97        } else {
98            0.
99        };
100        let saturation = 1.
101            - (if (max - 0.).abs() < Float::EPSILON {
102                0 as Float
103            } else {
104                min as Float / max as Float
105            });
106        HsvColor::new(hue, saturation * 100., max * 100.)
107    }
108}
109
110#[cfg(test)]
111mod test {
112    use super::super::*;
113
114    #[test]
115    fn hsv_to_rgb() {
116        test_utils::test_to_rgb_conversion(test_utils::RGB_HSV.iter())
117    }
118
119    #[test]
120    fn rgb_to_hsv() {
121        test_utils::test_conversion(test_utils::RGB_HSV.iter(), |actual_color, expected_hsv| {
122            let actual_rgb: RgbColor = (*actual_color).into();
123            let actual_hsv: HsvColor = actual_rgb.into();
124            let HsvColor {
125                hue: actual_h,
126                saturation: actual_s,
127                value: actual_v,
128            } = actual_hsv;
129            let (actual_h, actual_s, actual_v) =
130                (actual_h.round(), actual_s.round(), actual_v.round());
131            let HsvColor {
132                hue: expected_h,
133                saturation: expected_s,
134                value: expected_v,
135            } = *expected_hsv;
136            assert!(
137                test_utils::diff_less_than_f64(actual_h, expected_h, 1.),
138                "wrong hue: {} -> {} != {}",
139                actual_rgb,
140                actual_hsv,
141                expected_hsv
142            );
143            assert!(
144                test_utils::diff_less_than_f64(actual_s, expected_s, 1.),
145                "wrong saturation: {} -> {} != {}",
146                actual_rgb,
147                actual_hsv,
148                expected_hsv
149            );
150            assert!(
151                test_utils::diff_less_than_f64(actual_v, expected_v, 1.),
152                "wrong brightness: {} -> {} != {}",
153                actual_rgb,
154                actual_hsv,
155                expected_hsv
156            );
157        })
158    }
159}