css_colors/
rgb.rs

1use super::{deg, percent, Angle, Color, Ratio, HSL, HSLA};
2use std::fmt;
3
4/// Constructs a RGB Color from numerical values, similar to the
5/// [`rgb` function](css-rgb) in CSS.
6///
7/// # Example
8/// ```
9/// use css_colors::{Color, rgb};
10///
11/// let salmon = rgb(250, 128, 114);
12///
13/// assert_eq!(salmon.to_css(), "rgb(250, 128, 114)");
14/// ```
15///
16/// [css-rgb]: https://www.w3.org/TR/css-color-3/#rgb-color
17pub fn rgb(r: u8, g: u8, b: u8) -> RGB {
18    RGB {
19        r: Ratio::from_u8(r),
20        g: Ratio::from_u8(g),
21        b: Ratio::from_u8(b),
22    }
23}
24
25/// Constructs a RGB Color from numerical values, similar to the
26/// [`rgba` function](css-rgba) in CSS.
27///
28/// The alpha value is expressed as a float. Values outside of the
29/// 0.0-1.0 range will cause a panic.
30///
31/// # Example
32/// ```
33/// use css_colors::{Color, rgba};
34///
35/// let salmon = rgba(250, 128, 114, 0.50);
36///
37/// assert_eq!(salmon.to_css(), "rgba(250, 128, 114, 0.50)");
38/// ```
39///
40/// [css-rgba]: https://www.w3.org/TR/css-color-3/#rgba-color
41pub fn rgba(r: u8, g: u8, b: u8, a: f32) -> RGBA {
42    RGBA {
43        r: Ratio::from_u8(r),
44        g: Ratio::from_u8(g),
45        b: Ratio::from_u8(b),
46        a: Ratio::from_f32(a),
47    }
48}
49
50#[derive(Debug, Copy, Clone, PartialEq)]
51/// A struct to represent how much red, green, and blue should be added to create a color.
52///
53/// Valid values for r, g, and b must be a u8 between `0-255`, represented as a `Ratio`.
54///
55/// For more, see the [CSS Color Spec](https://www.w3.org/TR/2018/REC-css-color-3-20180619/#rgb-color).
56pub struct RGB {
57    // red
58    pub r: Ratio,
59
60    // green
61    pub g: Ratio,
62
63    // blue
64    pub b: Ratio,
65}
66
67impl fmt::Display for RGB {
68    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
69        write!(
70            f,
71            "rgb({}, {}, {})",
72            self.r.as_u8(),
73            self.g.as_u8(),
74            self.b.as_u8()
75        )
76    }
77}
78
79impl Color for RGB {
80    type Alpha = RGBA;
81
82    fn to_css(self) -> String {
83        self.to_string()
84    }
85
86    fn to_rgb(self) -> RGB {
87        self
88    }
89
90    fn to_rgba(self) -> RGBA {
91        let RGB { r, g, b } = self;
92
93        RGBA {
94            r,
95            g,
96            b,
97            a: percent(100),
98        }
99    }
100
101    /// The algorithm for converting from rgb to hsl format, which determines
102    /// the equivalent luminosity, saturation, and hue.
103    fn to_hsl(self) -> HSL {
104        self.to_rgba().to_hsl()
105    }
106
107    fn to_hsla(self) -> HSLA {
108        self.to_rgba().to_hsla()
109    }
110
111    fn saturate(self, amount: Ratio) -> Self {
112        self.to_rgba().saturate(amount).to_rgb()
113    }
114
115    fn desaturate(self, amount: Ratio) -> Self {
116        self.to_rgba().desaturate(amount).to_rgb()
117    }
118
119    fn lighten(self, amount: Ratio) -> Self {
120        self.to_rgba().lighten(amount).to_rgb()
121    }
122
123    fn darken(self, amount: Ratio) -> Self {
124        self.to_rgba().darken(amount).to_rgb()
125    }
126
127    fn fadein(self, amount: Ratio) -> RGBA {
128        self.to_rgba().fadein(amount)
129    }
130
131    fn fadeout(self, amount: Ratio) -> RGBA {
132        self.to_rgba().fadeout(amount)
133    }
134
135    fn fade(self, amount: Ratio) -> RGBA {
136        self.to_rgba().fade(amount)
137    }
138
139    fn spin(self, amount: Angle) -> Self {
140        self.to_rgba().spin(amount).to_rgb()
141    }
142
143    fn mix<T: Color>(self, other: T, weight: Ratio) -> RGBA {
144        self.to_rgba().mix(other, weight)
145    }
146
147    fn tint(self, weight: Ratio) -> Self {
148        self.to_rgba().tint(weight).to_rgb()
149    }
150
151    fn shade(self, weight: Ratio) -> Self {
152        self.to_rgba().shade(weight).to_rgb()
153    }
154
155    fn greyscale(self) -> Self {
156        self.to_rgba().greyscale().to_rgb()
157    }
158}
159
160#[derive(Debug, Copy, Clone, PartialEq)]
161/// A struct to represent how much red, green, and blue should be added to create a color.
162/// Also handles alpha specifications.
163///
164/// Valid values for r, g, and b must be a u8 between `0-255`, represented as a `Ratio`.
165/// Alpha (a) values must fall between `0-255`.
166///
167/// For more, see the [CSS Color Spec](https://www.w3.org/TR/2018/REC-css-color-3-20180619/#rgba-color).
168pub struct RGBA {
169    // red
170    pub r: Ratio,
171
172    // green
173    pub g: Ratio,
174
175    // blue
176    pub b: Ratio,
177
178    // alpha
179    pub a: Ratio,
180}
181
182impl fmt::Display for RGBA {
183    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
184        write!(
185            f,
186            "rgba({}, {}, {}, {:.02})",
187            self.r.as_u8(),
188            self.g.as_u8(),
189            self.b.as_u8(),
190            self.a.as_f32()
191        )
192    }
193}
194
195impl Color for RGBA {
196    type Alpha = Self;
197
198    fn to_css(self) -> String {
199        self.to_string()
200    }
201
202    fn to_rgb(self) -> RGB {
203        let RGBA { r, g, b, .. } = self;
204        RGB { r, g, b }
205    }
206
207    fn to_rgba(self) -> RGBA {
208        self
209    }
210
211    fn to_hsl(self) -> HSL {
212        self.to_hsla().to_hsl()
213    }
214
215    fn to_hsla(self) -> HSLA {
216        let RGBA { r, g, b, a } = self;
217
218        // If r, g, and b are the same, the color is a shade of grey (between
219        // black and white), with no hue or saturation. In that situation, there
220        // is no saturation or hue, and we can use any value to determine luminosity.
221        if r == g && g == b {
222            return HSLA {
223                h: deg(0),
224                s: percent(0),
225                l: r,
226                a,
227            };
228        }
229
230        // Otherwise, to determine luminosity, we conver the RGB values into a
231        // percentage value, find the max and the min of those values, sum them
232        // together, and divide by 2.
233        let r = self.r.as_f32();
234        let g = self.g.as_f32();
235        let b = self.b.as_f32();
236
237        let max = if r > g && r > b {
238            r
239        } else if g > b {
240            g
241        } else {
242            b
243        };
244
245        let min = if r < g && r < b {
246            r
247        } else if g < b {
248            g
249        } else {
250            b
251        };
252
253        let luminosity = (max + min) / 2.0;
254
255        // To find the saturation, we look at the max and min values.
256        // If the max and the min are the same, there is no saturation to the color.
257        // Otherwise, we calculate the saturation based on if the luminosity is
258        // greater than or less than 0.5.
259        let saturation = if max == min {
260            0.0
261        } else if luminosity < 0.5 {
262            (max - min) / (max + min)
263        } else {
264            (max - min) / (2.0 - (max + min))
265        };
266
267        // To calculate the hue, we look at which value (r, g, or b) is the max.
268        // Based on that, we subtract the difference between the other two values,
269        // adding 120 or 240 deg to account for the degrees on the color wheel, and
270        // then dividing that by the difference between the max and the min values.
271        // Finally, we multiply the hue value by 60 to convert it to degrees on
272        // the color wheel, accounting for negative hues as well.
273        let hue = if max == r {
274            60.0 * (g - b) / (max - min)
275        } else if max == g {
276            120.0 + 60.0 * (b - r) / (max - min)
277        } else {
278            240.0 + 60.0 * (r - g) / (max - min)
279        };
280
281        HSLA {
282            h: deg(hue.round() as i32),
283            s: Ratio::from_f32(saturation),
284            l: Ratio::from_f32(luminosity),
285            a,
286        }
287    }
288
289    fn saturate(self, amount: Ratio) -> Self {
290        self.to_hsla().saturate(amount).to_rgba()
291    }
292
293    fn desaturate(self, amount: Ratio) -> Self {
294        self.to_hsla().desaturate(amount).to_rgba()
295    }
296
297    fn lighten(self, amount: Ratio) -> Self {
298        self.to_hsla().lighten(amount).to_rgba()
299    }
300
301    fn darken(self, amount: Ratio) -> Self {
302        self.to_hsla().darken(amount).to_rgba()
303    }
304
305    fn fadein(self, amount: Ratio) -> Self {
306        self.fade(self.a + amount)
307    }
308
309    fn fadeout(self, amount: Ratio) -> Self {
310        self.fade(self.a - amount)
311    }
312
313    fn fade(self, amount: Ratio) -> Self {
314        let RGBA { r, g, b, .. } = self;
315        RGBA { r, g, b, a: amount }
316    }
317
318    fn spin(self, amount: Angle) -> Self {
319        self.to_hsla().spin(amount).to_rgba()
320    }
321
322    // This algorithm takes into account both the user-provided weight (w) and
323    // the difference between the alpha values of the two colors (a) to determine
324    // the weighted average of the two colors.
325    // Taken from Sass's implementation (http://sass-lang.com/documentation/Sass/Script/Functions.html#mix-instance_method)
326    fn mix<T: Color>(self, other: T, weight: Ratio) -> Self {
327        let RGBA {
328            r: r_lhs,
329            g: g_lhs,
330            b: b_lhs,
331            a: a_lhs,
332        } = self;
333
334        let RGBA {
335            r: r_rhs,
336            g: g_rhs,
337            b: b_rhs,
338            a: a_rhs,
339        } = other.to_rgba();
340
341        // Convert weight into a decimal, and then scale it so that it falls between a range of [-1, 1].
342        let w = (weight.as_f32() * 2.0) - 1.0;
343
344        // Find the difference between the left and right side's alphas (somewhere between [-1, 1]).
345        let a = a_lhs.as_f32() - a_rhs.as_f32();
346
347        // Find the combined rgb_weight, taking into account the user's passed-in weight and alpha (range of [-1, 1]).
348        let rgb_weight = if w * a == -1.0 {
349            w
350        } else {
351            (w + a) / (1.0 + w * a)
352        };
353
354        // Find the combined rgb weight, scaling it to fall in a range bewtween [0, 1].
355        let rgb_weight = (rgb_weight + 1.0) / 2.0;
356
357        // Convert left and right side's weights into Ratios.
358        let rgb_weight_lhs = Ratio::from_f32(rgb_weight);
359        let rgb_weight_rhs = Ratio::from_f32(1.0) - rgb_weight_lhs;
360
361        let alpha_weight_lhs = weight;
362        let alpha_weight_rhs = Ratio::from_f32(1.0) - alpha_weight_lhs;
363
364        RGBA {
365            r: (r_lhs * rgb_weight_lhs) + (r_rhs * rgb_weight_rhs),
366            g: (g_lhs * rgb_weight_lhs) + (g_rhs * rgb_weight_rhs),
367            b: (b_lhs * rgb_weight_lhs) + (b_rhs * rgb_weight_rhs),
368            a: (a_lhs * alpha_weight_lhs) + (a_rhs * alpha_weight_rhs),
369        }
370    }
371
372    fn tint(self, weight: Ratio) -> Self {
373        self.mix(rgb(255, 255, 255), weight)
374    }
375
376    fn shade(self, weight: Ratio) -> Self {
377        self.mix(rgb(0, 0, 0), weight)
378    }
379
380    fn greyscale(self) -> Self {
381        self.to_hsla().greyscale().to_rgba()
382    }
383}