css_colors/
hsl.rs

1use super::{deg, percent, Angle, Color, Ratio, RGB, RGBA};
2use std::fmt;
3
4/// Constructs a HSL Color from numerical values, similar to the
5/// [`hsl` function](css-hsl) in CSS.
6///
7/// The hue component is expressed in degrees. Values outside of
8/// the 0-359° range will be normalized accordingly. The saturation
9/// and lightness components are expressed in percentages. Values
10/// outside of the 0-100% range will cause a panic.
11///
12/// # Example
13/// ```
14/// use css_colors::{Color, hsl};
15///
16/// let salmon = hsl(6, 93, 71);
17///
18/// assert_eq!(salmon.to_css(), "hsl(6, 93%, 71%)");
19/// ```
20///
21/// [css-hsl]: https://www.w3.org/TR/css-color-3/#hsl-color
22pub fn hsl(h: i32, s: u8, l: u8) -> HSL {
23    HSL {
24        h: deg(h),
25        s: percent(s),
26        l: percent(l),
27    }
28}
29
30/// Constructs a HSLA Color from numerical values, similar to the
31/// [`hsla` function](css-hsla) in CSS.
32///
33/// The hue component is expressed in degrees. Values outside of
34/// the 0-359° range will be normalized accordingly. The saturation
35/// and lightness components are expressed in percentages. Values
36/// outside of the 0-100% range will cause a panic. The alpha value
37/// is expressed as a float. Values outside of the 0.0-1.0 range will
38/// cause a panic.
39///
40/// # Example
41/// ```
42/// use css_colors::{Color, hsla};
43///
44/// let salmon = hsla(6, 93, 71, 0.50);
45///
46/// assert_eq!(salmon.to_css(), "hsla(6, 93%, 71%, 0.50)");
47/// ```
48///
49/// [css-hsla]: https://www.w3.org/TR/css-color-3/#hsla-color
50pub fn hsla(h: i32, s: u8, l: u8, a: f32) -> HSLA {
51    HSLA {
52        h: deg(h),
53        s: percent(s),
54        l: percent(l),
55        a: Ratio::from_f32(a),
56    }
57}
58
59#[derive(Debug, Copy, Clone, PartialEq)]
60/// A struct to represent how much hue, saturation, and luminosity should be added to create a color.
61/// The hue is a degree on the color wheel; 0 (or 360) is red, 120 is green, 240 is blue.
62/// A valid value for `h` must range between `0-360`.
63/// The saturation ranges between `0-100`, where `0` is completely desaturated, and `100` is full saturation.
64/// The luminosity ranges between `0-100`, where `0` is no light (black), and `100` is full light (white).
65///
66/// For more, see the [CSS Color Spec](https://www.w3.org/TR/2018/REC-css-color-3-20180619/#hsl-color).
67pub struct HSL {
68    // hue
69    pub h: Angle,
70
71    // saturation
72    pub s: Ratio,
73
74    // luminosity
75    pub l: Ratio,
76}
77
78impl fmt::Display for HSL {
79    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
80        write!(f, "hsl({}, {}, {})", self.h.degrees(), self.s, self.l)
81    }
82}
83
84impl Color for HSL {
85    type Alpha = HSLA;
86
87    fn to_css(self) -> String {
88        self.to_string()
89    }
90
91    fn to_rgb(self) -> RGB {
92        self.to_hsla().to_rgb()
93    }
94
95    fn to_rgba(self) -> RGBA {
96        self.to_hsla().to_rgba()
97    }
98
99    fn to_hsl(self) -> HSL {
100        self
101    }
102
103    fn to_hsla(self) -> HSLA {
104        let HSL { h, s, l } = self;
105
106        HSLA {
107            h,
108            s,
109            l,
110            a: percent(100),
111        }
112    }
113
114    fn saturate(self, amount: Ratio) -> Self {
115        self.to_hsla().saturate(amount).to_hsl()
116    }
117
118    fn desaturate(self, amount: Ratio) -> Self {
119        self.to_hsla().desaturate(amount).to_hsl()
120    }
121
122    fn lighten(self, amount: Ratio) -> Self {
123        self.to_hsla().lighten(amount).to_hsl()
124    }
125
126    fn darken(self, amount: Ratio) -> Self {
127        self.to_hsla().darken(amount).to_hsl()
128    }
129
130    fn fadein(self, amount: Ratio) -> Self::Alpha {
131        self.to_hsla().fadein(amount)
132    }
133
134    fn fadeout(self, amount: Ratio) -> Self::Alpha {
135        self.to_hsla().fadeout(amount)
136    }
137
138    fn fade(self, amount: Ratio) -> Self::Alpha {
139        self.to_hsla().fade(amount)
140    }
141
142    fn spin(self, amount: Angle) -> Self {
143        self.to_hsla().spin(amount).to_hsl()
144    }
145
146    fn mix<T: Color>(self, other: T, weight: Ratio) -> Self::Alpha {
147        self.to_hsla().mix(other, weight)
148    }
149
150    fn tint(self, weight: Ratio) -> Self {
151        self.to_hsla().tint(weight).to_hsl()
152    }
153
154    fn shade(self, weight: Ratio) -> Self {
155        self.to_hsla().shade(weight).to_hsl()
156    }
157
158    fn greyscale(self) -> Self {
159        self.to_hsla().greyscale().to_hsl()
160    }
161}
162
163// A function to convert an HSL value (either h, s, or l) into the equivalent, valid RGB value.
164fn to_rgb_value(val: u16, temp_1: f32, temp_2: f32) -> f32 {
165    let value = val as f32 / 360.0;
166
167    if value > (2.0 / 3.0) {
168        // value > 0.66667
169        temp_2
170    } else if value > (1.0 / 2.0) {
171        // value is between 0.5 and 0.66667
172        temp_2 + ((temp_1 - temp_2) * ((2.0 / 3.0) - value) * 6.0)
173    } else if value > (1.0 / 6.0) {
174        // value is between 0.16667 and 0.5
175        temp_1
176    } else {
177        // value <= 0.16667
178        temp_2 + ((temp_1 - temp_2) * value * 6.0)
179    }
180}
181
182#[derive(Debug, Copy, Clone, PartialEq)]
183/// A struct to represent how much hue, saturation, and luminosity should be added to create a color.
184/// Also handles alpha specifications.
185///
186/// A valid value for `h` must range between `0-360`.
187/// The saturation ranges between `0-100`, where `0` is completely desaturated, and `100` is full saturation.
188/// The luminosity ranges between `0-100`, where `0` is no light (black), and `100` is full light (white).
189///
190/// For more, see the [CSS Color Spec](https://www.w3.org/TR/2018/REC-css-color-3-20180619/#hsla-color).
191pub struct HSLA {
192    // hue
193    pub h: Angle,
194
195    // saturation
196    pub s: Ratio,
197
198    // luminosity
199    pub l: Ratio,
200
201    // alpha
202    pub a: Ratio,
203}
204
205impl fmt::Display for HSLA {
206    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
207        write!(
208            f,
209            "hsla({}, {}, {}, {:.02})",
210            self.h.degrees(),
211            self.s,
212            self.l,
213            self.a.as_f32()
214        )
215    }
216}
217
218impl Color for HSLA {
219    type Alpha = Self;
220
221    fn to_css(self) -> String {
222        self.to_string()
223    }
224
225    fn to_rgb(self) -> RGB {
226        self.to_rgba().to_rgb()
227    }
228
229    fn to_rgba(self) -> RGBA {
230        let HSLA { h, s, l, a } = self;
231
232        // If there is no saturation, the color is a shade of grey.
233        // We can convert the luminosity and set r, g, and b to that value.
234        if s == percent(0) {
235            return RGBA {
236                r: l,
237                g: l,
238                b: l,
239                a,
240            };
241        }
242
243        let s = s.as_f32();
244        let l = l.as_f32();
245
246        // If the color is not a grey, then we need to create a temporary variable to continue with the algorithm.
247        // If the luminosity is less than 50%, we add 1.0 to the saturation and multiply by the luminosity.
248        // Otherwise, we add the luminosity and saturation, and subtract the product of luminosity and saturation from it.
249        let temp_1 = if l < 0.5 {
250            l * (1.0 + s)
251        } else {
252            (l + s) - (l * s)
253        };
254
255        // Another temporary variable.
256        let temp_2 = (2.0 * l) - temp_1;
257
258        // Create a rotation of 120 degrees in order to divide the angle into thirds.
259        let rotation = Angle::new(120);
260
261        // Then rotate the circle clockwise by 1/3 for the red value, and by 2/3rds for the blue value.
262        let temporary_r = (h + rotation).degrees();
263        let temporary_g = h.degrees();
264        let temporary_b = (h - rotation).degrees();
265
266        let red = to_rgb_value(temporary_r, temp_1, temp_2);
267        let green = to_rgb_value(temporary_g, temp_1, temp_2);
268        let blue = to_rgb_value(temporary_b, temp_1, temp_2);
269
270        RGBA {
271            r: Ratio::from_f32(red),
272            g: Ratio::from_f32(green),
273            b: Ratio::from_f32(blue),
274            a,
275        }
276    }
277
278    fn to_hsl(self) -> HSL {
279        let HSLA { h, s, l, .. } = self;
280        HSL { h, s, l }
281    }
282
283    fn to_hsla(self) -> HSLA {
284        self
285    }
286
287    fn saturate(self, amount: Ratio) -> Self {
288        let HSLA { h, s, l, a } = self;
289
290        HSLA {
291            h,
292            s: s + amount,
293            l,
294            a,
295        }
296    }
297
298    fn desaturate(self, amount: Ratio) -> Self {
299        let HSLA { h, s, l, a } = self;
300
301        HSLA {
302            h,
303            s: s - amount,
304            l,
305            a,
306        }
307    }
308
309    fn lighten(self, amount: Ratio) -> Self {
310        let HSLA { h, s, l, a } = self;
311
312        HSLA {
313            h,
314            s,
315            l: l + amount,
316            a,
317        }
318    }
319
320    fn darken(self, amount: Ratio) -> Self {
321        let HSLA { h, s, l, a } = self;
322
323        HSLA {
324            h,
325            s,
326            l: l - amount,
327            a,
328        }
329    }
330
331    fn fadein(self, amount: Ratio) -> Self {
332        self.fade(self.a + amount)
333    }
334
335    fn fadeout(self, amount: Ratio) -> Self {
336        self.fade(self.a - amount)
337    }
338
339    fn fade(self, amount: Ratio) -> Self::Alpha {
340        let HSLA { h, s, l, .. } = self;
341        HSLA { h, s, l, a: amount }
342    }
343
344    fn spin(self, amount: Angle) -> Self {
345        let HSLA { h, s, l, a } = self;
346
347        HSLA {
348            h: h + amount,
349            s,
350            l,
351            a,
352        }
353    }
354
355    fn mix<T: Color>(self, other: T, weight: Ratio) -> Self::Alpha {
356        self.to_rgba().mix(other, weight).to_hsla()
357    }
358
359    fn tint(self, weight: Ratio) -> Self {
360        self.to_rgba().tint(weight).to_hsla()
361    }
362
363    fn shade(self, weight: Ratio) -> Self {
364        self.to_rgba().shade(weight).to_hsla()
365    }
366
367    fn greyscale(self) -> Self {
368        let HSLA { h, l, a, .. } = self;
369
370        HSLA {
371            h,
372            s: percent(0),
373            l,
374            a,
375        }
376    }
377}