color.rs

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