1use crate::Error;
2use std::fmt;
3use std::str::FromStr;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub struct Rgb {
7 pub r: u8,
8 pub g: u8,
9 pub b: u8,
10}
11
12impl FromStr for Rgb {
13 type Err = Error;
14
15 fn from_str(s: &str) -> Result<Self, Self::Err> {
16 let hex = s.strip_prefix('#').unwrap_or(s);
17
18 if !hex.is_ascii() || hex.len() != 6 {
19 return Err(Error::InvalidHex(hex.to_string()));
20 }
21
22 let parse = |range: std::ops::Range<usize>| u8::from_str_radix(&hex[range], 16);
23
24 match (parse(0..2), parse(2..4), parse(4..6)) {
25 (Ok(r), Ok(g), Ok(b)) => Ok(Self { r, g, b }),
26 _ => Err(Error::InvalidHex(hex.to_string())),
27 }
28 }
29}
30
31impl fmt::Display for Rgb {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 write!(f, "#{:02X}{:02X}{:02X}", self.r, self.g, self.b)
34 }
35}
36
37impl Rgb {
38 #[must_use]
39 pub const fn as_floats(self) -> (f64, f64, f64) {
40 (
41 self.r as f64 / 255.0,
42 self.g as f64 / 255.0,
43 self.b as f64 / 255.0,
44 )
45 }
46
47 #[must_use]
51 pub const fn to_array(self) -> [f32; 3] {
52 [
53 self.r as f32 / 255.0,
54 self.g as f32 / 255.0,
55 self.b as f32 / 255.0,
56 ]
57 }
58
59 #[must_use]
63 pub const fn to_array_with_alpha(self) -> [f32; 4] {
64 [
65 self.r as f32 / 255.0,
66 self.g as f32 / 255.0,
67 self.b as f32 / 255.0,
68 1.0,
69 ]
70 }
71
72 #[must_use]
73 pub fn to_array_string(self) -> String {
74 format!("[{}, {}, {}]", self.r, self.g, self.b)
75 }
76
77 #[must_use]
82 pub fn lighten(self, factor: f64) -> Self {
83 let factor = factor.clamp(0.0, 1.0);
84 let (h, s, l) = self.to_hsl();
85 let new_l = l + (1.0 - l) * factor;
86 Self::from_hsl(h, s, new_l)
87 }
88
89 #[must_use]
94 pub fn darken(self, factor: f64) -> Self {
95 let factor = factor.clamp(0.0, 1.0);
96 let (h, s, l) = self.to_hsl();
97 let new_l = l * (1.0 - factor);
98 Self::from_hsl(h, s, new_l)
99 }
100
101 #[must_use]
106 pub fn brighten(self, amount: f64) -> Self {
107 let (h, s, l) = self.to_hsl();
108 let new_l = (l + amount).clamp(0.0, 1.0);
109 Self::from_hsl(h, s, new_l)
110 }
111
112 #[must_use]
116 pub fn mix(self, other: Self, factor: f64) -> Self {
117 let factor = factor.clamp(0.0, 1.0);
118 Self {
119 r: Self::blend_channel(self.r, other.r, factor),
120 g: Self::blend_channel(self.g, other.g, factor),
121 b: Self::blend_channel(self.b, other.b, factor),
122 }
123 }
124
125 fn to_hsl(self) -> (f64, f64, f64) {
132 let (r, g, b) = self.as_floats();
133
134 let max = r.max(g).max(b);
135 let min = r.min(g).min(b);
136 let l = (max + min) / 2.0;
137
138 if (max - min).abs() < f64::EPSILON {
139 return (0.0, 0.0, l);
140 }
141
142 let d = max - min;
143 let s = if l > 0.5 {
144 d / (2.0 - max - min)
145 } else {
146 d / (max + min)
147 };
148
149 let h = if (max - r).abs() < f64::EPSILON {
150 let mut h = (g - b) / d;
151 if g < b {
152 h += 6.0;
153 }
154 h
155 } else if (max - g).abs() < f64::EPSILON {
156 (b - r) / d + 2.0
157 } else {
158 (r - g) / d + 4.0
159 };
160
161 (h * 60.0, s, l)
162 }
163
164 fn from_hsl(h: f64, s: f64, l: f64) -> Self {
166 if s.abs() < f64::EPSILON {
167 let v = (l * 255.0).round() as u8;
168 return Self { r: v, g: v, b: v };
169 }
170
171 let q = if l < 0.5 {
172 l * (1.0 + s)
173 } else {
174 l + s - l * s
175 };
176 let p = 2.0 * l - q;
177 let h = h / 360.0;
178
179 let r = Self::hue_to_rgb(p, q, h + 1.0 / 3.0);
180 let g = Self::hue_to_rgb(p, q, h);
181 let b = Self::hue_to_rgb(p, q, h - 1.0 / 3.0);
182
183 Self {
184 r: (r * 255.0).round() as u8,
185 g: (g * 255.0).round() as u8,
186 b: (b * 255.0).round() as u8,
187 }
188 }
189
190 fn hue_to_rgb(p: f64, q: f64, t: f64) -> f64 {
191 let t = t.rem_euclid(1.0);
192
193 if t < 1.0 / 6.0 {
194 p + (q - p) * 6.0 * t
195 } else if t < 1.0 / 2.0 {
196 q
197 } else if t < 2.0 / 3.0 {
198 p + (q - p) * (2.0 / 3.0 - t) * 6.0
199 } else {
200 p
201 }
202 }
203
204 fn blend_channel(from: u8, to: u8, factor: f64) -> u8 {
205 let from = from as f64;
206 let to = to as f64;
207 (from + (to - from) * factor).round() as u8
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 fn approx_eq(a: f64, b: f64) -> bool {
216 (a - b).abs() < 0.001
217 }
218
219 #[test]
220 fn parse_with_hash() {
221 let rgb: Rgb = "#E26A3B".parse().unwrap();
222 assert_eq!(rgb.r, 226);
223 assert_eq!(rgb.g, 106);
224 assert_eq!(rgb.b, 59);
225 }
226
227 #[test]
228 fn parse_without_hash() {
229 let rgb: Rgb = "E26A3B".parse().unwrap();
230 assert_eq!(rgb.r, 226);
231 assert_eq!(rgb.g, 106);
232 assert_eq!(rgb.b, 59);
233 }
234
235 #[test]
236 fn parse_black() {
237 let rgb: Rgb = "#000000".parse().unwrap();
238 assert_eq!(rgb, Rgb { r: 0, g: 0, b: 0 });
239 }
240
241 #[test]
242 fn parse_white() {
243 let rgb: Rgb = "#FFFFFF".parse().unwrap();
244 assert_eq!(
245 rgb,
246 Rgb {
247 r: 255,
248 g: 255,
249 b: 255
250 }
251 );
252 }
253
254 #[test]
255 fn parse_lowercase() {
256 let rgb: Rgb = "#aabbcc".parse().unwrap();
257 assert_eq!(rgb.r, 170);
258 assert_eq!(rgb.g, 187);
259 assert_eq!(rgb.b, 204);
260 }
261
262 #[test]
263 fn parse_invalid_length_short() {
264 assert!("#FFF".parse::<Rgb>().is_err());
265 }
266
267 #[test]
268 fn parse_invalid_length_long() {
269 assert!("#FFFFFFFF".parse::<Rgb>().is_err());
270 }
271
272 #[test]
273 fn parse_invalid_chars() {
274 assert!("#GGGGGG".parse::<Rgb>().is_err());
275 }
276
277 #[test]
278 fn parse_empty() {
279 assert!("".parse::<Rgb>().is_err());
280 }
281
282 #[test]
283 fn parse_non_ascii() {
284 assert!("#ABCDEF".parse::<Rgb>().is_err());
285 }
286
287 #[test]
288 fn as_floats_black() {
289 let rgb = Rgb { r: 0, g: 0, b: 0 };
290 let (r, g, b) = rgb.as_floats();
291 assert!(approx_eq(r, 0.0));
292 assert!(approx_eq(g, 0.0));
293 assert!(approx_eq(b, 0.0));
294 }
295
296 #[test]
297 fn as_floats_white() {
298 let rgb = Rgb {
299 r: 255,
300 g: 255,
301 b: 255,
302 };
303 let (r, g, b) = rgb.as_floats();
304 assert!(approx_eq(r, 1.0));
305 assert!(approx_eq(g, 1.0));
306 assert!(approx_eq(b, 1.0));
307 }
308
309 #[test]
310 fn to_array_string_format() {
311 let rgb = Rgb {
312 r: 226,
313 g: 106,
314 b: 59,
315 };
316 assert_eq!(rgb.to_array_string(), "[226, 106, 59]");
317 }
318
319 #[test]
320 fn display_uppercase() {
321 let rgb = Rgb {
322 r: 226,
323 g: 106,
324 b: 59,
325 };
326 assert_eq!(rgb.to_string(), "#E26A3B");
327 }
328
329 #[test]
330 fn display_with_leading_zeros() {
331 let rgb = Rgb { r: 1, g: 2, b: 3 };
332 assert_eq!(rgb.to_string(), "#010203");
333 }
334
335 #[test]
336 fn lighten_zero_unchanged() {
337 let rgb = Rgb {
338 r: 100,
339 g: 100,
340 b: 100,
341 };
342 assert_eq!(rgb.lighten(0.0), rgb);
343 }
344
345 #[test]
346 fn lighten_full_becomes_white() {
347 let rgb = Rgb {
348 r: 100,
349 g: 100,
350 b: 100,
351 };
352 assert_eq!(
353 rgb.lighten(1.0),
354 Rgb {
355 r: 255,
356 g: 255,
357 b: 255
358 }
359 );
360 }
361
362 #[test]
363 fn lighten_half() {
364 let rgb = Rgb {
365 r: 100,
366 g: 100,
367 b: 100,
368 };
369 assert_eq!(
371 rgb.lighten(0.5),
372 Rgb {
373 r: 178,
374 g: 178,
375 b: 178
376 }
377 );
378 }
379
380 #[test]
381 fn darken_zero_unchanged() {
382 let rgb = Rgb {
383 r: 100,
384 g: 100,
385 b: 100,
386 };
387 assert_eq!(rgb.darken(0.0), rgb);
388 }
389
390 #[test]
391 fn darken_full_becomes_black() {
392 let rgb = Rgb {
393 r: 100,
394 g: 100,
395 b: 100,
396 };
397 assert_eq!(rgb.darken(1.0), Rgb { r: 0, g: 0, b: 0 });
398 }
399
400 #[test]
401 fn darken_half() {
402 let rgb = Rgb {
403 r: 100,
404 g: 100,
405 b: 100,
406 };
407 assert_eq!(
409 rgb.darken(0.5),
410 Rgb {
411 r: 50,
412 g: 50,
413 b: 50
414 }
415 );
416 }
417
418 #[test]
419 fn lighten_clamps_factor() {
420 let rgb = Rgb {
421 r: 100,
422 g: 100,
423 b: 100,
424 };
425 assert_eq!(
427 rgb.lighten(2.0),
428 Rgb {
429 r: 255,
430 g: 255,
431 b: 255
432 }
433 );
434 }
435
436 #[test]
437 fn darken_clamps_negative_factor() {
438 let rgb = Rgb {
439 r: 100,
440 g: 100,
441 b: 100,
442 };
443 assert_eq!(rgb.darken(-0.5), rgb);
445 }
446
447 #[test]
448 fn brighten_zero_unchanged() {
449 let rgb = Rgb {
450 r: 100,
451 g: 100,
452 b: 100,
453 };
454 assert_eq!(rgb.brighten(0.0), rgb);
455 }
456
457 #[test]
458 fn brighten_positive_increases_lightness() {
459 let rgb = Rgb {
460 r: 100,
461 g: 100,
462 b: 100,
463 };
464 let brightened = rgb.brighten(0.2);
465 assert!(brightened.r > rgb.r);
467 assert!(brightened.g > rgb.g);
468 assert!(brightened.b > rgb.b);
469 }
470
471 #[test]
472 fn brighten_negative_decreases_lightness() {
473 let rgb = Rgb {
474 r: 100,
475 g: 100,
476 b: 100,
477 };
478 let dimmed = rgb.brighten(-0.2);
479 assert!(dimmed.r < rgb.r);
481 assert!(dimmed.g < rgb.g);
482 assert!(dimmed.b < rgb.b);
483 }
484
485 #[test]
486 fn brighten_clamps_to_white() {
487 let rgb = Rgb {
488 r: 200,
489 g: 200,
490 b: 200,
491 };
492 let result = rgb.brighten(1.0);
493 assert_eq!(
494 result,
495 Rgb {
496 r: 255,
497 g: 255,
498 b: 255
499 }
500 );
501 }
502
503 #[test]
504 fn brighten_clamps_to_black() {
505 let rgb = Rgb {
506 r: 50,
507 g: 50,
508 b: 50,
509 };
510 let result = rgb.brighten(-1.0);
511 assert_eq!(result, Rgb { r: 0, g: 0, b: 0 });
512 }
513
514 #[test]
515 fn mix_zero_returns_self() {
516 let a = Rgb {
517 r: 100,
518 g: 100,
519 b: 100,
520 };
521 let b = Rgb {
522 r: 200,
523 g: 200,
524 b: 200,
525 };
526 assert_eq!(a.mix(b, 0.0), a);
527 }
528
529 #[test]
530 fn mix_one_returns_other() {
531 let a = Rgb {
532 r: 100,
533 g: 100,
534 b: 100,
535 };
536 let b = Rgb {
537 r: 200,
538 g: 200,
539 b: 200,
540 };
541 assert_eq!(a.mix(b, 1.0), b);
542 }
543
544 #[test]
545 fn mix_half() {
546 let a = Rgb {
547 r: 100,
548 g: 100,
549 b: 100,
550 };
551 let b = Rgb {
552 r: 200,
553 g: 200,
554 b: 200,
555 };
556 assert_eq!(
558 a.mix(b, 0.5),
559 Rgb {
560 r: 150,
561 g: 150,
562 b: 150
563 }
564 );
565 }
566
567 fn approx_eq_f32(a: f32, b: f32) -> bool {
568 (a - b).abs() < 0.001
569 }
570
571 #[test]
572 fn to_array_black() {
573 let rgb = Rgb { r: 0, g: 0, b: 0 };
574 let arr = rgb.to_array();
575 assert!(approx_eq_f32(arr[0], 0.0));
576 assert!(approx_eq_f32(arr[1], 0.0));
577 assert!(approx_eq_f32(arr[2], 0.0));
578 }
579
580 #[test]
581 fn to_array_white() {
582 let rgb = Rgb {
583 r: 255,
584 g: 255,
585 b: 255,
586 };
587 let arr = rgb.to_array();
588 assert!(approx_eq_f32(arr[0], 1.0));
589 assert!(approx_eq_f32(arr[1], 1.0));
590 assert!(approx_eq_f32(arr[2], 1.0));
591 }
592
593 #[test]
594 fn to_array_with_alpha_black() {
595 let rgb = Rgb { r: 0, g: 0, b: 0 };
596 let arr = rgb.to_array_with_alpha();
597 assert!(approx_eq_f32(arr[0], 0.0));
598 assert!(approx_eq_f32(arr[1], 0.0));
599 assert!(approx_eq_f32(arr[2], 0.0));
600 assert!(approx_eq_f32(arr[3], 1.0));
601 }
602
603 #[test]
604 fn to_array_with_alpha_white() {
605 let rgb = Rgb {
606 r: 255,
607 g: 255,
608 b: 255,
609 };
610 let arr = rgb.to_array_with_alpha();
611 assert!(approx_eq_f32(arr[0], 1.0));
612 assert!(approx_eq_f32(arr[1], 1.0));
613 assert!(approx_eq_f32(arr[2], 1.0));
614 assert!(approx_eq_f32(arr[3], 1.0));
615 }
616}