Skip to main content

game_gem/
color.rs

1//! Color types with comprehensive construction and manipulation.
2//!
3//! Unlike macroquad's bare `Color` struct, `game-gem` provides:
4//! - Named color constants (CSS4 extended)
5//! - Alpha-premultiplied blending helpers
6//! - HSL/HSV conversion
7//! - Lerp between colors
8//! - Parse from hex strings
9
10use std::str::FromStr;
11use crate::math::FloatExt;
12
13/// A color represented as linear RGBA floats (0.0–1.0).
14///
15/// All game-gem drawing functions accept this type.
16/// Internal rendering may convert to premultiplied alpha.
17#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
18#[repr(C)]
19pub struct Color {
20    /// Red channel (0.0–1.0).
21    pub r: f32,
22    /// Green channel (0.0–1.0).
23    pub g: f32,
24    /// Blue channel (0.0–1.0).
25    pub b: f32,
26    /// Alpha channel (0.0–1.0). 0.0 = fully transparent, 1.0 = fully opaque.
27    pub a: f32,
28}
29
30impl Color {
31    /// Create a new color from RGBA components (0.0–1.0).
32    #[inline]
33    pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
34        Self { r, g, b, a }
35    }
36
37    /// Create a fully opaque RGB color.
38    #[inline]
39    pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
40        Self { r, g, b, a: 1.0 }
41    }
42
43    /// Create from 8-bit RGBA values (0–255).
44    #[inline]
45    pub fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self {
46        Self {
47            r: r as f32 / 255.0,
48            g: g as f32 / 255.0,
49            b: b as f32 / 255.0,
50            a: a as f32 / 255.0,
51        }
52    }
53
54    /// Create from 8-bit RGB values (0–255), fully opaque.
55    #[inline]
56    pub fn from_rgb8(r: u8, g: u8, b: u8) -> Self {
57        Self::from_rgba8(r, g, b, 255)
58    }
59
60    /// Create from a hex string like `"#FF0080"` or `"FF008080"`.
61    ///
62    /// - 6 digits → RGB (fully opaque)
63    /// - 8 digits → RGBA
64    /// - Optional leading `#`
65    pub fn from_hex(hex: &str) -> Result<Self, ColorParseError> {
66        let hex = hex.trim_start_matches('#');
67        let val = u32::from_str_radix(hex, 16)
68            .map_err(|_| ColorParseError::InvalidHex(hex.to_string()))?;
69
70        match hex.len() {
71            6 => Ok(Self::from_rgb8(
72                ((val >> 16) & 0xFF) as u8,
73                ((val >> 8) & 0xFF) as u8,
74                (val & 0xFF) as u8,
75            )),
76            8 => Ok(Self::from_rgba8(
77                ((val >> 24) & 0xFF) as u8,
78                ((val >> 16) & 0xFF) as u8,
79                ((val >> 8) & 0xFF) as u8,
80                (val & 0xFF) as u8,
81            )),
82            _ => Err(ColorParseError::InvalidLength(hex.len())),
83        }
84    }
85
86    /// Convert to a 32-bit RGBA integer (0xRRGGBBAA).
87    pub fn to_rgba32(self) -> u32 {
88        ((self.r.clamp(0.0, 1.0) * 255.0) as u32) << 24
89            | ((self.g.clamp(0.0, 1.0) * 255.0) as u32) << 16
90            | ((self.b.clamp(0.0, 1.0) * 255.0) as u32) << 8
91            | (self.a.clamp(0.0, 1.0) * 255.0) as u32
92    }
93
94    /// Convert to premultiplied alpha form.
95    #[inline]
96    pub fn premultiplied(self) -> Self {
97        Self {
98            r: self.r * self.a,
99            g: self.g * self.a,
100            b: self.b * self.a,
101            a: self.a,
102        }
103    }
104
105    /// Get the luminance (perceived brightness).
106    #[inline]
107    pub fn luminance(self) -> f32 {
108        0.2126 * self.r + 0.7152 * self.g + 0.0722 * self.b
109    }
110
111    /// Lighten the color by `amount` (0.0–1.0).
112    pub fn lightened(self, amount: f32) -> Self {
113        Self {
114            r: (self.r + (1.0 - self.r) * amount).min(1.0),
115            g: (self.g + (1.0 - self.g) * amount).min(1.0),
116            b: (self.b + (1.0 - self.b) * amount).min(1.0),
117            a: self.a,
118        }
119    }
120
121    /// Darken the color by `amount` (0.0–1.0).
122    pub fn darkened(self, amount: f32) -> Self {
123        Self {
124            r: (self.r * (1.0 - amount)).max(0.0),
125            g: (self.g * (1.0 - amount)).max(0.0),
126            b: (self.b * (1.0 - amount)).max(0.0),
127            a: self.a,
128        }
129    }
130
131    /// Return the color with a new alpha.
132    #[inline]
133    pub fn with_alpha(self, a: f32) -> Self {
134        Self { a, ..self }
135    }
136
137    /// Linear interpolation between two colors.
138    #[inline]
139    pub fn lerp(self, other: Color, t: f32) -> Color {
140        Color {
141            r: self.r.lerp(other.r, t),
142            g: self.g.lerp(other.g, t),
143            b: self.b.lerp(other.b, t),
144            a: self.a.lerp(other.a, t),
145        }
146    }
147
148    /// Convert to HSLA (hue: 0–360, sat/light/alpha: 0–1).
149    pub fn to_hsla(self) -> (f32, f32, f32, f32) {
150        let max = self.r.max(self.g).max(self.b);
151        let min = self.r.min(self.g).min(self.b);
152        let lightness = (max + min) / 2.0;
153
154        if (max - min).abs() < 1e-6 {
155            return (0.0, 0.0, lightness, self.a);
156        }
157
158        let d = max - min;
159        let saturation = if lightness > 0.5 {
160            d / (2.0 - max - min)
161        } else {
162            d / (max + min)
163        };
164
165        let hue = if (max - self.r).abs() < 1e-6 {
166            ((self.g - self.b) / d + (if self.g < self.b { 6.0 } else { 0.0 })) * 60.0
167        } else if (max - self.g).abs() < 1e-6 {
168            ((self.b - self.r) / d + 2.0) * 60.0
169        } else {
170            ((self.r - self.g) / d + 4.0) * 60.0
171        };
172
173        (hue, saturation, lightness, self.a)
174    }
175
176    /// Create from HSLA values (hue: 0–360, sat/light/alpha: 0–1).
177    pub fn from_hsla(h: f32, s: f32, l: f32, a: f32) -> Self {
178        if (s - 0.0).abs() < 1e-6 {
179            return Color::new(l, l, l, a);
180        }
181
182        let hue2 = h / 60.0;
183        let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
184        let x = c * (1.0 - (hue2 % 2.0 - 1.0).abs());
185        let m = l - c / 2.0;
186
187        let (r1, g1, b1) = match hue2 as i32 {
188            0 => (c, x, 0.0),
189            1 => (x, c, 0.0),
190            2 => (0.0, c, x),
191            3 => (0.0, x, c),
192            4 => (x, 0.0, c),
193            _ => (c, 0.0, x),
194        };
195
196        Color::new(r1 + m, g1 + m, b1 + m, a)
197    }
198}
199
200impl Default for Color {
201    fn default() -> Self {
202        Color::WHITE
203    }
204}
205
206impl std::fmt::Display for Color {
207    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208        write!(
209            f,
210            "Color(#{:02X}{:02X}{:02X}{:02X})",
211            (self.r * 255.0) as u8,
212            (self.g * 255.0) as u8,
213            (self.b * 255.0) as u8,
214            (self.a * 255.0) as u8,
215        )
216    }
217}
218
219impl FromStr for Color {
220    type Err = ColorParseError;
221    fn from_str(s: &str) -> Result<Self, Self::Err> {
222        Color::from_hex(s)
223    }
224}
225
226/// Error type for color parsing.
227#[derive(Debug, Clone)]
228pub enum ColorParseError {
229    InvalidHex(String),
230    InvalidLength(usize),
231}
232
233impl std::fmt::Display for ColorParseError {
234    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235        match self {
236            ColorParseError::InvalidHex(h) => write!(f, "Invalid hex color: {}", h),
237            ColorParseError::InvalidLength(len) => {
238                write!(f, "Expected 6 or 8 hex digits, got {}", len)
239            }
240        }
241    }
242}
243
244impl std::error::Error for ColorParseError {}
245
246// --- Named color constants (CSS4 extended + common game colors) ---
247
248impl Color {
249    pub const WHITE:   Color = Color::new(1.0, 1.0, 1.0, 1.0);
250    pub const BLACK:   Color = Color::new(0.0, 0.0, 0.0, 1.0);
251    pub const RED:     Color = Color::new(1.0, 0.0, 0.0, 1.0);
252    pub const GREEN:   Color = Color::new(0.0, 0.8, 0.0, 1.0);
253    pub const BLUE:    Color = Color::new(0.0, 0.0, 1.0, 1.0);
254    pub const YELLOW:  Color = Color::new(1.0, 1.0, 0.0, 1.0);
255    pub const CYAN:    Color = Color::new(0.0, 1.0, 1.0, 1.0);
256    pub const MAGENTA: Color = Color::new(1.0, 0.0, 1.0, 1.0);
257    pub const ORANGE:  Color = Color::new(1.0, 0.5, 0.0, 1.0);
258
259    // Shades
260    pub const GRAY:    Color = Color::new(0.5, 0.5, 0.5, 1.0);
261    pub const LIGHT_GRAY: Color = Color::new(0.75, 0.75, 0.75, 1.0);
262    pub const DARK_GRAY:  Color = Color::new(0.25, 0.25, 0.25, 1.0);
263
264    // Transparent
265    pub const TRANSPARENT: Color = Color::new(0.0, 0.0, 0.0, 0.0);
266
267    // Game-specific common colors
268    pub const SKY_BLUE:  Color = Color::new(0.53, 0.81, 0.92, 1.0);
269    pub const GOLD:      Color = Color::new(1.0, 0.84, 0.0, 1.0);
270    pub const CORAL:     Color = Color::new(1.0, 0.5, 0.31, 1.0);
271    pub const SALMON:    Color = Color::new(0.98, 0.5, 0.45, 1.0);
272    pub const LIME:      Color = Color::new(0.0, 1.0, 0.0, 1.0);
273    pub const PURPLE:    Color = Color::new(0.5, 0.0, 0.5, 1.0);
274    pub const PINK:      Color = Color::new(1.0, 0.75, 0.8, 1.0);
275    pub const TEAL:      Color = Color::new(0.0, 0.5, 0.5, 1.0);
276    pub const NAVY:      Color = Color::new(0.0, 0.0, 0.5, 1.0);
277    pub const MAROON:    Color = Color::new(0.5, 0.0, 0.0, 1.0);
278    pub const OLIVE:     Color = Color::new(0.5, 0.5, 0.0, 1.0);
279    pub const AQUA:      Color = Color::new(0.0, 1.0, 1.0, 1.0);
280    pub const INDIGO:    Color = Color::new(0.29, 0.0, 0.51, 1.0);
281    pub const VIOLET:    Color = Color::new(0.58, 0.0, 0.83, 1.0);
282    pub const CRIMSON:   Color = Color::new(0.86, 0.08, 0.24, 1.0);
283    pub const TURQUOISE: Color = Color::new(0.25, 0.88, 0.82, 1.0);
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[test]
291    fn test_from_hex() {
292        let c = Color::from_hex("#FF0080").unwrap();
293        assert!((c.r - 1.0).abs() < 1e-6);
294        assert!((c.g - 0.0).abs() < 1e-6);
295        assert!((c.b - 0.502).abs() < 0.01);
296        assert!((c.a - 1.0).abs() < 1e-6);
297    }
298
299    #[test]
300    fn test_lerp() {
301        let a = Color::BLACK;
302        let b = Color::WHITE;
303        let mid = a.lerp(b, 0.5);
304        assert!((mid.r - 0.5).abs() < 1e-6);
305        assert!((mid.g - 0.5).abs() < 1e-6);
306    }
307
308    #[test]
309    fn test_hsla_roundtrip() {
310        let original = Color::new(0.8, 0.3, 0.5, 0.9);
311        let (h, s, l, a) = original.to_hsla();
312        let roundtrip = Color::from_hsla(h, s, l, a);
313        assert!((roundtrip.r - original.r).abs() < 0.01);
314        assert!((roundtrip.g - original.g).abs() < 0.01);
315        assert!((roundtrip.b - original.b).abs() < 0.01);
316    }
317}