color.rs

  1use anyhow::bail;
  2use serde::de::{self, Deserialize, Deserializer, Visitor};
  3use std::fmt;
  4
  5/// Convert an RGB hex color code number to a color type
  6pub fn rgb<C: From<Rgba>>(hex: u32) -> C {
  7    let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
  8    let g = ((hex >> 8) & 0xFF) as f32 / 255.0;
  9    let b = (hex & 0xFF) as f32 / 255.0;
 10    Rgba { r, g, b, a: 1.0 }.into()
 11}
 12
 13/// Convert an RGBA hex color code number to [`Rgba`]
 14pub fn rgba(hex: u32) -> Rgba {
 15    let r = ((hex >> 24) & 0xFF) as f32 / 255.0;
 16    let g = ((hex >> 16) & 0xFF) as f32 / 255.0;
 17    let b = ((hex >> 8) & 0xFF) as f32 / 255.0;
 18    let a = (hex & 0xFF) as f32 / 255.0;
 19    Rgba { r, g, b, a }
 20}
 21
 22/// An RGBA color
 23#[derive(PartialEq, Clone, Copy, Default)]
 24pub struct Rgba {
 25    /// The red component of the color, in the range 0.0 to 1.0
 26    pub r: f32,
 27    /// The green component of the color, in the range 0.0 to 1.0
 28    pub g: f32,
 29    /// The blue component of the color, in the range 0.0 to 1.0
 30    pub b: f32,
 31    /// The alpha component of the color, in the range 0.0 to 1.0
 32    pub a: f32,
 33}
 34
 35impl fmt::Debug for Rgba {
 36    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 37        write!(f, "rgba({:#010x})", u32::from(*self))
 38    }
 39}
 40
 41impl Rgba {
 42    /// Create a new [`Rgba`] color by blending this and another color together
 43    /// TODO!(docs): find the source for this algorithm
 44    pub fn blend(&self, other: Rgba) -> Self {
 45        if other.a >= 1.0 {
 46            other
 47        } else if other.a <= 0.0 {
 48            return *self;
 49        } else {
 50            return Rgba {
 51                r: (self.r * (1.0 - other.a)) + (other.r * other.a),
 52                g: (self.g * (1.0 - other.a)) + (other.g * other.a),
 53                b: (self.b * (1.0 - other.a)) + (other.b * other.a),
 54                a: self.a,
 55            };
 56        }
 57    }
 58}
 59
 60impl From<Rgba> for u32 {
 61    fn from(rgba: Rgba) -> Self {
 62        let r = (rgba.r * 255.0) as u32;
 63        let g = (rgba.g * 255.0) as u32;
 64        let b = (rgba.b * 255.0) as u32;
 65        let a = (rgba.a * 255.0) as u32;
 66        (r << 24) | (g << 16) | (b << 8) | a
 67    }
 68}
 69
 70struct RgbaVisitor;
 71
 72impl<'de> Visitor<'de> for RgbaVisitor {
 73    type Value = Rgba;
 74
 75    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
 76        formatter.write_str("a string in the format #rrggbb or #rrggbbaa")
 77    }
 78
 79    fn visit_str<E: de::Error>(self, value: &str) -> Result<Rgba, E> {
 80        Rgba::try_from(value).map_err(E::custom)
 81    }
 82}
 83
 84impl<'de> Deserialize<'de> for Rgba {
 85    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
 86        deserializer.deserialize_str(RgbaVisitor)
 87    }
 88}
 89
 90impl From<Hsla> for Rgba {
 91    fn from(color: Hsla) -> Self {
 92        let h = color.h;
 93        let s = color.s;
 94        let l = color.l;
 95
 96        let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
 97        let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs());
 98        let m = l - c / 2.0;
 99        let cm = c + m;
100        let xm = x + m;
101
102        let (r, g, b) = match (h * 6.0).floor() as i32 {
103            0 | 6 => (cm, xm, m),
104            1 => (xm, cm, m),
105            2 => (m, cm, xm),
106            3 => (m, xm, cm),
107            4 => (xm, m, cm),
108            _ => (cm, m, xm),
109        };
110
111        Rgba {
112            r,
113            g,
114            b,
115            a: color.a,
116        }
117    }
118}
119
120impl TryFrom<&'_ str> for Rgba {
121    type Error = anyhow::Error;
122
123    fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
124        const RGB: usize = "rgb".len();
125        const RGBA: usize = "rgba".len();
126        const RRGGBB: usize = "rrggbb".len();
127        const RRGGBBAA: usize = "rrggbbaa".len();
128
129        const EXPECTED_FORMATS: &str = "Expected #rgb, #rgba, #rrggbb, or #rrggbbaa";
130
131        let Some(("", hex)) = value.trim().split_once('#') else {
132            bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}");
133        };
134
135        let (r, g, b, a) = match hex.len() {
136            RGB | RGBA => {
137                let r = u8::from_str_radix(&hex[0..1], 16)?;
138                let g = u8::from_str_radix(&hex[1..2], 16)?;
139                let b = u8::from_str_radix(&hex[2..3], 16)?;
140                let a = if hex.len() == RGBA {
141                    u8::from_str_radix(&hex[3..4], 16)?
142                } else {
143                    0xf
144                };
145
146                /// Duplicates a given hex digit.
147                /// E.g., `0xf` -> `0xff`.
148                const fn duplicate(value: u8) -> u8 {
149                    value << 4 | value
150                }
151
152                (duplicate(r), duplicate(g), duplicate(b), duplicate(a))
153            }
154            RRGGBB | RRGGBBAA => {
155                let r = u8::from_str_radix(&hex[0..2], 16)?;
156                let g = u8::from_str_radix(&hex[2..4], 16)?;
157                let b = u8::from_str_radix(&hex[4..6], 16)?;
158                let a = if hex.len() == RRGGBBAA {
159                    u8::from_str_radix(&hex[6..8], 16)?
160                } else {
161                    0xff
162                };
163                (r, g, b, a)
164            }
165            _ => bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"),
166        };
167
168        Ok(Rgba {
169            r: r as f32 / 255.,
170            g: g as f32 / 255.,
171            b: b as f32 / 255.,
172            a: a as f32 / 255.,
173        })
174    }
175}
176
177/// An HSLA color
178#[derive(Default, Copy, Clone, Debug)]
179#[repr(C)]
180pub struct Hsla {
181    /// Hue, in a range from 0 to 1
182    pub h: f32,
183
184    /// Saturation, in a range from 0 to 1
185    pub s: f32,
186
187    /// Lightness, in a range from 0 to 1
188    pub l: f32,
189
190    /// Alpha, in a range from 0 to 1
191    pub a: f32,
192}
193
194impl PartialEq for Hsla {
195    fn eq(&self, other: &Self) -> bool {
196        self.h
197            .total_cmp(&other.h)
198            .then(self.s.total_cmp(&other.s))
199            .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a)))
200            .is_eq()
201    }
202}
203
204impl PartialOrd for Hsla {
205    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
206        // SAFETY: The total ordering relies on this always being Some()
207        Some(
208            self.h
209                .total_cmp(&other.h)
210                .then(self.s.total_cmp(&other.s))
211                .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a))),
212        )
213    }
214}
215
216impl Ord for Hsla {
217    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
218        // SAFETY: The partial comparison is a total comparison
219        unsafe { self.partial_cmp(other).unwrap_unchecked() }
220    }
221}
222
223impl Eq for Hsla {}
224
225/// Construct an [`Hsla`] object from plain values
226pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
227    Hsla {
228        h: h.clamp(0., 1.),
229        s: s.clamp(0., 1.),
230        l: l.clamp(0., 1.),
231        a: a.clamp(0., 1.),
232    }
233}
234
235/// Pure black in [`Hsla`]
236pub fn black() -> Hsla {
237    Hsla {
238        h: 0.,
239        s: 0.,
240        l: 0.,
241        a: 1.,
242    }
243}
244
245/// Transparent black in [`Hsla`]
246pub fn transparent_black() -> Hsla {
247    Hsla {
248        h: 0.,
249        s: 0.,
250        l: 0.,
251        a: 0.,
252    }
253}
254
255/// Pure white in [`Hsla`]
256pub fn white() -> Hsla {
257    Hsla {
258        h: 0.,
259        s: 0.,
260        l: 1.,
261        a: 1.,
262    }
263}
264
265/// The color red in [`Hsla`]
266pub fn red() -> Hsla {
267    Hsla {
268        h: 0.,
269        s: 1.,
270        l: 0.5,
271        a: 1.,
272    }
273}
274
275/// The color blue in [`Hsla`]
276pub fn blue() -> Hsla {
277    Hsla {
278        h: 0.6,
279        s: 1.,
280        l: 0.5,
281        a: 1.,
282    }
283}
284
285/// The color green in [`Hsla`]
286pub fn green() -> Hsla {
287    Hsla {
288        h: 0.33,
289        s: 1.,
290        l: 0.5,
291        a: 1.,
292    }
293}
294
295/// The color yellow in [`Hsla`]
296pub fn yellow() -> Hsla {
297    Hsla {
298        h: 0.16,
299        s: 1.,
300        l: 0.5,
301        a: 1.,
302    }
303}
304
305impl Hsla {
306    /// Converts this HSLA color to an RGBA color.
307    pub fn to_rgb(self) -> Rgba {
308        self.into()
309    }
310
311    /// The color red
312    pub fn red() -> Self {
313        red()
314    }
315
316    /// The color green
317    pub fn green() -> Self {
318        green()
319    }
320
321    /// The color blue
322    pub fn blue() -> Self {
323        blue()
324    }
325
326    /// The color black
327    pub fn black() -> Self {
328        black()
329    }
330
331    /// The color white
332    pub fn white() -> Self {
333        white()
334    }
335
336    /// The color transparent black
337    pub fn transparent_black() -> Self {
338        transparent_black()
339    }
340
341    /// Returns true if the HSLA color is fully transparent, false otherwise.
342    pub fn is_transparent(&self) -> bool {
343        self.a == 0.0
344    }
345
346    /// Blends `other` on top of `self` based on `other`'s alpha value. The resulting color is a combination of `self`'s and `other`'s colors.
347    ///
348    /// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color.
349    /// If `other`'s alpha value is 0.0 or less, `other` color is fully transparent, thus `self` is returned as the output color.
350    /// Else, the output color is calculated as a blend of `self` and `other` based on their weighted alpha values.
351    ///
352    /// Assumptions:
353    /// - Alpha values are contained in the range [0, 1], with 1 as fully opaque and 0 as fully transparent.
354    /// - The relative contributions of `self` and `other` is based on `self`'s alpha value (`self.a`) and `other`'s  alpha value (`other.a`), `self` contributing `self.a * (1.0 - other.a)` and `other` contributing its own alpha value.
355    /// - RGB color components are contained in the range [0, 1].
356    /// - If `self` and `other` colors are out of the valid range, the blend operation's output and behavior is undefined.
357    pub fn blend(self, other: Hsla) -> Hsla {
358        let alpha = other.a;
359
360        if alpha >= 1.0 {
361            other
362        } else if alpha <= 0.0 {
363            return self;
364        } else {
365            let converted_self = Rgba::from(self);
366            let converted_other = Rgba::from(other);
367            let blended_rgb = converted_self.blend(converted_other);
368            return Hsla::from(blended_rgb);
369        }
370    }
371
372    /// Returns a new HSLA color with the same hue, and lightness, but with no saturation.
373    pub fn grayscale(&self) -> Self {
374        Hsla {
375            h: self.h,
376            s: 0.,
377            l: self.l,
378            a: self.a,
379        }
380    }
381
382    /// Fade out the color by a given factor. This factor should be between 0.0 and 1.0.
383    /// Where 0.0 will leave the color unchanged, and 1.0 will completely fade out the color.
384    pub fn fade_out(&mut self, factor: f32) {
385        self.a *= 1.0 - factor.clamp(0., 1.);
386    }
387}
388
389impl From<Rgba> for Hsla {
390    fn from(color: Rgba) -> Self {
391        let r = color.r;
392        let g = color.g;
393        let b = color.b;
394
395        let max = r.max(g.max(b));
396        let min = r.min(g.min(b));
397        let delta = max - min;
398
399        let l = (max + min) / 2.0;
400        let s = if l == 0.0 || l == 1.0 {
401            0.0
402        } else if l < 0.5 {
403            delta / (2.0 * l)
404        } else {
405            delta / (2.0 - 2.0 * l)
406        };
407
408        let h = if delta == 0.0 {
409            0.0
410        } else if max == r {
411            ((g - b) / delta).rem_euclid(6.0) / 6.0
412        } else if max == g {
413            ((b - r) / delta + 2.0) / 6.0
414        } else {
415            ((r - g) / delta + 4.0) / 6.0
416        };
417
418        Hsla {
419            h,
420            s,
421            l,
422            a: color.a,
423        }
424    }
425}
426
427impl<'de> Deserialize<'de> for Hsla {
428    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
429    where
430        D: Deserializer<'de>,
431    {
432        // First, deserialize it into Rgba
433        let rgba = Rgba::deserialize(deserializer)?;
434
435        // Then, use the From<Rgba> for Hsla implementation to convert it
436        Ok(Hsla::from(rgba))
437    }
438}
439
440#[cfg(test)]
441mod tests {
442    use serde_json::json;
443
444    use super::*;
445
446    #[test]
447    fn test_deserialize_three_value_hex_to_rgba() {
448        let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap();
449
450        assert_eq!(actual, rgba(0xff0099ff))
451    }
452
453    #[test]
454    fn test_deserialize_four_value_hex_to_rgba() {
455        let actual: Rgba = serde_json::from_value(json!("#f09f")).unwrap();
456
457        assert_eq!(actual, rgba(0xff0099ff))
458    }
459
460    #[test]
461    fn test_deserialize_six_value_hex_to_rgba() {
462        let actual: Rgba = serde_json::from_value(json!("#ff0099")).unwrap();
463
464        assert_eq!(actual, rgba(0xff0099ff))
465    }
466
467    #[test]
468    fn test_deserialize_eight_value_hex_to_rgba() {
469        let actual: Rgba = serde_json::from_value(json!("#ff0099ff")).unwrap();
470
471        assert_eq!(actual, rgba(0xff0099ff))
472    }
473
474    #[test]
475    fn test_deserialize_eight_value_hex_with_padding_to_rgba() {
476        let actual: Rgba = serde_json::from_value(json!(" #f5f5f5ff   ")).unwrap();
477
478        assert_eq!(actual, rgba(0xf5f5f5ff))
479    }
480
481    #[test]
482    fn test_deserialize_eight_value_hex_with_mixed_case_to_rgba() {
483        let actual: Rgba = serde_json::from_value(json!("#DeAdbEeF")).unwrap();
484
485        assert_eq!(actual, rgba(0xdeadbeef))
486    }
487}