1use oxideav_core::Rgba;
2
3#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
8pub struct Color {
9 pub r: u8,
10 pub g: u8,
11 pub b: u8,
12 pub a: u8,
13}
14
15impl Color {
16 pub const TRANSPARENT: Self = Self {
17 r: 0,
18 g: 0,
19 b: 0,
20 a: 0,
21 };
22 pub const BLACK: Self = Self::rgb(0, 0, 0);
23 pub const WHITE: Self = Self::rgb(255, 255, 255);
24
25 #[inline]
26 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
27 Self { r, g, b, a: 255 }
28 }
29
30 #[inline]
31 pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
32 Self { r, g, b, a }
33 }
34
35 pub fn from_hex(s: &str) -> Option<Self> {
38 let s = s.strip_prefix('#').unwrap_or(s);
39 let h = |i: usize| u8::from_str_radix(&s[i..i + 2], 16).ok();
40 let n = |i: usize| u8::from_str_radix(&s[i..i + 1], 16).ok().map(|v| v * 17);
41 match s.len() {
42 3 => Some(Self::rgb(n(0)?, n(1)?, n(2)?)),
43 4 => Some(Self::rgba(n(0)?, n(1)?, n(2)?, n(3)?)),
44 6 => Some(Self::rgb(h(0)?, h(2)?, h(4)?)),
45 8 => Some(Self::rgba(h(0)?, h(2)?, h(4)?, h(6)?)),
46 _ => None,
47 }
48 }
49
50 #[inline]
52 pub const fn with_alpha(self, a: u8) -> Self {
53 Self { a, ..self }
54 }
55
56 pub fn mix(self, other: Color, t: f64) -> Color {
59 let t = t.clamp(0.0, 1.0);
60 let lerp = |a: u8, b: u8| (a as f64 + (b as f64 - a as f64) * t).round() as u8;
61 Color {
62 r: lerp(self.r, other.r),
63 g: lerp(self.g, other.g),
64 b: lerp(self.b, other.b),
65 a: lerp(self.a, other.a),
66 }
67 }
68
69 pub fn lighten(self, amount: f64) -> Color {
71 self.mix(Color::rgb(255, 255, 255).with_alpha(self.a), amount)
72 }
73
74 pub fn darken(self, amount: f64) -> Color {
76 self.mix(Color::rgb(0, 0, 0).with_alpha(self.a), amount)
77 }
78
79 pub fn luminance(self) -> f64 {
82 (0.2126 * self.r as f64 + 0.7152 * self.g as f64 + 0.0722 * self.b as f64) / 255.0
83 }
84
85 pub fn on_color(self) -> Color {
87 if self.luminance() > 0.5 {
88 Color::BLACK
89 } else {
90 Color::WHITE
91 }
92 }
93
94 #[inline]
96 pub const fn to_oxideav(self) -> Rgba {
97 Rgba {
98 r: self.r,
99 g: self.g,
100 b: self.b,
101 a: self.a,
102 }
103 }
104}
105
106impl From<Color> for Rgba {
107 #[inline]
108 fn from(c: Color) -> Rgba {
109 c.to_oxideav()
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn hex_parsing() {
119 assert_eq!(Color::from_hex("#ff0000"), Some(Color::rgb(255, 0, 0)));
120 assert_eq!(Color::from_hex("0f0"), Some(Color::rgb(0, 255, 0)));
121 assert_eq!(
122 Color::from_hex("#11223344"),
123 Some(Color::rgba(0x11, 0x22, 0x33, 0x44))
124 );
125 assert_eq!(Color::from_hex("#abc"), Some(Color::rgb(0xaa, 0xbb, 0xcc)));
126 assert_eq!(Color::from_hex("zzz"), None);
127 assert_eq!(Color::from_hex("#12345"), None);
128 }
129
130 #[test]
131 fn mix_lighten_darken() {
132 let c = Color::rgb(100, 100, 100);
133 assert_eq!(
134 c.mix(Color::rgb(200, 200, 200), 0.5),
135 Color::rgb(150, 150, 150)
136 );
137 assert_eq!(c.mix(Color::rgb(200, 200, 200), 0.0), c);
138 assert_eq!(
139 Color::rgb(100, 100, 100).lighten(0.5),
140 Color::rgb(178, 178, 178)
141 );
142 assert_eq!(
143 Color::rgb(100, 100, 100).darken(0.5),
144 Color::rgb(50, 50, 50)
145 );
146 }
147
148 #[test]
149 fn on_color_contrasts() {
150 assert_eq!(Color::WHITE.on_color(), Color::BLACK);
151 assert_eq!(Color::rgb(20, 20, 20).on_color(), Color::WHITE);
152 }
153}