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]
81 pub fn to_space_separated(self) -> String {
82 format!("{} {} {}", self.r, self.g, self.b)
83 }
84
85 #[must_use]
90 pub fn lighten(self, factor: f64) -> Self {
91 let factor = factor.clamp(0.0, 1.0);
92 let (h, s, l) = self.to_hsl();
93 let new_l = l + (1.0 - l) * factor;
94 Self::from_hsl(h, s, new_l)
95 }
96
97 #[must_use]
102 pub fn darken(self, factor: f64) -> Self {
103 let factor = factor.clamp(0.0, 1.0);
104 let (h, s, l) = self.to_hsl();
105 let new_l = l * (1.0 - factor);
106 Self::from_hsl(h, s, new_l)
107 }
108
109 #[must_use]
114 pub fn brighten(self, amount: f64) -> Self {
115 let (h, s, l) = self.to_hsl();
116 let new_l = (l + amount).clamp(0.0, 1.0);
117 Self::from_hsl(h, s, new_l)
118 }
119
120 #[must_use]
124 pub fn mix(self, other: Self, factor: f64) -> Self {
125 let factor = factor.clamp(0.0, 1.0);
126 Self {
127 r: Self::blend_channel(self.r, other.r, factor),
128 g: Self::blend_channel(self.g, other.g, factor),
129 b: Self::blend_channel(self.b, other.b, factor),
130 }
131 }
132
133 fn to_hsl(self) -> (f64, f64, f64) {
140 let (r, g, b) = self.as_floats();
141
142 let max = r.max(g).max(b);
143 let min = r.min(g).min(b);
144 let l = (max + min) / 2.0;
145
146 if (max - min).abs() < f64::EPSILON {
147 return (0.0, 0.0, l);
148 }
149
150 let d = max - min;
151 let s = if l > 0.5 {
152 d / (2.0 - max - min)
153 } else {
154 d / (max + min)
155 };
156
157 let h = if (max - r).abs() < f64::EPSILON {
158 let mut h = (g - b) / d;
159 if g < b {
160 h += 6.0;
161 }
162 h
163 } else if (max - g).abs() < f64::EPSILON {
164 (b - r) / d + 2.0
165 } else {
166 (r - g) / d + 4.0
167 };
168
169 (h * 60.0, s, l)
170 }
171
172 fn from_hsl(h: f64, s: f64, l: f64) -> Self {
174 if s.abs() < f64::EPSILON {
175 let v = (l * 255.0).round() as u8;
176 return Self { r: v, g: v, b: v };
177 }
178
179 let q = if l < 0.5 {
180 l * (1.0 + s)
181 } else {
182 l + s - l * s
183 };
184 let p = 2.0 * l - q;
185 let h = h / 360.0;
186
187 let r = Self::hue_to_rgb(p, q, h + 1.0 / 3.0);
188 let g = Self::hue_to_rgb(p, q, h);
189 let b = Self::hue_to_rgb(p, q, h - 1.0 / 3.0);
190
191 Self {
192 r: (r * 255.0).round() as u8,
193 g: (g * 255.0).round() as u8,
194 b: (b * 255.0).round() as u8,
195 }
196 }
197
198 fn hue_to_rgb(p: f64, q: f64, t: f64) -> f64 {
199 let t = t.rem_euclid(1.0);
200
201 if t < 1.0 / 6.0 {
202 p + (q - p) * 6.0 * t
203 } else if t < 1.0 / 2.0 {
204 q
205 } else if t < 2.0 / 3.0 {
206 p + (q - p) * (2.0 / 3.0 - t) * 6.0
207 } else {
208 p
209 }
210 }
211
212 fn blend_channel(from: u8, to: u8, factor: f64) -> u8 {
213 let from = from as f64;
214 let to = to as f64;
215 (from + (to - from) * factor).round() as u8
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 fn approx_eq(a: f64, b: f64) -> bool {
224 (a - b).abs() < 0.001
225 }
226
227 #[test]
228 fn parse_with_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_without_hash() {
237 let rgb: Rgb = "E26A3B".parse().unwrap();
238 assert_eq!(rgb.r, 226);
239 assert_eq!(rgb.g, 106);
240 assert_eq!(rgb.b, 59);
241 }
242
243 #[test]
244 fn parse_black() {
245 let rgb: Rgb = "#000000".parse().unwrap();
246 assert_eq!(rgb, Rgb { r: 0, g: 0, b: 0 });
247 }
248
249 #[test]
250 fn parse_white() {
251 let rgb: Rgb = "#FFFFFF".parse().unwrap();
252 assert_eq!(
253 rgb,
254 Rgb {
255 r: 255,
256 g: 255,
257 b: 255
258 }
259 );
260 }
261
262 #[test]
263 fn parse_lowercase() {
264 let rgb: Rgb = "#aabbcc".parse().unwrap();
265 assert_eq!(rgb.r, 170);
266 assert_eq!(rgb.g, 187);
267 assert_eq!(rgb.b, 204);
268 }
269
270 #[test]
271 fn parse_invalid_length_short() {
272 assert!("#FFF".parse::<Rgb>().is_err());
273 }
274
275 #[test]
276 fn parse_invalid_length_long() {
277 assert!("#FFFFFFFF".parse::<Rgb>().is_err());
278 }
279
280 #[test]
281 fn parse_invalid_chars() {
282 assert!("#GGGGGG".parse::<Rgb>().is_err());
283 }
284
285 #[test]
286 fn parse_empty() {
287 assert!("".parse::<Rgb>().is_err());
288 }
289
290 #[test]
291 fn parse_non_ascii() {
292 assert!("#ABCDEF".parse::<Rgb>().is_err());
293 }
294
295 #[test]
296 fn as_floats_black() {
297 let rgb = Rgb { r: 0, g: 0, b: 0 };
298 let (r, g, b) = rgb.as_floats();
299 assert!(approx_eq(r, 0.0));
300 assert!(approx_eq(g, 0.0));
301 assert!(approx_eq(b, 0.0));
302 }
303
304 #[test]
305 fn as_floats_white() {
306 let rgb = Rgb {
307 r: 255,
308 g: 255,
309 b: 255,
310 };
311 let (r, g, b) = rgb.as_floats();
312 assert!(approx_eq(r, 1.0));
313 assert!(approx_eq(g, 1.0));
314 assert!(approx_eq(b, 1.0));
315 }
316
317 #[test]
318 fn to_array_string_format() {
319 let rgb = Rgb {
320 r: 226,
321 g: 106,
322 b: 59,
323 };
324 assert_eq!(rgb.to_array_string(), "[226, 106, 59]");
325 }
326
327 #[test]
328 fn display_uppercase() {
329 let rgb = Rgb {
330 r: 226,
331 g: 106,
332 b: 59,
333 };
334 assert_eq!(rgb.to_string(), "#E26A3B");
335 }
336
337 #[test]
338 fn display_with_leading_zeros() {
339 let rgb = Rgb { r: 1, g: 2, b: 3 };
340 assert_eq!(rgb.to_string(), "#010203");
341 }
342
343 #[test]
344 fn lighten_zero_unchanged() {
345 let rgb = Rgb {
346 r: 100,
347 g: 100,
348 b: 100,
349 };
350 assert_eq!(rgb.lighten(0.0), rgb);
351 }
352
353 #[test]
354 fn lighten_full_becomes_white() {
355 let rgb = Rgb {
356 r: 100,
357 g: 100,
358 b: 100,
359 };
360 assert_eq!(
361 rgb.lighten(1.0),
362 Rgb {
363 r: 255,
364 g: 255,
365 b: 255
366 }
367 );
368 }
369
370 #[test]
371 fn lighten_half() {
372 let rgb = Rgb {
373 r: 100,
374 g: 100,
375 b: 100,
376 };
377 assert_eq!(
379 rgb.lighten(0.5),
380 Rgb {
381 r: 178,
382 g: 178,
383 b: 178
384 }
385 );
386 }
387
388 #[test]
389 fn darken_zero_unchanged() {
390 let rgb = Rgb {
391 r: 100,
392 g: 100,
393 b: 100,
394 };
395 assert_eq!(rgb.darken(0.0), rgb);
396 }
397
398 #[test]
399 fn darken_full_becomes_black() {
400 let rgb = Rgb {
401 r: 100,
402 g: 100,
403 b: 100,
404 };
405 assert_eq!(rgb.darken(1.0), Rgb { r: 0, g: 0, b: 0 });
406 }
407
408 #[test]
409 fn darken_half() {
410 let rgb = Rgb {
411 r: 100,
412 g: 100,
413 b: 100,
414 };
415 assert_eq!(
417 rgb.darken(0.5),
418 Rgb {
419 r: 50,
420 g: 50,
421 b: 50
422 }
423 );
424 }
425
426 #[test]
427 fn lighten_clamps_factor() {
428 let rgb = Rgb {
429 r: 100,
430 g: 100,
431 b: 100,
432 };
433 assert_eq!(
435 rgb.lighten(2.0),
436 Rgb {
437 r: 255,
438 g: 255,
439 b: 255
440 }
441 );
442 }
443
444 #[test]
445 fn darken_clamps_negative_factor() {
446 let rgb = Rgb {
447 r: 100,
448 g: 100,
449 b: 100,
450 };
451 assert_eq!(rgb.darken(-0.5), rgb);
453 }
454
455 #[test]
456 fn brighten_zero_unchanged() {
457 let rgb = Rgb {
458 r: 100,
459 g: 100,
460 b: 100,
461 };
462 assert_eq!(rgb.brighten(0.0), rgb);
463 }
464
465 #[test]
466 fn brighten_positive_increases_lightness() {
467 let rgb = Rgb {
468 r: 100,
469 g: 100,
470 b: 100,
471 };
472 let brightened = rgb.brighten(0.2);
473 assert!(brightened.r > rgb.r);
475 assert!(brightened.g > rgb.g);
476 assert!(brightened.b > rgb.b);
477 }
478
479 #[test]
480 fn brighten_negative_decreases_lightness() {
481 let rgb = Rgb {
482 r: 100,
483 g: 100,
484 b: 100,
485 };
486 let dimmed = rgb.brighten(-0.2);
487 assert!(dimmed.r < rgb.r);
489 assert!(dimmed.g < rgb.g);
490 assert!(dimmed.b < rgb.b);
491 }
492
493 #[test]
494 fn brighten_clamps_to_white() {
495 let rgb = Rgb {
496 r: 200,
497 g: 200,
498 b: 200,
499 };
500 let result = rgb.brighten(1.0);
501 assert_eq!(
502 result,
503 Rgb {
504 r: 255,
505 g: 255,
506 b: 255
507 }
508 );
509 }
510
511 #[test]
512 fn brighten_clamps_to_black() {
513 let rgb = Rgb {
514 r: 50,
515 g: 50,
516 b: 50,
517 };
518 let result = rgb.brighten(-1.0);
519 assert_eq!(result, Rgb { r: 0, g: 0, b: 0 });
520 }
521
522 #[test]
523 fn mix_zero_returns_self() {
524 let a = Rgb {
525 r: 100,
526 g: 100,
527 b: 100,
528 };
529 let b = Rgb {
530 r: 200,
531 g: 200,
532 b: 200,
533 };
534 assert_eq!(a.mix(b, 0.0), a);
535 }
536
537 #[test]
538 fn mix_one_returns_other() {
539 let a = Rgb {
540 r: 100,
541 g: 100,
542 b: 100,
543 };
544 let b = Rgb {
545 r: 200,
546 g: 200,
547 b: 200,
548 };
549 assert_eq!(a.mix(b, 1.0), b);
550 }
551
552 #[test]
553 fn mix_half() {
554 let a = Rgb {
555 r: 100,
556 g: 100,
557 b: 100,
558 };
559 let b = Rgb {
560 r: 200,
561 g: 200,
562 b: 200,
563 };
564 assert_eq!(
566 a.mix(b, 0.5),
567 Rgb {
568 r: 150,
569 g: 150,
570 b: 150
571 }
572 );
573 }
574
575 fn approx_eq_f32(a: f32, b: f32) -> bool {
576 (a - b).abs() < 0.001
577 }
578
579 #[test]
580 fn to_array_black() {
581 let rgb = Rgb { r: 0, g: 0, b: 0 };
582 let arr = rgb.to_array();
583 assert!(approx_eq_f32(arr[0], 0.0));
584 assert!(approx_eq_f32(arr[1], 0.0));
585 assert!(approx_eq_f32(arr[2], 0.0));
586 }
587
588 #[test]
589 fn to_array_white() {
590 let rgb = Rgb {
591 r: 255,
592 g: 255,
593 b: 255,
594 };
595 let arr = rgb.to_array();
596 assert!(approx_eq_f32(arr[0], 1.0));
597 assert!(approx_eq_f32(arr[1], 1.0));
598 assert!(approx_eq_f32(arr[2], 1.0));
599 }
600
601 #[test]
602 fn to_array_with_alpha_black() {
603 let rgb = Rgb { r: 0, g: 0, b: 0 };
604 let arr = rgb.to_array_with_alpha();
605 assert!(approx_eq_f32(arr[0], 0.0));
606 assert!(approx_eq_f32(arr[1], 0.0));
607 assert!(approx_eq_f32(arr[2], 0.0));
608 assert!(approx_eq_f32(arr[3], 1.0));
609 }
610
611 #[test]
612 fn to_array_with_alpha_white() {
613 let rgb = Rgb {
614 r: 255,
615 g: 255,
616 b: 255,
617 };
618 let arr = rgb.to_array_with_alpha();
619 assert!(approx_eq_f32(arr[0], 1.0));
620 assert!(approx_eq_f32(arr[1], 1.0));
621 assert!(approx_eq_f32(arr[2], 1.0));
622 assert!(approx_eq_f32(arr[3], 1.0));
623 }
624}