Skip to main content

launchy/canvas/
color.rs

1/// A simple float-based color struct. Each component should lie in 0..=1, but it can also be
2/// outside that range. If outside, it will be clipped for some operations
3#[derive(Debug, Copy, Clone, PartialEq, Default)]
4#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
5pub struct Color {
6    pub r: f32,
7    pub g: f32,
8    pub b: f32,
9}
10
11impl Color {
12    pub const BLACK: Color = Color {
13        r: 0.0,
14        g: 0.0,
15        b: 0.0,
16    };
17    pub const WHITE: Color = Color {
18        r: 1.0,
19        g: 1.0,
20        b: 1.0,
21    };
22    pub const RED: Color = Color {
23        r: 1.0,
24        g: 0.0,
25        b: 0.0,
26    };
27    pub const GREEN: Color = Color {
28        r: 0.0,
29        g: 1.0,
30        b: 0.0,
31    };
32    pub const BLUE: Color = Color {
33        r: 0.0,
34        g: 0.0,
35        b: 1.0,
36    };
37    pub const CYAN: Color = Color {
38        r: 0.0,
39        g: 1.0,
40        b: 1.0,
41    };
42    pub const MAGENTA: Color = Color {
43        r: 1.0,
44        g: 0.0,
45        b: 1.0,
46    };
47    pub const YELLOW: Color = Color {
48        r: 1.0,
49        g: 1.0,
50        b: 0.0,
51    };
52
53    /// Create a new color from the given red, green, and blue components
54    ///
55    /// Examples:
56    /// ```
57    /// # use launchy::Color;
58    /// let lime = Color::new(0.75, 1.0, 0.0);
59    /// let beige = Color::new(0.96, 0.96, 0.86);
60    /// ```
61    pub fn new(r: f32, g: f32, b: f32) -> Self {
62        Self { r, g, b }
63    }
64
65    /// Creates a color from a hue, starting at 0.0 (red) and ending at 1.0 (red). You can pass in
66    /// any number though, because the cycle repeats (think the `x` in `sin(x)`)
67    ///
68    /// ```
69    /// # use launchy::Color;
70    /// let red = Color::from_hue(0.0);
71    /// let orange = Color::from_hue(0.1);
72    /// let greenish_yellow = Color::from_hue(0.2);
73    /// let green = Color::from_hue(0.3);
74    /// let cyan = Color::from_hue(0.4);
75    /// let light_blue = Color::from_hue(0.5);
76    /// let blue = Color::from_hue(0.6);
77    /// let purple = Color::from_hue(0.7);
78    /// let light_pink = Color::from_hue(0.8);
79    /// let strong_pink = Color::from_hue(0.9);
80    /// ```
81    pub fn from_hue(hue: f32) -> Self {
82        match hue * 6.0 {
83            hue if (0.0..1.0).contains(&hue) => Self::new(1.0, hue, 0.0), // red -> yellow
84            hue if (1.0..2.0).contains(&hue) => Self::new(2.0 - hue, 1.0, 0.0), // yellow -> green
85            hue if (2.0..3.0).contains(&hue) => Self::new(0.0, 1.0, hue - 2.0), // green -> cyan
86            hue if (3.0..4.0).contains(&hue) => Self::new(0.0, 4.0 - hue, 1.0), // cyan -> blue
87            hue if (4.0..5.0).contains(&hue) => Self::new(hue - 4.0, 0.0, 1.0), // blue -> magenta
88            hue if (5.0..6.0).contains(&hue) => Self::new(1.0, 0.0, 6.0 - hue), // magenta -> red
89            _ => {
90                // calculate hue % 1 and then stick the modulo-ed value in
91                let hue = hue.fract();
92                let hue = if hue < 0.0 { 1.0 + hue } else { hue };
93                Self::from_hue(hue)
94            }
95        }
96    }
97
98    /// Util function that smoothly interpolates between the following 'keyframes':
99    /// - 0.00 => green
100    /// - 0.25 => yellow
101    /// - 0.50 => red
102    /// - 0.75 => yellow
103    /// - 1.00 => green
104    ///
105    /// and then the cycle continues.
106    ///
107    /// This function is useful to create a smooth cycling gradient of colors on non-RGB devices
108    /// such as the Launchpad S.
109    pub fn red_green_color(hue: f32) -> Self {
110        let a = |x| {
111            if x < 0.25 {
112                4.0 * x
113            } else if x >= 0.75 {
114                4.0 - 4.0 * x
115            } else {
116                1.0
117            }
118        };
119
120        let r = a(hue.fract());
121        let g = a((hue + 0.5).fract());
122        Self::new(r, g, 0.0)
123    }
124
125    /// Clamp to a range of 0..=1.
126    ///
127    /// If any component is below 0, it is brought up to 0. If any component is above 1, every
128    /// component will be multiplied by a certain value so that every component will be at most 1.
129    ///
130    /// This algorithm ensures that the color hue stays the same, no matter the brightness.
131    ///
132    /// ```rust
133    /// # use launchy::Color;
134    /// assert_eq!(
135    ///     launchy::Color::new(-1.0, 1.0, 3.0).clamp(),
136    ///     launchy::Color::new(0.0, 1.0/3.0, 1.0),
137    /// );
138    /// ```
139    pub fn clamp(self) -> Self {
140        let Self { r, g, b } = self;
141
142        let highest_component = r.max(g).max(b);
143        let multiplier = if highest_component > 1.0 {
144            1.0 / highest_component
145        } else {
146            1.0
147        };
148
149        let r = f32::clamp(r * multiplier, 0.0, 1.0);
150        let g = f32::clamp(g * multiplier, 0.0, 1.0);
151        let b = f32::clamp(b * multiplier, 0.0, 1.0);
152
153        Self { r, g, b }
154    }
155
156    /// Return a tuple of color components scaled from 0..=1 to 0..range using the same algorithm
157    /// as in [`Self::clamp`].
158    ///
159    /// This function is used by the Canvas implementation of the Launchpads to downscale the
160    /// high-precision [`Color`]s to their respective color width. For example the Launchpad S only
161    /// supports four levels of brightness for its red and green component, respectively. Therefore,
162    /// the Launchpad S calls `.quantize(4)` on a given [`Color`] to derive how that color should be
163    /// represented on the Launchpad S LEDs.
164    pub fn quantize(self, range: u8) -> (u8, u8, u8) {
165        let Self { r, g, b } = self.clamp();
166
167        let quantize_component = |c| u8::min((c * range as f32) as u8, range - 1);
168        (
169            quantize_component(r),
170            quantize_component(g),
171            quantize_component(b),
172        )
173    }
174
175    /// Mix two colors together. The proportion of the second color is specified by
176    /// `proportion_of_other`.
177    ///
178    /// Examples:
179    /// ```
180    /// # use launchy::Color;
181    /// let very_dark_red = Color::RED.mix(Color::BLACK, 0.9);
182    /// let orange = Color::RED.mix(Color::YELLOW, 0.5);
183    /// let dark_brown = Color::RED.mix(Color::YELLOW, 0.5).mix(Color::BLACK, 0.7);
184    /// ```
185    pub fn mix(self, other: Color, proportion_of_other: f32) -> Color {
186        other * proportion_of_other + self * (1.0 - proportion_of_other)
187    }
188}
189
190impl std::cmp::Eq for Color {}
191
192impl std::ops::Mul<f32> for Color {
193    type Output = Self;
194
195    fn mul(self, multiplier: f32) -> Self::Output {
196        Self {
197            r: self.r * multiplier,
198            g: self.g * multiplier,
199            b: self.b * multiplier,
200        }
201    }
202}
203
204impl std::ops::Div<f32> for Color {
205    type Output = Self;
206
207    fn div(self, multiplier: f32) -> Self::Output {
208        Self {
209            r: self.r / multiplier,
210            g: self.g / multiplier,
211            b: self.b / multiplier,
212        }
213    }
214}
215
216impl std::ops::Add for Color {
217    type Output = Self;
218
219    fn add(self, other: Self) -> Self {
220        Self {
221            r: self.r + other.r,
222            g: self.g + other.g,
223            b: self.b + other.b,
224        }
225    }
226}
227
228impl std::ops::Sub for Color {
229    type Output = Self;
230
231    fn sub(self, other: Self) -> Self {
232        Self {
233            r: self.r - other.r,
234            g: self.g - other.g,
235            b: self.b - other.b,
236        }
237    }
238}
239
240impl std::ops::Add<f32> for Color {
241    type Output = Self;
242
243    fn add(self, addend: f32) -> Self {
244        Self {
245            r: self.r + addend,
246            g: self.g + addend,
247            b: self.b + addend,
248        }
249    }
250}
251
252impl std::ops::Sub<f32> for Color {
253    type Output = Self;
254
255    fn sub(self, subtrand /* or something like that */: f32) -> Self {
256        Self {
257            r: self.r - subtrand,
258            g: self.g - subtrand,
259            b: self.b - subtrand,
260        }
261    }
262}
263
264impl std::ops::Neg for Color {
265    type Output = Self;
266
267    fn neg(self) -> Self {
268        Self {
269            r: -self.r,
270            g: -self.g,
271            b: -self.b,
272        }
273    }
274}
275
276impl std::iter::Sum for Color {
277    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
278        iter.fold(Color::BLACK, |a, b| a + b)
279    }
280}
281
282#[cfg(feature = "embedded-graphics")]
283impl From<Color> for embedded_graphics::pixelcolor::Rgb888 {
284    fn from(color: Color) -> Self {
285        let (r, g, b) = color.quantize(255);
286        Self::new(r, g, b)
287    }
288}
289
290#[cfg(feature = "embedded-graphics")]
291impl From<embedded_graphics::pixelcolor::Rgb888> for Color {
292    fn from(color: embedded_graphics::pixelcolor::Rgb888) -> Self {
293        use embedded_graphics::pixelcolor::RgbColor;
294
295        Color::new(
296            (color.r() as f32 + 0.5) / 256.0,
297            (color.g() as f32 + 0.5) / 256.0,
298            (color.b() as f32 + 0.5) / 256.0,
299        )
300    }
301}