1use std::str::FromStr;
11use crate::math::FloatExt;
12
13#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
18#[repr(C)]
19pub struct Color {
20 pub r: f32,
22 pub g: f32,
24 pub b: f32,
26 pub a: f32,
28}
29
30impl Color {
31 #[inline]
33 pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
34 Self { r, g, b, a }
35 }
36
37 #[inline]
39 pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
40 Self { r, g, b, a: 1.0 }
41 }
42
43 #[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 #[inline]
56 pub fn from_rgb8(r: u8, g: u8, b: u8) -> Self {
57 Self::from_rgba8(r, g, b, 255)
58 }
59
60 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 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 #[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 #[inline]
107 pub fn luminance(self) -> f32 {
108 0.2126 * self.r + 0.7152 * self.g + 0.0722 * self.b
109 }
110
111 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 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 #[inline]
133 pub fn with_alpha(self, a: f32) -> Self {
134 Self { a, ..self }
135 }
136
137 #[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 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 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#[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
246impl 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 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 pub const TRANSPARENT: Color = Color::new(0.0, 0.0, 0.0, 0.0);
266
267 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}