1#[derive(Debug, Clone, Copy, PartialEq, Default)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct Color {
17 pub r: f32,
19 pub g: f32,
21 pub b: f32,
23 pub a: f32,
25}
26
27impl Color {
28 pub const BLACK: Color = Color {
30 r: 0.0,
31 g: 0.0,
32 b: 0.0,
33 a: 1.0,
34 };
35
36 pub const WHITE: Color = Color {
38 r: 1.0,
39 g: 1.0,
40 b: 1.0,
41 a: 1.0,
42 };
43
44 pub const TRANSPARENT: Color = Color {
46 r: 0.0,
47 g: 0.0,
48 b: 0.0,
49 a: 0.0,
50 };
51
52 const fn new(r: f32, g: f32, b: f32, a: f32) -> Color {
57 debug_assert!(
58 r >= 0.0 && r <= 1.0,
59 "Red component must be in [0, 1] range."
60 );
61 debug_assert!(
62 g >= 0.0 && g <= 1.0,
63 "Green component must be in [0, 1] range."
64 );
65 debug_assert!(
66 b >= 0.0 && b <= 1.0,
67 "Blue component must be in [0, 1] range."
68 );
69
70 Color { r, g, b, a }
71 }
72
73 pub const fn from_rgb(r: f32, g: f32, b: f32) -> Color {
75 Color::from_rgba(r, g, b, 1.0f32)
76 }
77
78 pub const fn from_rgba(r: f32, g: f32, b: f32, a: f32) -> Color {
80 Color::new(r, g, b, a)
81 }
82
83 pub const fn from_rgb8(r: u8, g: u8, b: u8) -> Color {
85 Color::from_rgba8(r, g, b, 1.0)
86 }
87
88 pub const fn from_rgba8(r: u8, g: u8, b: u8, a: f32) -> Color {
90 Color::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a)
91 }
92
93 pub fn from_linear_rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
95 fn gamma_component(u: f32) -> f32 {
98 if u < 0.0031308 {
99 12.92 * u
100 } else {
101 1.055 * u.powf(1.0 / 2.4) - 0.055
102 }
103 }
104
105 Self::new(
106 gamma_component(r),
107 gamma_component(g),
108 gamma_component(b),
109 a,
110 )
111 }
112
113 #[must_use]
115 pub fn into_rgba8(self) -> [u8; 4] {
116 [
117 (self.r * 255.0).round() as u8,
118 (self.g * 255.0).round() as u8,
119 (self.b * 255.0).round() as u8,
120 (self.a * 255.0).round() as u8,
121 ]
122 }
123
124 pub fn into_linear(self) -> [f32; 4] {
126 fn linear_component(u: f32) -> f32 {
129 if u < 0.04045 {
130 u / 12.92
131 } else {
132 ((u + 0.055) / 1.055).powf(2.4)
133 }
134 }
135
136 [
137 linear_component(self.r),
138 linear_component(self.g),
139 linear_component(self.b),
140 self.a,
141 ]
142 }
143
144 pub fn invert(&mut self) {
146 self.r = 1.0f32 - self.r;
147 self.b = 1.0f32 - self.g;
148 self.g = 1.0f32 - self.b;
149 }
150
151 pub fn inverse(self) -> Color {
153 Color::new(1.0f32 - self.r, 1.0f32 - self.g, 1.0f32 - self.b, self.a)
154 }
155
156 pub fn scale_alpha(self, factor: f32) -> Color {
158 Self {
159 a: self.a * factor,
160 ..self
161 }
162 }
163
164 pub fn relative_luminance(self) -> f32 {
167 let linear = self.into_linear();
168 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2]
169 }
170
171 pub fn relative_contrast(self, b: Color) -> f32 {
175 let lum_a = self.relative_luminance();
176 let lum_b = b.relative_luminance();
177
178 (lum_a.max(lum_b) + 0.05) / (lum_a.min(lum_b) + 0.05)
179 }
180
181 pub fn is_readable_on(self, background: Color) -> bool {
184 background.relative_contrast(self) >= 6.0
185 }
186}
187
188impl From<[f32; 3]> for Color {
189 fn from([r, g, b]: [f32; 3]) -> Self {
190 Color::new(r, g, b, 1.0)
191 }
192}
193
194impl From<[f32; 4]> for Color {
195 fn from([r, g, b, a]: [f32; 4]) -> Self {
196 Color::new(r, g, b, a)
197 }
198}
199
200#[derive(Debug, thiserror::Error)]
204pub enum ParseError {
205 #[error(transparent)]
207 ParseIntError(#[from] std::num::ParseIntError),
208 #[error(
210 "expected hex string of length 3, 4, 6 or 8 excluding optional prefix '#', found {0}"
211 )]
212 InvalidLength(usize),
213}
214
215impl std::str::FromStr for Color {
216 type Err = ParseError;
217
218 fn from_str(s: &str) -> Result<Self, Self::Err> {
219 let hex = s.strip_prefix('#').unwrap_or(s);
220
221 let parse_channel =
222 |from: usize, to: usize| -> Result<f32, std::num::ParseIntError> {
223 let num =
224 usize::from_str_radix(&hex[from..=to], 16)? as f32 / 255.0;
225
226 Ok(if from == to { num + num * 16.0 } else { num })
228 };
229
230 let val = match hex.len() {
231 3 => Color::from_rgb(
232 parse_channel(0, 0)?,
233 parse_channel(1, 1)?,
234 parse_channel(2, 2)?,
235 ),
236 4 => Color::from_rgba(
237 parse_channel(0, 0)?,
238 parse_channel(1, 1)?,
239 parse_channel(2, 2)?,
240 parse_channel(3, 3)?,
241 ),
242 6 => Color::from_rgb(
243 parse_channel(0, 1)?,
244 parse_channel(2, 3)?,
245 parse_channel(4, 5)?,
246 ),
247 8 => Color::from_rgba(
248 parse_channel(0, 1)?,
249 parse_channel(2, 3)?,
250 parse_channel(4, 5)?,
251 parse_channel(6, 7)?,
252 ),
253 _ => return Err(ParseError::InvalidLength(hex.len())),
254 };
255
256 Ok(val)
257 }
258}
259
260impl std::fmt::Display for Color {
261 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262 let [r, g, b, a] = self.into_rgba8();
263
264 if self.a == 1.0 {
265 return write!(f, "#{r:02x}{g:02x}{b:02x}");
266 }
267
268 write!(f, "#{r:02x}{g:02x}{b:02x}{a:02x}")
269 }
270}
271
272#[macro_export]
285macro_rules! color {
286 ($r:expr, $g:expr, $b:expr) => {
287 $crate::Color::from_rgb8($r, $g, $b)
288 };
289 ($r:expr, $g:expr, $b:expr, $a:expr) => {{ $crate::Color::from_rgba8($r, $g, $b, $a) }};
290 ($hex:literal) => {{ $crate::color!($hex, 1.0) }};
291 ($hex:literal, $a:expr) => {{
292 let mut hex = $hex as u32;
293
294 if stringify!($hex).len() == 5 {
296 let r = hex & 0xF00;
297 let g = hex & 0xF0;
298 let b = hex & 0xF;
299
300 hex = (r << 12) | (r << 8) | (g << 8) | (g << 4) | (b << 4) | b;
301 }
302
303 debug_assert!(hex <= 0xffffff, "color! value must not exceed 0xffffff");
304
305 let r = (hex & 0xff0000) >> 16;
306 let g = (hex & 0xff00) >> 8;
307 let b = (hex & 0xff);
308
309 $crate::color!(r as u8, g as u8, b as u8, $a)
310 }};
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn parse() {
319 let tests = [
320 ("#ff0000", [255, 0, 0, 255], "#ff0000"),
321 ("00ff0080", [0, 255, 0, 128], "#00ff0080"),
322 ("#F80", [255, 136, 0, 255], "#ff8800"),
323 ("#00f1", [0, 0, 255, 17], "#0000ff11"),
324 ("#00ff", [0, 0, 255, 255], "#0000ff"),
325 ];
326
327 for (arg, expected_rgba8, expected_str) in tests {
328 let color = arg.parse::<Color>().expect("color must parse");
329
330 assert_eq!(color.into_rgba8(), expected_rgba8);
331 assert_eq!(color.to_string(), expected_str);
332 }
333
334 assert!("invalid".parse::<Color>().is_err());
335 }
336
337 const SHORTHAND: Color = color!(0x123);
338
339 #[test]
340 fn shorthand_notation() {
341 assert_eq!(SHORTHAND, Color::from_rgb8(0x11, 0x22, 0x33));
342 }
343}