1use palette::rgb::{Srgb, Srgba};
2
3#[derive(Debug, Clone, Copy, PartialEq, Default)]
5pub struct Color {
6 pub r: f32,
8 pub g: f32,
10 pub b: f32,
12 pub a: f32,
14}
15
16impl Color {
17 pub const BLACK: Color = Color {
19 r: 0.0,
20 g: 0.0,
21 b: 0.0,
22 a: 1.0,
23 };
24
25 pub const WHITE: Color = Color {
27 r: 1.0,
28 g: 1.0,
29 b: 1.0,
30 a: 1.0,
31 };
32
33 pub const TRANSPARENT: Color = Color {
35 r: 0.0,
36 g: 0.0,
37 b: 0.0,
38 a: 0.0,
39 };
40
41 pub fn new(r: f32, g: f32, b: f32, a: f32) -> Color {
46 debug_assert!(
47 (0.0..=1.0).contains(&r),
48 "Red component must be on [0, 1]"
49 );
50 debug_assert!(
51 (0.0..=1.0).contains(&g),
52 "Green component must be on [0, 1]"
53 );
54 debug_assert!(
55 (0.0..=1.0).contains(&b),
56 "Blue component must be on [0, 1]"
57 );
58 debug_assert!(
59 (0.0..=1.0).contains(&a),
60 "Alpha component must be on [0, 1]"
61 );
62
63 Color { r, g, b, a }
64 }
65
66 pub const fn from_rgb(r: f32, g: f32, b: f32) -> Color {
68 Color::from_rgba(r, g, b, 1.0f32)
69 }
70
71 pub const fn from_rgba(r: f32, g: f32, b: f32, a: f32) -> Color {
73 Color { r, g, b, a }
74 }
75
76 pub fn from_rgb8(r: u8, g: u8, b: u8) -> Color {
78 Color::from_rgba8(r, g, b, 1.0)
79 }
80
81 pub fn from_rgba8(r: u8, g: u8, b: u8, a: f32) -> Color {
83 Color {
84 r: f32::from(r) / 255.0,
85 g: f32::from(g) / 255.0,
86 b: f32::from(b) / 255.0,
87 a,
88 }
89 }
90
91 pub fn from_linear_rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
93 fn gamma_component(u: f32) -> f32 {
96 if u < 0.0031308 {
97 12.92 * u
98 } else {
99 1.055 * u.powf(1.0 / 2.4) - 0.055
100 }
101 }
102
103 Self {
104 r: gamma_component(r),
105 g: gamma_component(g),
106 b: gamma_component(b),
107 a,
108 }
109 }
110
111 pub fn parse(s: &str) -> Option<Color> {
121 let hex = s.strip_prefix('#').unwrap_or(s);
122
123 let parse_channel = |from: usize, to: usize| {
124 let num =
125 usize::from_str_radix(&hex[from..=to], 16).ok()? as f32 / 255.0;
126
127 Some(if from == to { num + num * 16.0 } else { num })
129 };
130
131 Some(match hex.len() {
132 3 => Color::from_rgb(
133 parse_channel(0, 0)?,
134 parse_channel(1, 1)?,
135 parse_channel(2, 2)?,
136 ),
137 4 => Color::from_rgba(
138 parse_channel(0, 0)?,
139 parse_channel(1, 1)?,
140 parse_channel(2, 2)?,
141 parse_channel(3, 3)?,
142 ),
143 6 => Color::from_rgb(
144 parse_channel(0, 1)?,
145 parse_channel(2, 3)?,
146 parse_channel(4, 5)?,
147 ),
148 8 => Color::from_rgba(
149 parse_channel(0, 1)?,
150 parse_channel(2, 3)?,
151 parse_channel(4, 5)?,
152 parse_channel(6, 7)?,
153 ),
154 _ => None?,
155 })
156 }
157
158 #[must_use]
160 pub fn into_rgba8(self) -> [u8; 4] {
161 [
162 (self.r * 255.0).round() as u8,
163 (self.g * 255.0).round() as u8,
164 (self.b * 255.0).round() as u8,
165 (self.a * 255.0).round() as u8,
166 ]
167 }
168
169 pub fn into_linear(self) -> [f32; 4] {
171 fn linear_component(u: f32) -> f32 {
174 if u < 0.04045 {
175 u / 12.92
176 } else {
177 ((u + 0.055) / 1.055).powf(2.4)
178 }
179 }
180
181 [
182 linear_component(self.r),
183 linear_component(self.g),
184 linear_component(self.b),
185 self.a,
186 ]
187 }
188
189 pub fn invert(&mut self) {
191 self.r = 1.0f32 - self.r;
192 self.b = 1.0f32 - self.g;
193 self.g = 1.0f32 - self.b;
194 }
195
196 pub fn inverse(self) -> Color {
198 Color::new(1.0f32 - self.r, 1.0f32 - self.g, 1.0f32 - self.b, self.a)
199 }
200
201 pub fn scale_alpha(self, factor: f32) -> Color {
203 Self {
204 a: self.a * factor,
205 ..self
206 }
207 }
208}
209
210impl From<[f32; 3]> for Color {
211 fn from([r, g, b]: [f32; 3]) -> Self {
212 Color::new(r, g, b, 1.0)
213 }
214}
215
216impl From<[f32; 4]> for Color {
217 fn from([r, g, b, a]: [f32; 4]) -> Self {
218 Color::new(r, g, b, a)
219 }
220}
221
222#[macro_export]
235macro_rules! color {
236 ($r:expr, $g:expr, $b:expr) => {
237 $crate::color!($r, $g, $b, 1.0)
238 };
239 ($r:expr, $g:expr, $b:expr, $a:expr) => {{
240 let r = $r as f32 / 255.0;
241 let g = $g as f32 / 255.0;
242 let b = $b as f32 / 255.0;
243
244 #[allow(clippy::manual_range_contains)]
245 {
246 debug_assert!(
247 r >= 0.0 && r <= 1.0,
248 "R channel must be in [0, 255] range."
249 );
250 debug_assert!(
251 g >= 0.0 && g <= 1.0,
252 "G channel must be in [0, 255] range."
253 );
254 debug_assert!(
255 b >= 0.0 && b <= 1.0,
256 "B channel must be in [0, 255] range."
257 );
258 }
259
260 $crate::Color { r, g, b, a: $a }
261 }};
262 ($hex:expr) => {{
263 $crate::color!($hex, 1.0)
264 }};
265 ($hex:expr, $a:expr) => {{
266 let hex = $hex as u32;
267
268 debug_assert!(hex <= 0xffffff, "color! value must not exceed 0xffffff");
269
270 let r = (hex & 0xff0000) >> 16;
271 let g = (hex & 0xff00) >> 8;
272 let b = (hex & 0xff);
273
274 $crate::color!(r, g, b, $a)
275 }};
276}
277
278impl From<Srgba> for Color {
280 fn from(rgba: Srgba) -> Self {
281 Color::new(rgba.red, rgba.green, rgba.blue, rgba.alpha)
282 }
283}
284
285impl From<Color> for Srgba {
287 fn from(c: Color) -> Self {
288 Srgba::new(c.r, c.g, c.b, c.a)
289 }
290}
291
292impl From<Srgb> for Color {
294 fn from(rgb: Srgb) -> Self {
295 Color::new(rgb.red, rgb.green, rgb.blue, 1.0)
296 }
297}
298
299impl From<Color> for Srgb {
301 fn from(c: Color) -> Self {
302 Srgb::new(c.r, c.g, c.b)
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use palette::blend::Blend;
310
311 #[test]
312 fn srgba_traits() {
313 let c = Color::from_rgb(0.5, 0.4, 0.3);
314 let s: Srgba = c.into();
316 let r: Color = s.into();
317 assert_eq!(c, r);
318 }
319
320 #[test]
321 fn color_manipulation() {
322 use approx::assert_relative_eq;
323
324 let c1 = Color::from_rgb(0.5, 0.4, 0.3);
325 let c2 = Color::from_rgb(0.2, 0.5, 0.3);
326
327 let l1 = Srgba::from(c1).into_linear();
329 let l2 = Srgba::from(c2).into_linear();
330
331 let lighter = l1.lighten(l2);
333
334 let result: Color = Srgba::from_linear(lighter).into();
336
337 assert_relative_eq!(result.r, 0.5);
338 assert_relative_eq!(result.g, 0.5);
339 assert_relative_eq!(result.b, 0.3);
340 assert_relative_eq!(result.a, 1.0);
341 }
342
343 #[test]
344 fn parse() {
345 let tests = [
346 ("#ff0000", [255, 0, 0, 255]),
347 ("00ff0080", [0, 255, 0, 128]),
348 ("#F80", [255, 136, 0, 255]),
349 ("#00f1", [0, 0, 255, 17]),
350 ];
351
352 for (arg, expected) in tests {
353 assert_eq!(
354 Color::parse(arg).expect("color must parse").into_rgba8(),
355 expected
356 );
357 }
358
359 assert!(Color::parse("invalid").is_none());
360 }
361}