primitives/foundation/colorspace/
hsl.rs

1use super::{utils, Color, ColorError, Float};
2use std::fmt;
3
4/// Hsl color representation
5#[derive(Clone, Copy, PartialEq, Debug)]
6pub struct HslColor {
7    /// Hue component
8    pub hue: Float,
9    /// Saturation component
10    pub saturation: Float,
11    /// Lightness component
12    pub lightness: Float,
13}
14
15impl HslColor {
16    /// Create new Hsl color with parameters
17    pub fn new(h: Float, s: Float, l: Float) -> Self {
18        Self {
19            hue: h % 360.0,
20            saturation: if s > 100.0 { 100.0 } else { s },
21            lightness: if s > 100.0 { 100.0 } else { l },
22        }
23    }
24}
25
26impl fmt::Display for HslColor {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        write!(
29            f,
30            "hsl({}°, {}%, {}%)",
31            self.hue, self.saturation, self.lightness
32        )
33    }
34}
35
36// HSL -> RGB
37impl From<HslColor> for Color {
38    fn from(hsl: HslColor) -> Self {
39        let HslColor {
40            hue,
41            saturation,
42            lightness,
43        } = hsl;
44        let c = (1. - ((2. * (lightness as Float / 100.)) - 1.).abs()) * (saturation as Float / 100.);
45        let x = c * (1. - ((((hue as Float) / 60.) % 2.) - 1.).abs());
46        let m = (lightness as Float / 100.) - (c / 2.);
47
48        let (r_prime, g_prime, b_prime) = {
49            if (0.0..60.0).contains(&hue) {
50                (c, x, 0.)
51            } else if (60.0..120.0).contains(&hue) {
52                (x, c, 0.)
53            } else if (120.0..180.0).contains(&hue) {
54                (0., c, x)
55            } else if (180.0..240.0).contains(&hue) {
56                (0., x, c)
57            } else if (240.0..300.0).contains(&hue) {
58                (x, 0., c)
59            } else if (300.0..360.0).contains(&hue) {
60                (c, 0., x)
61            } else {
62                unreachable!("{}", ColorError::DegreeOverflow)
63            }
64        };
65        Color {
66            red: r_prime + m,
67            green: g_prime + m,
68            blue: b_prime + m,
69            alpha: 1.,
70        }
71    }
72}
73
74// RGB -> HSL
75impl From<Color> for HslColor {
76    fn from(rgb: Color) -> Self {
77        let Color {
78            red,
79            green,
80            blue,
81            alpha: _,
82        } = rgb;
83
84        let (c_min, c_max) = utils::min_max_tuple([red, green, blue].iter());
85        let delta = c_max - c_min;
86
87        let hue = if (delta - 0.).abs() < Float::EPSILON {
88            0.
89        } else {
90            match c_max {
91                x if (x - red).abs() < Float::MIN_POSITIVE => 60. * (((green - blue) / delta) % 6.),
92                x if (x - green).abs() < Float::MIN_POSITIVE => 60. * (((blue - red) / delta) + 2.),
93                x if (x - blue).abs() < Float::MIN_POSITIVE => 60. * (((red - green) / delta) + 4.),
94                _ => unreachable!("Invalid hue calculation!"),
95            }
96        };
97
98        let lightness = (c_max + c_min) / 2.;
99
100        let saturation = if (delta - 0.).abs() < Float::EPSILON {
101            0.
102        } else {
103            delta / (1. - ((2. * lightness) - 1.).abs()) * 100.
104        };
105
106        HslColor {
107            hue,
108            saturation,
109            lightness: lightness * 100.,
110        }
111    }
112}
113
114#[cfg(test)]
115mod test {
116    use super::super::test_utils;
117    use super::super::*;
118
119    #[test]
120    fn to_rgb() {
121        test_utils::test_to_rgb_conversion(test_utils::RGB_HSL.iter())
122    }
123
124    #[test]
125    fn rgb_to_hsv() {
126        test_utils::test_conversion(test_utils::RGB_HSL.iter(), |actual_color, expected_hsl| {
127            let actual_rgb: RgbColor = (*actual_color).into();
128            let actual_hsl: HslColor = actual_rgb.into();
129            let HslColor {
130                hue: actual_h,
131                saturation: actual_s,
132                lightness: actual_l,
133            } = actual_hsl;
134            let (actual_h, actual_s, actual_l) =
135                (actual_h.round(), actual_s.round(), actual_l.round());
136            let HslColor {
137                hue: expected_h,
138                saturation: expected_s,
139                lightness: expected_l,
140            } = *expected_hsl;
141            assert!(
142                test_utils::diff_less_than_f64(actual_h, expected_h, 1.),
143                "wrong hue: {} -> {} != {}",
144                actual_rgb,
145                actual_hsl,
146                expected_hsl
147            );
148            assert!(
149                test_utils::diff_less_than_f64(actual_s, expected_s, 1.),
150                "wrong saturation: {} -> {} != {}",
151                actual_rgb,
152                actual_hsl,
153                expected_hsl
154            );
155            assert!(
156                test_utils::diff_less_than_f64(actual_l, expected_l, 1.),
157                "wrong lightness: {} -> {} != {}",
158                actual_rgb,
159                actual_hsl,
160                expected_hsl
161            );
162        })
163    }
164}