bracket_color/
rgb.rs

1use crate::prelude::{HSV, RGBA};
2use std::convert::From;
3use std::ops;
4
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6#[derive(PartialEq, Copy, Clone, Default, Debug)]
7/// Represents an R/G/B triplet, in the range 0..1 (32-bit float)
8pub struct RGB {
9    /// The red component (0..1)
10    pub r: f32,
11    /// The green components (0..1)
12    pub g: f32,
13    /// The blue component (0..1)
14    pub b: f32,
15}
16
17#[derive(Debug, PartialEq, Copy, Clone)]
18/// Error message type when failing to convert a hex code to RGB.
19pub enum HtmlColorConversionError {
20    /// The HTML string was not a valid length. (Expects #AABBCC)
21    InvalidStringLength,
22    /// No # was included in the string.
23    MissingHash,
24    /// An unexpected character (not #, A-F) was detected in the color string.
25    InvalidCharacter,
26}
27
28// Implement operator overloading
29
30/// Support adding a float to a color. The result is clamped via the constructor.
31impl ops::Add<f32> for RGB {
32    type Output = Self;
33    #[must_use]
34    fn add(mut self, rhs: f32) -> Self {
35        self.r += rhs;
36        self.g += rhs;
37        self.b += rhs;
38        self
39    }
40}
41
42/// Support adding an RGB to a color. The result is clamped via the constructor.
43impl ops::Add<RGB> for RGB {
44    type Output = Self;
45    #[must_use]
46    fn add(mut self, rhs: Self) -> Self {
47        self.r += rhs.r;
48        self.g += rhs.g;
49        self.b += rhs.b;
50        self
51    }
52}
53
54/// Support subtracting a float from a color. The result is clamped via the constructor.
55impl ops::Sub<f32> for RGB {
56    type Output = Self;
57    #[must_use]
58    fn sub(mut self, rhs: f32) -> Self {
59        self.r -= rhs;
60        self.g -= rhs;
61        self.b -= rhs;
62        self
63    }
64}
65
66/// Support subtracting an RGB from a color. The result is clamped via the constructor.
67impl ops::Sub<RGB> for RGB {
68    type Output = Self;
69    #[must_use]
70    fn sub(mut self, rhs: Self) -> Self {
71        self.r -= rhs.r;
72        self.g -= rhs.g;
73        self.b -= rhs.b;
74        self
75    }
76}
77
78/// Support multiplying a color by a float. The result is clamped via the constructor.
79impl ops::Mul<f32> for RGB {
80    type Output = Self;
81    #[must_use]
82    fn mul(mut self, rhs: f32) -> Self {
83        self.r *= rhs;
84        self.g *= rhs;
85        self.b *= rhs;
86        self
87    }
88}
89
90/// Support multiplying a color by another color. The result is clamped via the constructor.
91impl ops::Mul<RGB> for RGB {
92    type Output = Self;
93    #[must_use]
94    fn mul(mut self, rhs: Self) -> Self {
95        self.r *= rhs.r;
96        self.g *= rhs.g;
97        self.b *= rhs.b;
98        self
99    }
100}
101
102/// Support conversion from a color tuple
103impl From<(u8, u8, u8)> for RGB {
104    fn from(vals: (u8, u8, u8)) -> Self {
105        Self::named(vals)
106    }
107}
108
109/// Support conversion from HSV
110impl From<HSV> for RGB {
111    fn from(hsv: HSV) -> Self {
112        hsv.to_rgb()
113    }
114}
115
116/// Support conversion from RGBA
117impl From<RGBA> for RGB {
118    fn from(item: RGBA) -> Self {
119        Self::from_f32(item.r, item.g, item.b)
120    }
121}
122
123// Support conversion from Bevy
124#[cfg(feature = "bevy")]
125impl From<bevy::prelude::Color> for RGB {
126    fn from(item: bevy::prelude::Color) -> Self {
127        Self::from_f32(item.r(), item.g(), item.b())
128    }
129}
130
131#[cfg(feature = "bevy")]
132impl From<RGB> for bevy::prelude::Color {
133    fn from(item: RGB) -> Self {
134        Self::from([item.r, item.g, item.b])
135    }
136}
137
138impl RGB {
139    /// Constructs a new, zeroed (black) RGB triplet.
140    #[must_use]
141    pub fn new() -> Self {
142        Self {
143            r: 0.0,
144            g: 0.0,
145            b: 0.0,
146        }
147    }
148
149    /// Constructs a new RGB color, from 3 32-bit floats in the range 0..1
150    /// 
151    /// # Arguments
152    /// 
153    /// * `r` - the red component (0..1)
154    /// * `g` - the green component (0..1)
155    /// * `b` - the blue component (0..1)
156    /// 
157    /// # Example
158    /// 
159    /// ```rust
160    /// use bracket_color::prelude::*;
161    /// let red = RGB::from_f32(1.0, 0.0, 0.0);
162    /// let green = RGB::from_f32(0.0, 1.0, 0.0);
163    /// ```
164    #[inline]
165    #[must_use]
166    pub fn from_f32(r: f32, g: f32, b: f32) -> Self {
167        let r_clamped = f32::min(1.0, f32::max(0.0, r));
168        let g_clamped = f32::min(1.0, f32::max(0.0, g));
169        let b_clamped = f32::min(1.0, f32::max(0.0, b));
170        Self {
171            r: r_clamped,
172            g: g_clamped,
173            b: b_clamped,
174        }
175    }
176
177    /// Constructs a new RGB color, from 3 bytes in the range 0..255
178    /// 
179    /// # Arguments
180    /// 
181    /// * `r` - the red component, ranged from 0 to 255
182    /// * `g` - the green component, ranged from 0 to 255
183    /// * `b` - the blue component, ranged from 0 to 255
184    /// 
185    /// # Example
186    /// 
187    /// ```rust
188    /// use bracket_color::prelude::*;
189    /// let red = RGB::from_u8(255, 0, 0);
190    /// let green = RGB::from_u8(0, 255, 0);
191    /// ```
192    #[inline]
193    #[must_use]
194    pub fn from_u8(r: u8, g: u8, b: u8) -> Self {
195        Self {
196            r: f32::from(r) / 255.0,
197            g: f32::from(g) / 255.0,
198            b: f32::from(b) / 255.0,
199        }
200    }
201
202    /// Construct an RGB color from a tuple of u8, or a named constant
203    /// 
204    /// # Arguments
205    /// 
206    /// * `col` a tuple of three `u8` values. See `from_u8`. These are usually provided from the `named` colors list.
207    /// 
208    /// # Example
209    /// 
210    /// ```rust
211    /// use bracket_color::prelude::*;
212    /// let red = RGB::named(RED);
213    /// let green = RGB::named((0, 255, 0));
214    /// ```
215    #[inline]
216    #[must_use]
217    pub fn named(col: (u8, u8, u8)) -> Self {
218        Self::from_u8(col.0, col.1, col.2)
219    }
220
221    /// Constructs from an HTML color code (e.g. "#eeffee")
222    /// 
223    /// # Arguments
224    /// 
225    /// * `code` - an HTML color notation (e.g. "#ffeeff")
226    /// 
227    /// # Example
228    /// 
229    /// ```rust
230    /// use bracket_color::prelude::*;
231    /// let red = RGB::from_hex("#FF0000");
232    /// let green = RGB::from_hex("#00FF00");
233    /// ```
234    ///
235    /// # Errors
236    /// 
237    /// See `HtmlColorConversionError`
238    #[allow(clippy::cast_precision_loss)]
239    pub fn from_hex<S: AsRef<str>>(code: S) -> Result<Self, HtmlColorConversionError> {
240        let mut full_code = code.as_ref().chars();
241
242        if let Some(hash) = full_code.next() {
243            if hash != '#' {
244                return Err(HtmlColorConversionError::MissingHash);
245            }
246        } else {
247            return Err(HtmlColorConversionError::InvalidStringLength);
248        }
249
250        let red1 = match full_code.next() {
251            Some(red) => match red.to_digit(16) {
252                Some(red) => red * 16,
253                None => return Err(HtmlColorConversionError::InvalidCharacter),
254            },
255            None => return Err(HtmlColorConversionError::InvalidStringLength),
256        };
257        let red2 = match full_code.next() {
258            Some(red) => match red.to_digit(16) {
259                Some(red) => red,
260                None => return Err(HtmlColorConversionError::InvalidCharacter),
261            },
262            None => return Err(HtmlColorConversionError::InvalidStringLength),
263        };
264
265        let green1 = match full_code.next() {
266            Some(green) => match green.to_digit(16) {
267                Some(green) => green * 16,
268                None => return Err(HtmlColorConversionError::InvalidCharacter),
269            },
270            None => return Err(HtmlColorConversionError::InvalidStringLength),
271        };
272        let green2 = match full_code.next() {
273            Some(green) => match green.to_digit(16) {
274                Some(green) => green,
275                None => return Err(HtmlColorConversionError::InvalidCharacter),
276            },
277            None => return Err(HtmlColorConversionError::InvalidStringLength),
278        };
279
280        let blue1 = match full_code.next() {
281            Some(blue) => match blue.to_digit(16) {
282                Some(blue) => blue * 16,
283                None => return Err(HtmlColorConversionError::InvalidCharacter),
284            },
285            None => return Err(HtmlColorConversionError::InvalidStringLength),
286        };
287        let blue2 = match full_code.next() {
288            Some(blue) => match blue.to_digit(16) {
289                Some(blue) => blue,
290                None => return Err(HtmlColorConversionError::InvalidCharacter),
291            },
292            None => return Err(HtmlColorConversionError::InvalidStringLength),
293        };
294
295        if full_code.next().is_some() {
296            return Err(HtmlColorConversionError::InvalidStringLength);
297        }
298
299        Ok(Self {
300            r: (red1 + red2) as f32 / 255.0,
301            g: (green1 + green2) as f32 / 255.0,
302            b: (blue1 + blue2) as f32 / 255.0,
303        })
304    }
305
306    /// Converts an RGB triple to an HSV triple.
307    #[allow(clippy::many_single_char_names)]
308    #[must_use]
309    pub fn to_hsv(&self) -> HSV {
310        let r = self.r;
311        let g = self.g;
312        let b = self.b;
313
314        let max = f32::max(f32::max(r, g), b);
315        let min = f32::min(f32::min(r, g), b);
316
317        let mut h: f32 = max;
318        let v: f32 = max;
319
320        let d = max - min;
321        let s = if max == 0.0 { 0.0 } else { d / max };
322
323        if (max - min).abs() < std::f32::EPSILON {
324            h = 0.0; // Achromatic
325        } else {
326            if (max - r).abs() < std::f32::EPSILON {
327                if g < b {
328                    h = (g - b) / d + 6.0;
329                } else {
330                    h = (g - b) / d;
331                }
332            } else if (max - g).abs() < std::f32::EPSILON {
333                h = (b - r) / d + 2.0;
334            } else if (max - b).abs() < std::f32::EPSILON {
335                h = (r - g) / d + 4.0;
336            }
337
338            h /= 6.0;
339        }
340
341        HSV::from_f32(h, s, v)
342    }
343
344    /// Converts an RGB to an RGBA
345    #[inline]
346    #[must_use]
347    pub fn to_rgba(&self, alpha: f32) -> RGBA {
348        RGBA::from_f32(self.r, self.g, self.b, alpha)
349    }
350
351    /// Applies a quick grayscale conversion to the color
352    #[inline]
353    #[must_use]
354    pub fn to_greyscale(&self) -> Self {
355        let linear = (self.r * 0.2126) + (self.g * 0.7152) + (self.b * 0.0722);
356        Self::from_f32(linear, linear, linear)
357    }
358
359    /// Applies a lengthier desaturate (via HSV) to the color
360    #[inline]
361    #[must_use]
362    pub fn desaturate(&self) -> Self {
363        let mut hsv = self.to_hsv();
364        hsv.s = 0.0;
365        hsv.to_rgb()
366    }
367
368    /// Lerps by a specified percentage (from 0 to 1) between this color and another
369    #[inline]
370    #[must_use]
371    pub fn lerp(&self, color: Self, percent: f32) -> Self {
372        let range = (color.r - self.r, color.g - self.g, color.b - self.b);
373        Self {
374            r: self.r + range.0 * percent,
375            g: self.g + range.1 * percent,
376            b: self.b + range.2 * percent,
377        }
378    }
379}
380
381#[cfg(feature = "crossterm")]
382mod crossterm_features {
383    use super::RGB;
384    use crossterm::style::Color;
385    use std::convert::TryFrom;
386
387    impl TryFrom<RGB> for Color {
388        type Error = &'static str;
389
390        fn try_from(rgb: RGB) -> Result<Self, Self::Error> {
391            let (r, g, b) = (rgb.r, rgb.g, rgb.b);
392            for c in [r, g, b].iter() {
393                if *c < 0.0 {
394                    return Err("Value < 0.0 found!");
395                }
396                if *c > 1.0 {
397                    return Err("Value > 1.0 found!");
398                }
399            }
400            let (r, g, b) = ((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8);
401            let rgb = Color::Rgb { r, g, b };
402            Ok(rgb)
403        }
404    }
405
406    #[cfg(test)]
407    mod tests {
408        use crate::prelude::RGB;
409        use crossterm::style::Color;
410        use std::convert::TryInto;
411
412        #[test]
413        fn basic_conversion() {
414            let rgb = RGB {
415                r: 0.0,
416                g: 0.5,
417                b: 1.0,
418            };
419            let rgb: Color = rgb.try_into().unwrap();
420            match rgb {
421                Color::Rgb { r, g, b } => {
422                    assert_eq!(r, 0);
423                    assert_eq!(g, 127);
424                    assert_eq!(b, 255);
425                }
426                _ => unreachable!(),
427            }
428        }
429
430        #[test]
431        fn negative_rgb() {
432            let rgb = RGB {
433                r: 0.0,
434                g: 0.5,
435                b: -1.0,
436            };
437            let rgb: Result<Color, _> = rgb.try_into();
438            assert!(rgb.is_err());
439        }
440
441        #[test]
442        fn too_large_rgb() {
443            let rgb = RGB {
444                r: 0.0,
445                g: 0.5,
446                b: 1.1,
447            };
448            let rgb: Result<Color, _> = rgb.try_into();
449            assert!(rgb.is_err());
450        }
451    }
452}
453
454// Unit tests for the color system
455
456#[cfg(test)]
457mod tests {
458    use crate::prelude::*;
459
460    #[test]
461    // Tests that we make an RGB triplet at defaults and it is black.
462    fn make_rgb_minimal() {
463        let black = RGB::new();
464        assert!(black.r < std::f32::EPSILON);
465        assert!(black.g < std::f32::EPSILON);
466        assert!(black.b < std::f32::EPSILON);
467    }
468
469    #[test]
470    // Tests that we make an HSV triplet at defaults and it is black.
471    fn convert_olive_to_rgb() {
472        let grey = HSV::from_f32(60.0 / 360.0, 1.0, 0.501_960_8);
473        let rgb = grey.to_rgb();
474        assert!(f32::abs(rgb.r - 128.0 / 255.0) < std::f32::EPSILON);
475        assert!(f32::abs(rgb.g - 128.0 / 255.0) < std::f32::EPSILON);
476        assert!(rgb.b < std::f32::EPSILON);
477    }
478
479    #[test]
480    // Tests that we make an HSV triplet at defaults and it is black.
481    fn test_red_hex() {
482        let rgb = RGB::from_hex("#FF0000").expect("Invalid hex string");
483        assert!(f32::abs(rgb.r - 1.0) < std::f32::EPSILON);
484        assert!(rgb.g < std::f32::EPSILON);
485        assert!(rgb.b < std::f32::EPSILON);
486    }
487
488    #[test]
489    // Tests that we make an HSV triplet at defaults and it is black.
490    fn test_green_hex() {
491        let rgb = RGB::from_hex("#00FF00").expect("Invalid hex string");
492        assert!(rgb.r < std::f32::EPSILON);
493        assert!(f32::abs(rgb.g - 1.0) < std::f32::EPSILON);
494        assert!(rgb.b < std::f32::EPSILON);
495    }
496
497    #[test]
498    // Tests that we make an HSV triplet at defaults and it is black.
499    fn test_blue_hex() {
500        let rgb = RGB::from_hex("#0000FF").expect("Invalid hex string");
501        assert!(rgb.r < std::f32::EPSILON);
502        assert!(rgb.g < std::f32::EPSILON);
503        assert!(f32::abs(rgb.b - 1.0) < std::f32::EPSILON);
504    }
505
506    #[test]
507    // Tests that we make an HSV triplet at defaults and it is black.
508    fn test_blue_named() {
509        let rgb = RGB::named(BLUE);
510        assert!(rgb.r < std::f32::EPSILON);
511        assert!(rgb.g < std::f32::EPSILON);
512        assert!(f32::abs(rgb.b - 1.0) < std::f32::EPSILON);
513    }
514
515    #[test]
516    // Test the lerp function
517    fn test_lerp() {
518        let black = RGB::named(BLACK);
519        let white = RGB::named(WHITE);
520        assert!(black.lerp(white, 0.0) == black);
521        assert!(black.lerp(white, 1.0) == white);
522    }
523}