1use crate::animation::Interpolable;
3
4#[derive(Debug, Clone, Copy, PartialEq, Default)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19#[must_use]
20pub struct Color {
21 pub r: f32,
23 pub g: f32,
25 pub b: f32,
27 pub a: f32,
29}
30
31impl Color {
32 pub const BLACK: Color = Color {
34 r: 0.0,
35 g: 0.0,
36 b: 0.0,
37 a: 1.0,
38 };
39
40 pub const WHITE: Color = Color {
42 r: 1.0,
43 g: 1.0,
44 b: 1.0,
45 a: 1.0,
46 };
47
48 pub const TRANSPARENT: Color = Color {
50 r: 0.0,
51 g: 0.0,
52 b: 0.0,
53 a: 0.0,
54 };
55
56 const fn new(r: f32, g: f32, b: f32, a: f32) -> Color {
61 debug_assert!(
62 r >= 0.0 && r <= 1.0,
63 "Red component must be in [0, 1] range."
64 );
65 debug_assert!(
66 g >= 0.0 && g <= 1.0,
67 "Green component must be in [0, 1] range."
68 );
69 debug_assert!(
70 b >= 0.0 && b <= 1.0,
71 "Blue component must be in [0, 1] range."
72 );
73
74 Self { r, g, b, a }
75 }
76
77 pub const fn from_rgb(r: f32, g: f32, b: f32) -> Self {
79 Self::from_rgba(r, g, b, 1.0f32)
80 }
81
82 pub const fn from_rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
84 Self::new(r, g, b, a)
85 }
86
87 pub const fn from_rgb8(r: u8, g: u8, b: u8) -> Self {
89 Self::from_rgba8(r, g, b, 1.0)
90 }
91
92 pub const fn from_rgba8(r: u8, g: u8, b: u8, a: f32) -> Self {
94 Self::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a)
95 }
96
97 pub const fn from_packed_rgb8(rgb: u32) -> Self {
99 Self::from_packed_rgba8(rgb, 1.0)
100 }
101
102 pub const fn from_packed_rgba8(rgb: u32, a: f32) -> Self {
105 let r = (rgb & 0xff0000) >> 16;
106 let g = (rgb & 0xff00) >> 8;
107 let b = rgb & 0xff;
108
109 Self::from_rgba8(r as u8, g as u8, b as u8, a)
110 }
111
112 pub fn from_linear_rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
114 fn gamma_component(u: f32) -> f32 {
117 if u < 0.0031308 {
118 12.92 * u
119 } else {
120 1.055 * u.powf(1.0 / 2.4) - 0.055
121 }
122 }
123
124 Self::new(
125 gamma_component(r),
126 gamma_component(g),
127 gamma_component(b),
128 a,
129 )
130 }
131
132 pub fn from_oklch(oklch: Oklch) -> Color {
134 let Oklch { l, c, h, a: alpha } = oklch;
136
137 let a = c * h.cos();
138 let b = c * h.sin();
139
140 let l_ = l + 0.39633778 * a + 0.21580376 * b;
142 let m_ = l - 0.105561346 * a - 0.06385417 * b;
143 let s_ = l - 0.08948418 * a - 1.2914855 * b;
144
145 let l = l_ * l_ * l_;
147 let m = m_ * m_ * m_;
148 let s = s_ * s_ * s_;
149
150 let r = 4.0767417 * l - 3.3077116 * m + 0.23096994 * s;
151 let g = -1.268438 * l + 2.6097574 * m - 0.34131938 * s;
152 let b = -0.0041960863 * l - 0.7034186 * m + 1.7076147 * s;
153
154 Color::from_linear_rgba(
155 r.clamp(0.0, 1.0),
156 g.clamp(0.0, 1.0),
157 b.clamp(0.0, 1.0),
158 alpha,
159 )
160 }
161
162 pub const fn invert(&mut self) {
164 self.r = 1.0f32 - self.r;
165 self.b = 1.0f32 - self.g;
166 self.g = 1.0f32 - self.b;
167 }
168
169 pub const fn inverse(self) -> Self {
171 Self::new(1.0f32 - self.r, 1.0f32 - self.g, 1.0f32 - self.b, self.a)
172 }
173
174 pub const fn scale_alpha(self, factor: f32) -> Self {
176 Self {
177 a: self.a * factor,
178 ..self
179 }
180 }
181
182 pub fn mix(self, b: Color, factor: f32) -> Color {
184 let b_amount = factor.clamp(0.0, 1.0);
185 let a_amount = 1.0 - b_amount;
186
187 let a_linear = self.into_linear().map(|c| c * a_amount);
188 let b_linear = b.into_linear().map(|c| c * b_amount);
189
190 Color::from_linear_rgba(
191 a_linear[0] + b_linear[0],
192 a_linear[1] + b_linear[1],
193 a_linear[2] + b_linear[2],
194 a_linear[3] + b_linear[3],
195 )
196 }
197
198 #[must_use]
201 pub fn relative_luminance(self) -> f32 {
202 let linear = self.into_linear();
203 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2]
204 }
205
206 #[must_use]
210 pub fn relative_contrast(self, b: Self) -> f32 {
211 let lum_a = self.relative_luminance();
212 let lum_b = b.relative_luminance();
213
214 (lum_a.max(lum_b) + 0.05) / (lum_a.min(lum_b) + 0.05)
215 }
216
217 #[must_use]
220 pub fn is_readable_on(self, background: Self) -> bool {
221 background.relative_contrast(self) >= 6.0
222 }
223
224 #[must_use]
226 pub const fn into_rgba8(self) -> [u8; 4] {
227 [
228 (self.r * 255.0).round() as u8,
229 (self.g * 255.0).round() as u8,
230 (self.b * 255.0).round() as u8,
231 (self.a * 255.0).round() as u8,
232 ]
233 }
234
235 #[must_use]
237 pub fn into_linear(self) -> [f32; 4] {
238 fn linear_component(u: f32) -> f32 {
241 if u < 0.04045 {
242 u / 12.92
243 } else {
244 ((u + 0.055) / 1.055).powf(2.4)
245 }
246 }
247
248 [
249 linear_component(self.r),
250 linear_component(self.g),
251 linear_component(self.b),
252 self.a,
253 ]
254 }
255
256 pub fn into_oklch(self) -> Oklch {
258 let [r, g, b, alpha] = self.into_linear();
260
261 let l = 0.41222146 * r + 0.53633255 * g + 0.051445995 * b;
263 let m = 0.2119035 * r + 0.6806995 * g + 0.10739696 * b;
264 let s = 0.08830246 * r + 0.28171885 * g + 0.6299787 * b;
265
266 let l_ = l.cbrt();
268 let m_ = m.cbrt();
269 let s_ = s.cbrt();
270
271 let l = 0.21045426 * l_ + 0.7936178 * m_ - 0.004072047 * s_;
273 let a = 1.9779985 * l_ - 2.4285922 * m_ + 0.4505937 * s_;
274 let b = 0.025904037 * l_ + 0.78277177 * m_ - 0.80867577 * s_;
275
276 let c = (a * a + b * b).sqrt();
278 let h = b.atan2(a); Oklch { l, c, h, a: alpha }
281 }
282}
283
284impl From<[f32; 3]> for Color {
285 fn from([r, g, b]: [f32; 3]) -> Self {
286 Color::new(r, g, b, 1.0)
287 }
288}
289
290impl From<[f32; 4]> for Color {
291 fn from([r, g, b, a]: [f32; 4]) -> Self {
292 Color::new(r, g, b, a)
293 }
294}
295
296impl From<Oklch> for Color {
297 fn from(oklch: Oklch) -> Self {
298 Self::from_oklch(oklch)
299 }
300}
301
302impl From<Color> for Oklch {
303 fn from(color: Color) -> Self {
304 color.into_oklch()
305 }
306}
307
308#[derive(Debug, thiserror::Error)]
312pub enum ParseError {
313 #[error(transparent)]
315 ParseIntError(#[from] std::num::ParseIntError),
316 #[error("expected hex string of length 3, 4, 6 or 8 excluding optional prefix '#', found {0}")]
318 InvalidLength(usize),
319}
320
321impl std::str::FromStr for Color {
322 type Err = ParseError;
323
324 fn from_str(s: &str) -> Result<Self, Self::Err> {
325 let hex = s.strip_prefix('#').unwrap_or(s);
326
327 let parse_channel = |from: usize, to: usize| -> Result<f32, std::num::ParseIntError> {
328 let num = usize::from_str_radix(&hex[from..=to], 16)? as f32 / 255.0;
329
330 Ok(if from == to { num + num * 16.0 } else { num })
332 };
333
334 let val = match hex.len() {
335 3 => Color::from_rgb(
336 parse_channel(0, 0)?,
337 parse_channel(1, 1)?,
338 parse_channel(2, 2)?,
339 ),
340 4 => Color::from_rgba(
341 parse_channel(0, 0)?,
342 parse_channel(1, 1)?,
343 parse_channel(2, 2)?,
344 parse_channel(3, 3)?,
345 ),
346 6 => Color::from_rgb(
347 parse_channel(0, 1)?,
348 parse_channel(2, 3)?,
349 parse_channel(4, 5)?,
350 ),
351 8 => Color::from_rgba(
352 parse_channel(0, 1)?,
353 parse_channel(2, 3)?,
354 parse_channel(4, 5)?,
355 parse_channel(6, 7)?,
356 ),
357 _ => return Err(ParseError::InvalidLength(hex.len())),
358 };
359
360 Ok(val)
361 }
362}
363
364impl std::fmt::Display for Color {
365 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
366 let [r, g, b, a] = self.into_rgba8();
367
368 if self.a == 1.0 {
369 return write!(f, "#{r:02x}{g:02x}{b:02x}");
370 }
371
372 write!(f, "#{r:02x}{g:02x}{b:02x}{a:02x}")
373 }
374}
375
376impl Interpolable for Color {
377 fn interpolated(&self, other: Self, ratio: f32) -> Self {
379 self.mix(other, ratio)
380 }
381}
382
383pub struct Oklch {
386 pub l: f32,
388 pub c: f32,
390 pub h: f32,
392 pub a: f32,
394}
395
396#[macro_export]
409macro_rules! color {
410 ($r:expr, $g:expr, $b:expr) => {
411 $crate::Color::from_rgb8($r, $g, $b)
412 };
413 ($r:expr, $g:expr, $b:expr, $a:expr) => {{ $crate::Color::from_rgba8($r, $g, $b, $a) }};
414 ($hex:literal) => {{ $crate::color!($hex, 1.0) }};
415 ($hex:literal, $a:expr) => {{
416 let mut hex = $hex as u32;
417
418 if stringify!($hex).len() == 5 {
420 let r = hex & 0xF00;
421 let g = hex & 0xF0;
422 let b = hex & 0xF;
423
424 hex = (r << 12) | (r << 8) | (g << 8) | (g << 4) | (b << 4) | b;
425 }
426
427 debug_assert!(hex <= 0xffffff, "color! value must not exceed 0xffffff");
428
429 $crate::Color::from_packed_rgba8(hex, $a)
430 }};
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn parse() {
439 let tests = [
440 ("#ff0000", [255, 0, 0, 255], "#ff0000"),
441 ("00ff0080", [0, 255, 0, 128], "#00ff0080"),
442 ("#F80", [255, 136, 0, 255], "#ff8800"),
443 ("#00f1", [0, 0, 255, 17], "#0000ff11"),
444 ("#00ff", [0, 0, 255, 255], "#0000ff"),
445 ];
446
447 for (arg, expected_rgba8, expected_str) in tests {
448 let color = arg.parse::<Color>().expect("color must parse");
449
450 assert_eq!(color.into_rgba8(), expected_rgba8);
451 assert_eq!(color.to_string(), expected_str);
452 }
453
454 assert!("invalid".parse::<Color>().is_err());
455 }
456
457 const SHORTHAND: Color = color!(0x123);
458
459 #[test]
460 fn shorthand_notation() {
461 assert_eq!(SHORTHAND, Color::from_rgb8(0x11, 0x22, 0x33));
462 }
463}