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