color.rs

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