coolor/
hsl.rs

1use crate::*;
2
3/// HSL color
4#[derive(Clone, Copy, Debug, PartialEq)]
5pub struct Hsl {
6    /// hue in `[0,360[`
7    pub h: f32,
8    /// saturation in `[0,1]`
9    pub s: f32,
10    /// luminosity in `[0,1]`
11    pub l: f32,
12}
13
14impl Hsl {
15    /// Create a new HSL color from its components
16    pub fn new(h: f32, s: f32, l: f32) -> Self {
17        debug_assert!(h >= 0.0 && h < 360.0);
18        debug_assert!(s >= 0.0 && s <= 1.0);
19        debug_assert!(l >= 0.0 && l <= 1.0);
20        Self { h, s, l }
21    }
22    /// Create a new HSL color from its components, checking the ranges
23    pub fn checked(h: f32, s: f32, l: f32) -> Result<Self, CoolorError> {
24        if !(h >= 0.0 && h < 360.0 && s >= 0.0 && s <= 1.0 && l >= 0.0 && l <= 1.0) {
25            Ok(Self { h, s, l })
26        } else {
27            Err(CoolorError::InvalidHsl(h, s, l))
28        }
29    }
30    pub fn mix(c1: Self, w1: f32, c2: Self, w2: f32) -> Self {
31        debug_assert!(w1 + w2 > 0.0);
32        let h = if dist(c1.h, c2.h) > 180.0 {
33            // the shortest path involve crossing Tau
34            let (h1, h2) = if c1.h < c2.h {
35                (c1.h + 360.0, c2.h)
36            } else {
37                (c1.h, c2.h + 360.0)
38            };
39            ((w1 * h1 + w2 * h2) / (w1 + w2)) % 360.0
40        } else {
41            // direct way
42            (w1 * c1.h + w2 * c2.h) / (w1 + w2)
43        };
44        //let h = (w1*c1.h + w2*c2.h) / (w1+w2);
45        let s = (w1 * c1.s + w2 * c2.s) / (w1 + w2);
46        let l = (w1 * c1.l + w2 * c2.l) / (w1 + w2);
47        Self { h, s, l }
48    }
49    /// Return the nearest ANSI color
50    ///
51    /// This is a slow function as it literally tries all
52    /// ANSI colors and picks the nearest one.
53    pub fn to_ansi(self) -> AnsiColor {
54        let mut best = AnsiColor { code: 16 };
55        let mut smallest_distance: f32 = self.distance_to(best);
56        for code in 17..=255 {
57            let color = AnsiColor { code };
58            let distance = self.distance_to(color);
59            if distance < smallest_distance {
60                best = color;
61                smallest_distance = distance;
62            }
63        }
64        best
65    }
66    pub fn to_rgb(self) -> Rgb {
67        let h = self.h / 360.0;
68        let s = self.s;
69        let l = self.l;
70        let rgb = if s == 0.0 {
71            (l, l, l)
72        } else {
73            let v2 = if l < 0.5 {
74                l * (1.0 + s)
75            } else {
76                l + s - (s * l)
77            };
78            let v1 = 2.0 * l - v2;
79            (
80                hue_to_rgb_component(v1, v2, h + (1.0 / 3.0)),
81                hue_to_rgb_component(v1, v2, h),
82                hue_to_rgb_component(v1, v2, h - (1.0 / 3.0)),
83            )
84        };
85        rgb.into()
86    }
87    pub fn delta_h(self, other: Hsl) -> f32 {
88        dist(self.h, other.h).min(dist(self.h, 360.0)) // it's a circle, 0==360
89    }
90    pub fn delta_s(self, other: Hsl) -> f32 {
91        dist(self.s, other.s)
92    }
93    pub fn delta_l(self, other: Hsl) -> f32 {
94        dist(self.l, other.l)
95    }
96    /// tentatively perceptual distance between the two colors,
97    ///  except it's just as unscientific it can possibly be so
98    ///  check it looks good before trying ot use it, at least...
99    pub fn distance_to<H: Into<Hsl>>(self, other: H) -> f32 {
100        let other: Hsl = other.into();
101        self.delta_h(other) / 360.0 + self.delta_s(other) + self.delta_l(other)
102    }
103    /// Tell whether it's about the same color
104    ///
105    /// There's no theory behind this function, it should not
106    /// be used outside of unit tests
107    pub fn near(self, other: Hsl) -> bool {
108        self.distance_to(other) < 0.01
109    }
110}
111
112impl From<AnsiColor> for Hsl {
113    fn from(ansi: AnsiColor) -> Self {
114        ansi.to_hsl()
115    }
116}
117impl From<Rgb> for Hsl {
118    fn from(rgb: Rgb) -> Self {
119        rgb.to_hsl()
120    }
121}
122
123fn hue_to_rgb_component(v1: f32, v2: f32, vh: f32) -> f32 {
124    let vh = (vh + 1.0) % 1.0;
125    if 6.0 * vh < 1.0 {
126        (v1 + (v2 - v1) * 6.0 * vh).clamp(0.0, 1.0)
127    } else if 2.0 * vh < 1.0 {
128        v2
129    } else if 3.0 * vh < 2.0 {
130        (v1 + (v2 - v1) * ((2.0 / 3.0) - vh) * 6.0).clamp(0.0, 1.0)
131    } else {
132        v1
133    }
134}
135
136fn dist(a: f32, b: f32) -> f32 {
137    if a < b {
138        b - a
139    } else {
140        a - b
141    }
142}