Skip to main content

yog_graphics/
color.rs

1//! Colors: `0xAARRGGBB`, straight alpha — what every Yog format and API
2//! passes.
3
4/// A color, `0xAARRGGBB`.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
6pub struct Color(pub u32);
7
8/// Where alpha sits in `0xAARRGGBB`, and the bits it and the rest take.
9const ALPHA_SHIFT: u32 = 24;
10const ALPHA_BITS: u32 = 0xff00_0000;
11const RGB_BITS: u32 = 0x00ff_ffff;
12/// A channel's top, as a fraction's 1.
13const CHANNEL_MAX: f32 = u8::MAX as f32;
14
15impl Color {
16    pub const TRANSPARENT: Color = Color(0);
17    pub const WHITE: Color = Color(0xffff_ffff);
18    pub const BLACK: Color = Color(0xff00_0000);
19
20    pub const fn argb(a: u8, r: u8, g: u8, b: u8) -> Color {
21        Color((a as u32) << 24 | (r as u32) << 16 | (g as u32) << 8 | b as u32)
22    }
23
24    /// Opaque `0xRRGGBB`.
25    pub const fn rgb(rgb: u32) -> Color {
26        Color(ALPHA_BITS | rgb)
27    }
28
29    pub const fn a(self) -> u8 {
30        (self.0 >> ALPHA_SHIFT) as u8
31    }
32    pub const fn r(self) -> u8 {
33        (self.0 >> 16) as u8
34    }
35    pub const fn g(self) -> u8 {
36        (self.0 >> 8) as u8
37    }
38    pub const fn b(self) -> u8 {
39        self.0 as u8
40    }
41
42    /// `[r, g, b, a]` bytes, as an RGBA image keeps them.
43    pub const fn to_rgba8(self) -> crate::Rgba8 {
44        [self.r(), self.g(), self.b(), self.a()]
45    }
46
47    pub const fn from_rgba8(p: crate::Rgba8) -> Color {
48        Color::argb(p[3], p[0], p[1], p[2])
49    }
50
51    /// `[r, g, b, a]` in `0..=1`.
52    pub fn to_f32(self) -> [f32; 4] {
53        [self.r(), self.g(), self.b(), self.a()].map(|v| v as f32 / CHANNEL_MAX)
54    }
55
56    pub fn from_f32(c: [f32; 4]) -> Color {
57        let [r, g, b, a] = c.map(|v| (v.clamp(0.0, 1.0) * CHANNEL_MAX).round() as u8);
58        Color::argb(a, r, g, b)
59    }
60
61    /// Each channel multiplied by `o`'s (a tint).
62    pub fn multiply(self, o: Color) -> Color {
63        let (a, b) = (self.to_f32(), o.to_f32());
64        Color::from_f32([a[0] * b[0], a[1] * b[1], a[2] * b[2], a[3] * b[3]])
65    }
66
67    /// `#RRGGBB` / `#AARRGGBB` (or `0x…`, `_` allowed), or a decimal
68    /// `0xAARRGGBB`.
69    pub fn parse(value: &str) -> Option<Color> {
70        let v = value.trim();
71        if !v.is_empty() && v.bytes().all(|b| b.is_ascii_digit()) {
72            return v.parse().ok().map(Color);
73        }
74        let hex = v
75            .strip_prefix('#')
76            .or_else(|| v.strip_prefix("0x"))
77            .or_else(|| v.strip_prefix("0X"))?
78            .replace('_', "");
79        let n = u32::from_str_radix(&hex, 16).ok()?;
80        match hex.len() {
81            6 => Some(Color::rgb(n)),
82            8 => Some(Color(n)),
83            _ => None,
84        }
85    }
86
87    /// With alpha `a`.
88    pub const fn with_alpha(self, a: u8) -> Color {
89        Color(self.0 & RGB_BITS | (a as u32) << ALPHA_SHIFT)
90    }
91}
92
93impl From<u32> for Color {
94    fn from(v: u32) -> Self {
95        Color(v)
96    }
97}
98
99/// In Yog Data, a color value: `#RRGGBB`/`#AARRGGBB` in text.
100impl yog_data::Schema for Color {
101    fn shape() -> yog_data::schema::Shape {
102        yog_data::schema::Shape::Color
103    }
104    fn read(v: &yog_data::ValueRef<'_>, cx: &mut yog_data::schema::Reader) -> Option<Self> {
105        match v {
106            yog_data::ValueRef::Color(c) => Some(Color(*c)),
107            yog_data::ValueRef::Str(s) if s.starts_with('#') => {
108                yog_data::typed::parse_color(s).map(Color).or_else(|| {
109                    cx.problem(format!("`{s}` isn't a color (#RRGGBB or #AARRGGBB)"));
110                    None
111                })
112            }
113            other => {
114                cx.expected("a color (#RRGGBB or #AARRGGBB)", other);
115                None
116            }
117        }
118    }
119    fn write(&self) -> yog_data::Value {
120        yog_data::Value::Color(self.0)
121    }
122}