1use anyhow::bail;
2use serde::de::{self, Deserialize, Deserializer, Visitor};
3use std::fmt;
4
5/// Convert an RGB hex color code number to a color type
6pub fn rgb(hex: u32) -> Rgba {
7 let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
8 let g = ((hex >> 8) & 0xFF) as f32 / 255.0;
9 let b = (hex & 0xFF) as f32 / 255.0;
10 Rgba { r, g, b, a: 1.0 }
11}
12
13/// Convert an RGBA hex color code number to [`Rgba`]
14pub fn rgba(hex: u32) -> Rgba {
15 let r = ((hex >> 24) & 0xFF) as f32 / 255.0;
16 let g = ((hex >> 16) & 0xFF) as f32 / 255.0;
17 let b = ((hex >> 8) & 0xFF) as f32 / 255.0;
18 let a = (hex & 0xFF) as f32 / 255.0;
19 Rgba { r, g, b, a }
20}
21
22/// An RGBA color
23#[derive(PartialEq, Clone, Copy, Default)]
24pub struct Rgba {
25 /// The red component of the color, in the range 0.0 to 1.0
26 pub r: f32,
27 /// The green component of the color, in the range 0.0 to 1.0
28 pub g: f32,
29 /// The blue component of the color, in the range 0.0 to 1.0
30 pub b: f32,
31 /// The alpha component of the color, in the range 0.0 to 1.0
32 pub a: f32,
33}
34
35impl fmt::Debug for Rgba {
36 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
37 write!(f, "rgba({:#010x})", u32::from(*self))
38 }
39}
40
41impl Rgba {
42 /// Create a new [`Rgba`] color by blending this and another color together
43 pub fn blend(&self, other: Rgba) -> Self {
44 if other.a >= 1.0 {
45 other
46 } else if other.a <= 0.0 {
47 return *self;
48 } else {
49 return Rgba {
50 r: (self.r * (1.0 - other.a)) + (other.r * other.a),
51 g: (self.g * (1.0 - other.a)) + (other.g * other.a),
52 b: (self.b * (1.0 - other.a)) + (other.b * other.a),
53 a: self.a,
54 };
55 }
56 }
57}
58
59impl From<Rgba> for u32 {
60 fn from(rgba: Rgba) -> Self {
61 let r = (rgba.r * 255.0) as u32;
62 let g = (rgba.g * 255.0) as u32;
63 let b = (rgba.b * 255.0) as u32;
64 let a = (rgba.a * 255.0) as u32;
65 (r << 24) | (g << 16) | (b << 8) | a
66 }
67}
68
69struct RgbaVisitor;
70
71impl<'de> Visitor<'de> for RgbaVisitor {
72 type Value = Rgba;
73
74 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
75 formatter.write_str("a string in the format #rrggbb or #rrggbbaa")
76 }
77
78 fn visit_str<E: de::Error>(self, value: &str) -> Result<Rgba, E> {
79 Rgba::try_from(value).map_err(E::custom)
80 }
81}
82
83impl<'de> Deserialize<'de> for Rgba {
84 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
85 deserializer.deserialize_str(RgbaVisitor)
86 }
87}
88
89impl From<Hsla> for Rgba {
90 fn from(color: Hsla) -> Self {
91 let h = color.h;
92 let s = color.s;
93 let l = color.l;
94
95 let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
96 let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs());
97 let m = l - c / 2.0;
98 let cm = c + m;
99 let xm = x + m;
100
101 let (r, g, b) = match (h * 6.0).floor() as i32 {
102 0 | 6 => (cm, xm, m),
103 1 => (xm, cm, m),
104 2 => (m, cm, xm),
105 3 => (m, xm, cm),
106 4 => (xm, m, cm),
107 _ => (cm, m, xm),
108 };
109
110 Rgba {
111 r,
112 g,
113 b,
114 a: color.a,
115 }
116 }
117}
118
119impl TryFrom<&'_ str> for Rgba {
120 type Error = anyhow::Error;
121
122 fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
123 const RGB: usize = "rgb".len();
124 const RGBA: usize = "rgba".len();
125 const RRGGBB: usize = "rrggbb".len();
126 const RRGGBBAA: usize = "rrggbbaa".len();
127
128 const EXPECTED_FORMATS: &str = "Expected #rgb, #rgba, #rrggbb, or #rrggbbaa";
129
130 let Some(("", hex)) = value.trim().split_once('#') else {
131 bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}");
132 };
133
134 let (r, g, b, a) = match hex.len() {
135 RGB | RGBA => {
136 let r = u8::from_str_radix(&hex[0..1], 16)?;
137 let g = u8::from_str_radix(&hex[1..2], 16)?;
138 let b = u8::from_str_radix(&hex[2..3], 16)?;
139 let a = if hex.len() == RGBA {
140 u8::from_str_radix(&hex[3..4], 16)?
141 } else {
142 0xf
143 };
144
145 /// Duplicates a given hex digit.
146 /// E.g., `0xf` -> `0xff`.
147 const fn duplicate(value: u8) -> u8 {
148 value << 4 | value
149 }
150
151 (duplicate(r), duplicate(g), duplicate(b), duplicate(a))
152 }
153 RRGGBB | RRGGBBAA => {
154 let r = u8::from_str_radix(&hex[0..2], 16)?;
155 let g = u8::from_str_radix(&hex[2..4], 16)?;
156 let b = u8::from_str_radix(&hex[4..6], 16)?;
157 let a = if hex.len() == RRGGBBAA {
158 u8::from_str_radix(&hex[6..8], 16)?
159 } else {
160 0xff
161 };
162 (r, g, b, a)
163 }
164 _ => bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"),
165 };
166
167 Ok(Rgba {
168 r: r as f32 / 255.,
169 g: g as f32 / 255.,
170 b: b as f32 / 255.,
171 a: a as f32 / 255.,
172 })
173 }
174}
175
176/// An HSLA color
177#[derive(Default, Copy, Clone, Debug)]
178#[repr(C)]
179pub struct Hsla {
180 /// Hue, in a range from 0 to 1
181 pub h: f32,
182
183 /// Saturation, in a range from 0 to 1
184 pub s: f32,
185
186 /// Lightness, in a range from 0 to 1
187 pub l: f32,
188
189 /// Alpha, in a range from 0 to 1
190 pub a: f32,
191}
192
193impl PartialEq for Hsla {
194 fn eq(&self, other: &Self) -> bool {
195 self.h
196 .total_cmp(&other.h)
197 .then(self.s.total_cmp(&other.s))
198 .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a)))
199 .is_eq()
200 }
201}
202
203impl PartialOrd for Hsla {
204 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
205 Some(self.cmp(other))
206 }
207}
208
209impl Ord for Hsla {
210 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
211 self.h
212 .total_cmp(&other.h)
213 .then(self.s.total_cmp(&other.s))
214 .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a)))
215 }
216}
217
218impl Eq for Hsla {}
219
220/// Construct an [`Hsla`] object from plain values
221pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
222 Hsla {
223 h: h.clamp(0., 1.),
224 s: s.clamp(0., 1.),
225 l: l.clamp(0., 1.),
226 a: a.clamp(0., 1.),
227 }
228}
229
230/// Pure black in [`Hsla`]
231pub fn black() -> Hsla {
232 Hsla {
233 h: 0.,
234 s: 0.,
235 l: 0.,
236 a: 1.,
237 }
238}
239
240/// Transparent black in [`Hsla`]
241pub fn transparent_black() -> Hsla {
242 Hsla {
243 h: 0.,
244 s: 0.,
245 l: 0.,
246 a: 0.,
247 }
248}
249
250/// Opaque grey in [`Hsla`], values will be clamped to the range [0, 1]
251pub fn opaque_grey(lightness: f32, opacity: f32) -> Hsla {
252 Hsla {
253 h: 0.,
254 s: 0.,
255 l: lightness.clamp(0., 1.),
256 a: opacity.clamp(0., 1.),
257 }
258}
259
260/// Pure white in [`Hsla`]
261pub fn white() -> Hsla {
262 Hsla {
263 h: 0.,
264 s: 0.,
265 l: 1.,
266 a: 1.,
267 }
268}
269
270/// The color red in [`Hsla`]
271pub fn red() -> Hsla {
272 Hsla {
273 h: 0.,
274 s: 1.,
275 l: 0.5,
276 a: 1.,
277 }
278}
279
280/// The color blue in [`Hsla`]
281pub fn blue() -> Hsla {
282 Hsla {
283 h: 0.6,
284 s: 1.,
285 l: 0.5,
286 a: 1.,
287 }
288}
289
290/// The color green in [`Hsla`]
291pub fn green() -> Hsla {
292 Hsla {
293 h: 0.33,
294 s: 1.,
295 l: 0.5,
296 a: 1.,
297 }
298}
299
300/// The color yellow in [`Hsla`]
301pub fn yellow() -> Hsla {
302 Hsla {
303 h: 0.16,
304 s: 1.,
305 l: 0.5,
306 a: 1.,
307 }
308}
309
310impl Hsla {
311 /// Converts this HSLA color to an RGBA color.
312 pub fn to_rgb(self) -> Rgba {
313 self.into()
314 }
315
316 /// The color red
317 pub fn red() -> Self {
318 red()
319 }
320
321 /// The color green
322 pub fn green() -> Self {
323 green()
324 }
325
326 /// The color blue
327 pub fn blue() -> Self {
328 blue()
329 }
330
331 /// The color black
332 pub fn black() -> Self {
333 black()
334 }
335
336 /// The color white
337 pub fn white() -> Self {
338 white()
339 }
340
341 /// The color transparent black
342 pub fn transparent_black() -> Self {
343 transparent_black()
344 }
345
346 /// Returns true if the HSLA color is fully transparent, false otherwise.
347 pub fn is_transparent(&self) -> bool {
348 self.a == 0.0
349 }
350
351 /// Blends `other` on top of `self` based on `other`'s alpha value. The resulting color is a combination of `self`'s and `other`'s colors.
352 ///
353 /// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color.
354 /// If `other`'s alpha value is 0.0 or less, `other` color is fully transparent, thus `self` is returned as the output color.
355 /// Else, the output color is calculated as a blend of `self` and `other` based on their weighted alpha values.
356 ///
357 /// Assumptions:
358 /// - Alpha values are contained in the range [0, 1], with 1 as fully opaque and 0 as fully transparent.
359 /// - The relative contributions of `self` and `other` is based on `self`'s alpha value (`self.a`) and `other`'s alpha value (`other.a`), `self` contributing `self.a * (1.0 - other.a)` and `other` contributing its own alpha value.
360 /// - RGB color components are contained in the range [0, 1].
361 /// - If `self` and `other` colors are out of the valid range, the blend operation's output and behavior is undefined.
362 pub fn blend(self, other: Hsla) -> Hsla {
363 let alpha = other.a;
364
365 if alpha >= 1.0 {
366 other
367 } else if alpha <= 0.0 {
368 return self;
369 } else {
370 let converted_self = Rgba::from(self);
371 let converted_other = Rgba::from(other);
372 let blended_rgb = converted_self.blend(converted_other);
373 return Hsla::from(blended_rgb);
374 }
375 }
376
377 /// Returns a new HSLA color with the same hue, and lightness, but with no saturation.
378 pub fn grayscale(&self) -> Self {
379 Hsla {
380 h: self.h,
381 s: 0.,
382 l: self.l,
383 a: self.a,
384 }
385 }
386
387 /// Fade out the color by a given factor. This factor should be between 0.0 and 1.0.
388 /// Where 0.0 will leave the color unchanged, and 1.0 will completely fade out the color.
389 pub fn fade_out(&mut self, factor: f32) {
390 self.a *= 1.0 - factor.clamp(0., 1.);
391 }
392}
393
394impl From<Rgba> for Hsla {
395 fn from(color: Rgba) -> Self {
396 let r = color.r;
397 let g = color.g;
398 let b = color.b;
399
400 let max = r.max(g.max(b));
401 let min = r.min(g.min(b));
402 let delta = max - min;
403
404 let l = (max + min) / 2.0;
405 let s = if l == 0.0 || l == 1.0 {
406 0.0
407 } else if l < 0.5 {
408 delta / (2.0 * l)
409 } else {
410 delta / (2.0 - 2.0 * l)
411 };
412
413 let h = if delta == 0.0 {
414 0.0
415 } else if max == r {
416 ((g - b) / delta).rem_euclid(6.0) / 6.0
417 } else if max == g {
418 ((b - r) / delta + 2.0) / 6.0
419 } else {
420 ((r - g) / delta + 4.0) / 6.0
421 };
422
423 Hsla {
424 h,
425 s,
426 l,
427 a: color.a,
428 }
429 }
430}
431
432impl<'de> Deserialize<'de> for Hsla {
433 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
434 where
435 D: Deserializer<'de>,
436 {
437 // First, deserialize it into Rgba
438 let rgba = Rgba::deserialize(deserializer)?;
439
440 // Then, use the From<Rgba> for Hsla implementation to convert it
441 Ok(Hsla::from(rgba))
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 use serde_json::json;
448
449 use super::*;
450
451 #[test]
452 fn test_deserialize_three_value_hex_to_rgba() {
453 let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap();
454
455 assert_eq!(actual, rgba(0xff0099ff))
456 }
457
458 #[test]
459 fn test_deserialize_four_value_hex_to_rgba() {
460 let actual: Rgba = serde_json::from_value(json!("#f09f")).unwrap();
461
462 assert_eq!(actual, rgba(0xff0099ff))
463 }
464
465 #[test]
466 fn test_deserialize_six_value_hex_to_rgba() {
467 let actual: Rgba = serde_json::from_value(json!("#ff0099")).unwrap();
468
469 assert_eq!(actual, rgba(0xff0099ff))
470 }
471
472 #[test]
473 fn test_deserialize_eight_value_hex_to_rgba() {
474 let actual: Rgba = serde_json::from_value(json!("#ff0099ff")).unwrap();
475
476 assert_eq!(actual, rgba(0xff0099ff))
477 }
478
479 #[test]
480 fn test_deserialize_eight_value_hex_with_padding_to_rgba() {
481 let actual: Rgba = serde_json::from_value(json!(" #f5f5f5ff ")).unwrap();
482
483 assert_eq!(actual, rgba(0xf5f5f5ff))
484 }
485
486 #[test]
487 fn test_deserialize_eight_value_hex_with_mixed_case_to_rgba() {
488 let actual: Rgba = serde_json::from_value(json!("#DeAdbEeF")).unwrap();
489
490 assert_eq!(actual, rgba(0xdeadbeef))
491 }
492}