primitives/foundation/colorspace/
hsv.rs1use super::{utils, Color, ColorError, Float};
2use std::fmt;
3use utils::{hue_bound, percentage_to_fraction};
4
5#[derive(Clone, Copy, PartialEq, Debug)]
7pub struct HsvColor {
8 pub hue: Float,
10 pub saturation: Float,
12 pub value: Float,
14}
15
16impl HsvColor {
17 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
45impl From<HsvColor> for Color {
47 fn from(hsv: HsvColor) -> Self {
48 let hue = hue_bound(hsv.hue);
50 let value = percentage_to_fraction(hsv.value);
51 let saturation = percentage_to_fraction(hsv.saturation);
52
53 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
75impl From<Color> for HsvColor {
77 fn from(rgb: Color) -> Self {
78 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 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}