1use crate::prelude::{HSV, RGBA};
2use std::convert::From;
3use std::ops;
4
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6#[derive(PartialEq, Copy, Clone, Default, Debug)]
7pub struct RGB {
9 pub r: f32,
11 pub g: f32,
13 pub b: f32,
15}
16
17#[derive(Debug, PartialEq, Copy, Clone)]
18pub enum HtmlColorConversionError {
20 InvalidStringLength,
22 MissingHash,
24 InvalidCharacter,
26}
27
28impl ops::Add<f32> for RGB {
32 type Output = Self;
33 #[must_use]
34 fn add(mut self, rhs: f32) -> Self {
35 self.r += rhs;
36 self.g += rhs;
37 self.b += rhs;
38 self
39 }
40}
41
42impl ops::Add<RGB> for RGB {
44 type Output = Self;
45 #[must_use]
46 fn add(mut self, rhs: Self) -> Self {
47 self.r += rhs.r;
48 self.g += rhs.g;
49 self.b += rhs.b;
50 self
51 }
52}
53
54impl ops::Sub<f32> for RGB {
56 type Output = Self;
57 #[must_use]
58 fn sub(mut self, rhs: f32) -> Self {
59 self.r -= rhs;
60 self.g -= rhs;
61 self.b -= rhs;
62 self
63 }
64}
65
66impl ops::Sub<RGB> for RGB {
68 type Output = Self;
69 #[must_use]
70 fn sub(mut self, rhs: Self) -> Self {
71 self.r -= rhs.r;
72 self.g -= rhs.g;
73 self.b -= rhs.b;
74 self
75 }
76}
77
78impl ops::Mul<f32> for RGB {
80 type Output = Self;
81 #[must_use]
82 fn mul(mut self, rhs: f32) -> Self {
83 self.r *= rhs;
84 self.g *= rhs;
85 self.b *= rhs;
86 self
87 }
88}
89
90impl ops::Mul<RGB> for RGB {
92 type Output = Self;
93 #[must_use]
94 fn mul(mut self, rhs: Self) -> Self {
95 self.r *= rhs.r;
96 self.g *= rhs.g;
97 self.b *= rhs.b;
98 self
99 }
100}
101
102impl From<(u8, u8, u8)> for RGB {
104 fn from(vals: (u8, u8, u8)) -> Self {
105 Self::named(vals)
106 }
107}
108
109impl From<HSV> for RGB {
111 fn from(hsv: HSV) -> Self {
112 hsv.to_rgb()
113 }
114}
115
116impl From<RGBA> for RGB {
118 fn from(item: RGBA) -> Self {
119 Self::from_f32(item.r, item.g, item.b)
120 }
121}
122
123#[cfg(feature = "bevy")]
125impl From<bevy::prelude::Color> for RGB {
126 fn from(item: bevy::prelude::Color) -> Self {
127 Self::from_f32(item.r(), item.g(), item.b())
128 }
129}
130
131#[cfg(feature = "bevy")]
132impl From<RGB> for bevy::prelude::Color {
133 fn from(item: RGB) -> Self {
134 Self::from([item.r, item.g, item.b])
135 }
136}
137
138impl RGB {
139 #[must_use]
141 pub fn new() -> Self {
142 Self {
143 r: 0.0,
144 g: 0.0,
145 b: 0.0,
146 }
147 }
148
149 #[inline]
165 #[must_use]
166 pub fn from_f32(r: f32, g: f32, b: f32) -> Self {
167 let r_clamped = f32::min(1.0, f32::max(0.0, r));
168 let g_clamped = f32::min(1.0, f32::max(0.0, g));
169 let b_clamped = f32::min(1.0, f32::max(0.0, b));
170 Self {
171 r: r_clamped,
172 g: g_clamped,
173 b: b_clamped,
174 }
175 }
176
177 #[inline]
193 #[must_use]
194 pub fn from_u8(r: u8, g: u8, b: u8) -> Self {
195 Self {
196 r: f32::from(r) / 255.0,
197 g: f32::from(g) / 255.0,
198 b: f32::from(b) / 255.0,
199 }
200 }
201
202 #[inline]
216 #[must_use]
217 pub fn named(col: (u8, u8, u8)) -> Self {
218 Self::from_u8(col.0, col.1, col.2)
219 }
220
221 #[allow(clippy::cast_precision_loss)]
239 pub fn from_hex<S: AsRef<str>>(code: S) -> Result<Self, HtmlColorConversionError> {
240 let mut full_code = code.as_ref().chars();
241
242 if let Some(hash) = full_code.next() {
243 if hash != '#' {
244 return Err(HtmlColorConversionError::MissingHash);
245 }
246 } else {
247 return Err(HtmlColorConversionError::InvalidStringLength);
248 }
249
250 let red1 = match full_code.next() {
251 Some(red) => match red.to_digit(16) {
252 Some(red) => red * 16,
253 None => return Err(HtmlColorConversionError::InvalidCharacter),
254 },
255 None => return Err(HtmlColorConversionError::InvalidStringLength),
256 };
257 let red2 = match full_code.next() {
258 Some(red) => match red.to_digit(16) {
259 Some(red) => red,
260 None => return Err(HtmlColorConversionError::InvalidCharacter),
261 },
262 None => return Err(HtmlColorConversionError::InvalidStringLength),
263 };
264
265 let green1 = match full_code.next() {
266 Some(green) => match green.to_digit(16) {
267 Some(green) => green * 16,
268 None => return Err(HtmlColorConversionError::InvalidCharacter),
269 },
270 None => return Err(HtmlColorConversionError::InvalidStringLength),
271 };
272 let green2 = match full_code.next() {
273 Some(green) => match green.to_digit(16) {
274 Some(green) => green,
275 None => return Err(HtmlColorConversionError::InvalidCharacter),
276 },
277 None => return Err(HtmlColorConversionError::InvalidStringLength),
278 };
279
280 let blue1 = match full_code.next() {
281 Some(blue) => match blue.to_digit(16) {
282 Some(blue) => blue * 16,
283 None => return Err(HtmlColorConversionError::InvalidCharacter),
284 },
285 None => return Err(HtmlColorConversionError::InvalidStringLength),
286 };
287 let blue2 = match full_code.next() {
288 Some(blue) => match blue.to_digit(16) {
289 Some(blue) => blue,
290 None => return Err(HtmlColorConversionError::InvalidCharacter),
291 },
292 None => return Err(HtmlColorConversionError::InvalidStringLength),
293 };
294
295 if full_code.next().is_some() {
296 return Err(HtmlColorConversionError::InvalidStringLength);
297 }
298
299 Ok(Self {
300 r: (red1 + red2) as f32 / 255.0,
301 g: (green1 + green2) as f32 / 255.0,
302 b: (blue1 + blue2) as f32 / 255.0,
303 })
304 }
305
306 #[allow(clippy::many_single_char_names)]
308 #[must_use]
309 pub fn to_hsv(&self) -> HSV {
310 let r = self.r;
311 let g = self.g;
312 let b = self.b;
313
314 let max = f32::max(f32::max(r, g), b);
315 let min = f32::min(f32::min(r, g), b);
316
317 let mut h: f32 = max;
318 let v: f32 = max;
319
320 let d = max - min;
321 let s = if max == 0.0 { 0.0 } else { d / max };
322
323 if (max - min).abs() < std::f32::EPSILON {
324 h = 0.0; } else {
326 if (max - r).abs() < std::f32::EPSILON {
327 if g < b {
328 h = (g - b) / d + 6.0;
329 } else {
330 h = (g - b) / d;
331 }
332 } else if (max - g).abs() < std::f32::EPSILON {
333 h = (b - r) / d + 2.0;
334 } else if (max - b).abs() < std::f32::EPSILON {
335 h = (r - g) / d + 4.0;
336 }
337
338 h /= 6.0;
339 }
340
341 HSV::from_f32(h, s, v)
342 }
343
344 #[inline]
346 #[must_use]
347 pub fn to_rgba(&self, alpha: f32) -> RGBA {
348 RGBA::from_f32(self.r, self.g, self.b, alpha)
349 }
350
351 #[inline]
353 #[must_use]
354 pub fn to_greyscale(&self) -> Self {
355 let linear = (self.r * 0.2126) + (self.g * 0.7152) + (self.b * 0.0722);
356 Self::from_f32(linear, linear, linear)
357 }
358
359 #[inline]
361 #[must_use]
362 pub fn desaturate(&self) -> Self {
363 let mut hsv = self.to_hsv();
364 hsv.s = 0.0;
365 hsv.to_rgb()
366 }
367
368 #[inline]
370 #[must_use]
371 pub fn lerp(&self, color: Self, percent: f32) -> Self {
372 let range = (color.r - self.r, color.g - self.g, color.b - self.b);
373 Self {
374 r: self.r + range.0 * percent,
375 g: self.g + range.1 * percent,
376 b: self.b + range.2 * percent,
377 }
378 }
379}
380
381#[cfg(feature = "crossterm")]
382mod crossterm_features {
383 use super::RGB;
384 use crossterm::style::Color;
385 use std::convert::TryFrom;
386
387 impl TryFrom<RGB> for Color {
388 type Error = &'static str;
389
390 fn try_from(rgb: RGB) -> Result<Self, Self::Error> {
391 let (r, g, b) = (rgb.r, rgb.g, rgb.b);
392 for c in [r, g, b].iter() {
393 if *c < 0.0 {
394 return Err("Value < 0.0 found!");
395 }
396 if *c > 1.0 {
397 return Err("Value > 1.0 found!");
398 }
399 }
400 let (r, g, b) = ((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8);
401 let rgb = Color::Rgb { r, g, b };
402 Ok(rgb)
403 }
404 }
405
406 #[cfg(test)]
407 mod tests {
408 use crate::prelude::RGB;
409 use crossterm::style::Color;
410 use std::convert::TryInto;
411
412 #[test]
413 fn basic_conversion() {
414 let rgb = RGB {
415 r: 0.0,
416 g: 0.5,
417 b: 1.0,
418 };
419 let rgb: Color = rgb.try_into().unwrap();
420 match rgb {
421 Color::Rgb { r, g, b } => {
422 assert_eq!(r, 0);
423 assert_eq!(g, 127);
424 assert_eq!(b, 255);
425 }
426 _ => unreachable!(),
427 }
428 }
429
430 #[test]
431 fn negative_rgb() {
432 let rgb = RGB {
433 r: 0.0,
434 g: 0.5,
435 b: -1.0,
436 };
437 let rgb: Result<Color, _> = rgb.try_into();
438 assert!(rgb.is_err());
439 }
440
441 #[test]
442 fn too_large_rgb() {
443 let rgb = RGB {
444 r: 0.0,
445 g: 0.5,
446 b: 1.1,
447 };
448 let rgb: Result<Color, _> = rgb.try_into();
449 assert!(rgb.is_err());
450 }
451 }
452}
453
454#[cfg(test)]
457mod tests {
458 use crate::prelude::*;
459
460 #[test]
461 fn make_rgb_minimal() {
463 let black = RGB::new();
464 assert!(black.r < std::f32::EPSILON);
465 assert!(black.g < std::f32::EPSILON);
466 assert!(black.b < std::f32::EPSILON);
467 }
468
469 #[test]
470 fn convert_olive_to_rgb() {
472 let grey = HSV::from_f32(60.0 / 360.0, 1.0, 0.501_960_8);
473 let rgb = grey.to_rgb();
474 assert!(f32::abs(rgb.r - 128.0 / 255.0) < std::f32::EPSILON);
475 assert!(f32::abs(rgb.g - 128.0 / 255.0) < std::f32::EPSILON);
476 assert!(rgb.b < std::f32::EPSILON);
477 }
478
479 #[test]
480 fn test_red_hex() {
482 let rgb = RGB::from_hex("#FF0000").expect("Invalid hex string");
483 assert!(f32::abs(rgb.r - 1.0) < std::f32::EPSILON);
484 assert!(rgb.g < std::f32::EPSILON);
485 assert!(rgb.b < std::f32::EPSILON);
486 }
487
488 #[test]
489 fn test_green_hex() {
491 let rgb = RGB::from_hex("#00FF00").expect("Invalid hex string");
492 assert!(rgb.r < std::f32::EPSILON);
493 assert!(f32::abs(rgb.g - 1.0) < std::f32::EPSILON);
494 assert!(rgb.b < std::f32::EPSILON);
495 }
496
497 #[test]
498 fn test_blue_hex() {
500 let rgb = RGB::from_hex("#0000FF").expect("Invalid hex string");
501 assert!(rgb.r < std::f32::EPSILON);
502 assert!(rgb.g < std::f32::EPSILON);
503 assert!(f32::abs(rgb.b - 1.0) < std::f32::EPSILON);
504 }
505
506 #[test]
507 fn test_blue_named() {
509 let rgb = RGB::named(BLUE);
510 assert!(rgb.r < std::f32::EPSILON);
511 assert!(rgb.g < std::f32::EPSILON);
512 assert!(f32::abs(rgb.b - 1.0) < std::f32::EPSILON);
513 }
514
515 #[test]
516 fn test_lerp() {
518 let black = RGB::named(BLACK);
519 let white = RGB::named(WHITE);
520 assert!(black.lerp(white, 0.0) == black);
521 assert!(black.lerp(white, 1.0) == white);
522 }
523}