Skip to main content

kozan_primitives/
color.rs

1/// sRGB color with alpha, stored as four `f32` channels in [0.0, 1.0].
2///
3/// Uses straight (non-premultiplied) alpha. Conversion to premultiplied
4/// happens at the rendering boundary when handing off to the GPU.
5#[derive(Clone, Copy, Debug, PartialEq)]
6pub struct Color {
7    pub r: f32,
8    pub g: f32,
9    pub b: f32,
10    pub a: f32,
11}
12
13impl Color {
14    pub const TRANSPARENT: Self = Self::rgba(0.0, 0.0, 0.0, 0.0);
15    pub const BLACK: Self = Self::rgb(0.0, 0.0, 0.0);
16    pub const WHITE: Self = Self::rgb(1.0, 1.0, 1.0);
17    pub const RED: Self = Self::rgb(1.0, 0.0, 0.0);
18    pub const GREEN: Self = Self::rgb(0.0, 1.0, 0.0);
19    pub const BLUE: Self = Self::rgb(0.0, 0.0, 1.0);
20
21    #[must_use]
22    pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
23        Self { r, g, b, a: 1.0 }
24    }
25
26    #[must_use]
27    pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
28        Self { r, g, b, a }
29    }
30
31    /// Construct from 8-bit per channel values (0–255).
32    #[must_use]
33    pub fn from_rgb8(r: u8, g: u8, b: u8) -> Self {
34        Self::rgb(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0)
35    }
36
37    #[must_use]
38    pub fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self {
39        Self::rgba(
40            r as f32 / 255.0,
41            g as f32 / 255.0,
42            b as f32 / 255.0,
43            a as f32 / 255.0,
44        )
45    }
46
47    /// Construct from a 32-bit hex value: `0xRRGGBB` or `0xRRGGBBAA`.
48    #[must_use]
49    pub fn from_hex(hex: u32) -> Self {
50        if hex > 0xFFFFFF {
51            Self::from_rgba8(
52                ((hex >> 24) & 0xFF) as u8,
53                ((hex >> 16) & 0xFF) as u8,
54                ((hex >> 8) & 0xFF) as u8,
55                (hex & 0xFF) as u8,
56            )
57        } else {
58            Self::from_rgb8(
59                ((hex >> 16) & 0xFF) as u8,
60                ((hex >> 8) & 0xFF) as u8,
61                (hex & 0xFF) as u8,
62            )
63        }
64    }
65
66    #[must_use]
67    pub fn with_alpha(self, a: f32) -> Self {
68        Self { a, ..self }
69    }
70
71    #[must_use]
72    pub fn is_opaque(self) -> bool {
73        self.a >= 1.0
74    }
75
76    #[must_use]
77    pub fn is_transparent(self) -> bool {
78        self.a <= 0.0
79    }
80
81    /// Linear interpolation between two colors.
82    #[must_use]
83    pub fn lerp(self, other: Self, t: f32) -> Self {
84        Self {
85            r: self.r + (other.r - self.r) * t,
86            g: self.g + (other.g - self.g) * t,
87            b: self.b + (other.b - self.b) * t,
88            a: self.a + (other.a - self.a) * t,
89        }
90    }
91
92    /// Pack to 32-bit RGBA (8 bits per channel).
93    #[must_use]
94    pub fn to_rgba8(self) -> [u8; 4] {
95        [
96            (self.r.clamp(0.0, 1.0) * 255.0 + 0.5) as u8,
97            (self.g.clamp(0.0, 1.0) * 255.0 + 0.5) as u8,
98            (self.b.clamp(0.0, 1.0) * 255.0 + 0.5) as u8,
99            (self.a.clamp(0.0, 1.0) * 255.0 + 0.5) as u8,
100        ]
101    }
102}
103
104impl Default for Color {
105    fn default() -> Self {
106        Self::BLACK
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn from_rgb8_roundtrip() {
116        let c = Color::from_rgb8(128, 64, 255);
117        let [r, g, b, a] = c.to_rgba8();
118        assert_eq!(r, 128);
119        assert_eq!(g, 64);
120        assert_eq!(b, 255);
121        assert_eq!(a, 255);
122    }
123
124    #[test]
125    fn from_hex_rgb() {
126        let c = Color::from_hex(0xFF8000);
127        let [r, g, b, _] = c.to_rgba8();
128        assert_eq!(r, 255);
129        assert_eq!(g, 128);
130        assert_eq!(b, 0);
131    }
132
133    #[test]
134    fn from_hex_rgba() {
135        let c = Color::from_hex(0xFF800080);
136        let [r, g, b, a] = c.to_rgba8();
137        assert_eq!(r, 255);
138        assert_eq!(g, 128);
139        assert_eq!(b, 0);
140        assert_eq!(a, 128);
141    }
142
143    #[test]
144    fn with_alpha() {
145        let c = Color::RED.with_alpha(0.5);
146        assert_eq!(c.r, 1.0);
147        assert!((c.a - 0.5).abs() < f32::EPSILON);
148    }
149
150    #[test]
151    fn lerp_midpoint() {
152        let mid = Color::BLACK.lerp(Color::WHITE, 0.5);
153        assert!((mid.r - 0.5).abs() < f32::EPSILON);
154        assert!((mid.g - 0.5).abs() < f32::EPSILON);
155        assert!((mid.b - 0.5).abs() < f32::EPSILON);
156    }
157
158    #[test]
159    fn opaque_and_transparent() {
160        assert!(Color::RED.is_opaque());
161        assert!(!Color::RED.is_transparent());
162        assert!(Color::TRANSPARENT.is_transparent());
163        assert!(!Color::TRANSPARENT.is_opaque());
164    }
165}