1use crate::prelude::{HtmlColorConversionError, HSV, RGB};
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 RGBA {
9 pub r: f32,
11 pub g: f32,
13 pub b: f32,
15 pub a: f32,
17}
18
19impl ops::Add<f32> for RGBA {
23 type Output = Self;
24 #[must_use]
25 fn add(mut self, rhs: f32) -> Self {
26 self.r += rhs;
27 self.g += rhs;
28 self.b += rhs;
29 self.a += rhs;
30 self
31 }
32}
33
34impl ops::Add<RGBA> for RGBA {
36 type Output = Self;
37 #[must_use]
38 fn add(mut self, rhs: Self) -> Self {
39 self.r += rhs.r;
40 self.g += rhs.g;
41 self.b += rhs.b;
42 self.a += rhs.a;
43 self
44 }
45}
46
47impl ops::Sub<f32> for RGBA {
49 type Output = Self;
50 #[must_use]
51 fn sub(mut self, rhs: f32) -> Self {
52 self.r -= rhs;
53 self.g -= rhs;
54 self.b -= rhs;
55 self.a -= rhs;
56 self
57 }
58}
59
60impl ops::Sub<RGBA> for RGBA {
62 type Output = Self;
63 #[must_use]
64 fn sub(mut self, rhs: Self) -> Self {
65 self.r -= rhs.r;
66 self.g -= rhs.g;
67 self.b -= rhs.b;
68 self.a -= rhs.a;
69 self
70 }
71}
72
73impl ops::Mul<f32> for RGBA {
75 type Output = Self;
76 #[must_use]
77 fn mul(mut self, rhs: f32) -> Self {
78 self.r *= rhs;
79 self.g *= rhs;
80 self.b *= rhs;
81 self.a *= rhs;
82 self
83 }
84}
85
86impl ops::Mul<RGBA> for RGBA {
88 type Output = Self;
89 #[must_use]
90 fn mul(mut self, rhs: Self) -> Self {
91 self.r *= rhs.r;
92 self.g *= rhs.g;
93 self.b *= rhs.b;
94 self.a *= rhs.a;
95 self
96 }
97}
98
99impl RGBA {
100 #[must_use]
102 pub fn new() -> Self {
103 Self {
104 r: 0.0,
105 g: 0.0,
106 b: 0.0,
107 a: 0.0,
108 }
109 }
110
111 #[inline]
129 #[must_use]
130 pub fn from_f32(r: f32, g: f32, b: f32, a: f32) -> Self {
131 let r_clamped = f32::min(1.0, f32::max(0.0, r));
132 let g_clamped = f32::min(1.0, f32::max(0.0, g));
133 let b_clamped = f32::min(1.0, f32::max(0.0, b));
134 let a_clamped = f32::min(1.0, f32::max(0.0, a));
135 Self {
136 r: r_clamped,
137 g: g_clamped,
138 b: b_clamped,
139 a: a_clamped,
140 }
141 }
142
143 #[inline]
160 #[must_use]
161 pub fn from_u8(r: u8, g: u8, b: u8, a: u8) -> Self {
162 Self {
163 r: f32::from(r) / 255.0,
164 g: f32::from(g) / 255.0,
165 b: f32::from(b) / 255.0,
166 a: f32::from(a) / 255.0,
167 }
168 }
169
170 #[inline]
184 #[must_use]
185 pub fn named(col: (u8, u8, u8)) -> Self {
186 Self::from_u8(col.0, col.1, col.2, 255)
187 }
188
189 #[allow(clippy::cast_precision_loss)]
207 pub fn from_hex<S: AsRef<str>>(code: S) -> Result<Self, HtmlColorConversionError> {
208 let mut full_code = code.as_ref().chars();
209
210 if let Some(hash) = full_code.next() {
211 if hash != '#' {
212 return Err(HtmlColorConversionError::MissingHash);
213 }
214 } else {
215 return Err(HtmlColorConversionError::InvalidStringLength);
216 }
217
218 let red1 = match full_code.next() {
219 Some(red) => match red.to_digit(16) {
220 Some(red) => red * 16,
221 None => return Err(HtmlColorConversionError::InvalidCharacter),
222 },
223 None => return Err(HtmlColorConversionError::InvalidStringLength),
224 };
225 let red2 = match full_code.next() {
226 Some(red) => match red.to_digit(16) {
227 Some(red) => red,
228 None => return Err(HtmlColorConversionError::InvalidCharacter),
229 },
230 None => return Err(HtmlColorConversionError::InvalidStringLength),
231 };
232
233 let green1 = match full_code.next() {
234 Some(green) => match green.to_digit(16) {
235 Some(green) => green * 16,
236 None => return Err(HtmlColorConversionError::InvalidCharacter),
237 },
238 None => return Err(HtmlColorConversionError::InvalidStringLength),
239 };
240 let green2 = match full_code.next() {
241 Some(green) => match green.to_digit(16) {
242 Some(green) => green,
243 None => return Err(HtmlColorConversionError::InvalidCharacter),
244 },
245 None => return Err(HtmlColorConversionError::InvalidStringLength),
246 };
247
248 let blue1 = match full_code.next() {
249 Some(blue) => match blue.to_digit(16) {
250 Some(blue) => blue * 16,
251 None => return Err(HtmlColorConversionError::InvalidCharacter),
252 },
253 None => return Err(HtmlColorConversionError::InvalidStringLength),
254 };
255 let blue2 = match full_code.next() {
256 Some(blue) => match blue.to_digit(16) {
257 Some(blue) => blue,
258 None => return Err(HtmlColorConversionError::InvalidCharacter),
259 },
260 None => return Err(HtmlColorConversionError::InvalidStringLength),
261 };
262
263 let alpha1 = match full_code.next() {
264 Some(alpha) => match alpha.to_digit(16) {
265 Some(alpha) => alpha * 16,
266 None => return Err(HtmlColorConversionError::InvalidCharacter),
267 },
268 None => return Err(HtmlColorConversionError::InvalidStringLength),
269 };
270 let alpha2 = match full_code.next() {
271 Some(alpha) => match alpha.to_digit(16) {
272 Some(alpha) => alpha,
273 None => return Err(HtmlColorConversionError::InvalidCharacter),
274 },
275 None => return Err(HtmlColorConversionError::InvalidStringLength),
276 };
277
278 if full_code.next().is_some() {
279 return Err(HtmlColorConversionError::InvalidStringLength);
280 }
281
282 Ok(Self {
283 r: (red1 + red2) as f32 / 255.0,
284 g: (green1 + green2) as f32 / 255.0,
285 b: (blue1 + blue2) as f32 / 255.0,
286 a: (alpha1 + alpha2) as f32 / 255.0,
287 })
288 }
289
290 #[inline]
292 #[must_use]
293 pub fn to_rgb(&self) -> RGB {
294 RGB::from_f32(self.r, self.g, self.b)
295 }
296
297 #[cfg(feature = "bevy")]
299 #[must_use]
300 pub fn as_rgba_f32(&self) -> [f32; 4] {
301 [self.r, self.g, self.b, self.a]
302 }
303
304 #[inline]
306 #[must_use]
307 pub fn to_greyscale(&self) -> Self {
308 let linear = (self.r * 0.2126) + (self.g * 0.7152) + (self.b * 0.0722);
309 Self::from_f32(linear, linear, linear, self.a)
310 }
311
312 #[inline]
314 #[must_use]
315 pub fn desaturate(&self) -> Self {
316 let mut hsv = self.to_rgb().to_hsv();
317 hsv.s = 0.0;
318 hsv.to_rgb().to_rgba(self.a)
319 }
320
321 #[inline]
323 #[must_use]
324 pub fn lerp(&self, color: Self, percent: f32) -> Self {
325 let range = (
326 color.r - self.r,
327 color.g - self.g,
328 color.b - self.b,
329 color.a - self.a,
330 );
331 Self {
332 r: self.r + range.0 * percent,
333 g: self.g + range.1 * percent,
334 b: self.b + range.2 * percent,
335 a: self.a + range.3 * percent,
336 }
337 }
338
339 #[inline]
341 #[must_use]
342 pub fn lerp_alpha(&self, color: Self, percent: f32) -> Self {
343 let range = color.a - self.a;
344 Self {
345 r: self.r,
346 g: self.g,
347 b: self.b,
348 a: self.a + range * percent,
349 }
350 }
351}
352
353impl From<RGB> for RGBA {
355 fn from(item: RGB) -> Self {
356 Self::from_f32(item.r, item.g, item.b, 1.0)
357 }
358}
359
360impl From<HSV> for RGBA {
362 fn from(item: HSV) -> Self {
363 item.to_rgba(1.0)
364 }
365}
366
367impl From<(u8, u8, u8, u8)> for RGBA {
369 fn from(vals: (u8, u8, u8, u8)) -> Self {
370 Self::from_u8(vals.0, vals.1, vals.2, vals.3)
371 }
372}
373
374impl From<(u8, u8, u8)> for RGBA {
376 fn from(vals: (u8, u8, u8)) -> Self {
377 Self::from_u8(vals.0, vals.1, vals.2, 255)
378 }
379}
380
381impl From<[f32; 4]> for RGBA {
382 fn from(item: [f32; 4]) -> Self {
383 Self::from_f32(item[0], item[1], item[2], item[3])
384 }
385}
386
387#[cfg(feature = "bevy")]
389impl From<bevy::prelude::Color> for RGBA {
390 fn from(item: bevy::prelude::Color) -> Self {
391 Self::from_f32(item.r(), item.g(), item.b(), item.a())
392 }
393}
394
395#[cfg(feature = "bevy")]
396impl From<RGBA> for bevy::prelude::Color {
397 fn from(item: RGBA) -> Self {
398 Self::from([item.r, item.g, item.b, item.a])
399 }
400}
401
402#[cfg(feature = "crossterm")]
403mod crossterm_features {
404 use super::RGBA;
405 use crossterm::style::Color;
406 use std::convert::TryFrom;
407
408 impl TryFrom<RGBA> for Color {
409 type Error = &'static str;
410
411 fn try_from(rgb: RGBA) -> Result<Self, Self::Error> {
412 let (r, g, b) = (rgb.r, rgb.g, rgb.b);
413 for c in [r, g, b].iter() {
414 if *c < 0.0 {
415 return Err("Value < 0.0 found!");
416 }
417 if *c > 1.0 {
418 return Err("Value > 1.0 found!");
419 }
420 }
421 let (r, g, b) = ((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8);
422 let rgb = Color::Rgb { r, g, b };
423 Ok(rgb)
424 }
425 }
426
427 #[cfg(test)]
428 mod tests {
429 use crate::prelude::RGBA;
430 use crossterm::style::Color;
431 use std::convert::TryInto;
432
433 #[test]
434 fn basic_conversion() {
435 let rgb = RGBA {
436 r: 0.0,
437 g: 0.5,
438 b: 1.0,
439 a: 1.0,
440 };
441 let rgb: Color = rgb.try_into().unwrap();
442 match rgb {
443 Color::Rgb { r, g, b } => {
444 assert_eq!(r, 0);
445 assert_eq!(g, 127);
446 assert_eq!(b, 255);
447 }
448 _ => unreachable!(),
449 }
450 }
451
452 #[test]
453 fn negative_rgb() {
454 let rgb = RGBA {
455 r: 0.0,
456 g: 0.5,
457 b: -1.0,
458 a: 1.0,
459 };
460 let rgb: Result<Color, _> = rgb.try_into();
461 assert!(rgb.is_err());
462 }
463
464 #[test]
465 fn too_large_rgb() {
466 let rgb = RGBA {
467 r: 0.0,
468 g: 0.5,
469 b: 1.1,
470 a: 1.0,
471 };
472 let rgb: Result<Color, _> = rgb.try_into();
473 assert!(rgb.is_err());
474 }
475 }
476}
477
478#[cfg(test)]
481mod tests {
482 use crate::prelude::*;
483
484 #[test]
485 fn make_rgba_minimal() {
487 let black = RGBA::new();
488 assert!(black.r < std::f32::EPSILON);
489 assert!(black.g < std::f32::EPSILON);
490 assert!(black.b < std::f32::EPSILON);
491 assert!(black.a < std::f32::EPSILON);
492 }
493
494 #[test]
495 fn convert_olive_to_rgb() {
497 let grey = HSV::from_f32(60.0 / 360.0, 1.0, 0.501_960_8);
498 let rgb = grey.to_rgba(1.0);
499 assert!(f32::abs(rgb.r - 128.0 / 255.0) < std::f32::EPSILON);
500 assert!(f32::abs(rgb.g - 128.0 / 255.0) < std::f32::EPSILON);
501 assert!(rgb.b < std::f32::EPSILON);
502 assert!((rgb.a - 1.0).abs() < std::f32::EPSILON);
503 }
504
505 #[test]
506 fn test_red_hex() {
508 let rgb = RGBA::from_hex("#FF0000FF").expect("Invalid hex string");
509 assert!(f32::abs(rgb.r - 1.0) < std::f32::EPSILON);
510 assert!(rgb.g < std::f32::EPSILON);
511 assert!(rgb.b < std::f32::EPSILON);
512 assert!((rgb.a - 1.0).abs() < std::f32::EPSILON);
513 }
514
515 #[test]
516 fn test_green_hex() {
518 let rgb = RGBA::from_hex("#00FF00FF").expect("Invalid hex string");
519 assert!(rgb.r < std::f32::EPSILON);
520 assert!(f32::abs(rgb.g - 1.0) < std::f32::EPSILON);
521 assert!(rgb.b < std::f32::EPSILON);
522 assert!((rgb.a - 1.0).abs() < std::f32::EPSILON);
523 }
524
525 #[test]
526 fn test_blue_hex() {
528 let rgb = RGBA::from_hex("#0000FFFF").expect("Invalid hex string");
529 assert!(rgb.r < std::f32::EPSILON);
530 assert!(rgb.g < std::f32::EPSILON);
531 assert!(f32::abs(rgb.b - 1.0) < std::f32::EPSILON);
532 assert!((rgb.a - 1.0).abs() < std::f32::EPSILON);
533 }
534
535 #[test]
536 fn test_blue_named() {
538 let rgb = RGBA::named(BLUE);
539 assert!(rgb.r < std::f32::EPSILON);
540 assert!(rgb.g < std::f32::EPSILON);
541 assert!(f32::abs(rgb.b - 1.0) < std::f32::EPSILON);
542 assert!((rgb.a - 1.0).abs() < std::f32::EPSILON);
543 }
544
545 #[test]
546 fn test_lerp() {
548 let black = RGBA::named(BLACK);
549 let white = RGBA::named(WHITE);
550 assert!(black.lerp(white, 0.0) == black);
551 assert!(black.lerp(white, 1.0) == white);
552 }
553
554 #[test]
555 fn test_lerp_alpha() {
557 let black = RGB::named(BLACK).to_rgba(0.0);
558 let white = RGB::named(WHITE).to_rgba(1.0);
559
560 let l0 = black.lerp_alpha(white, 0.0);
561 let l1 = black.lerp_alpha(white, 1.0);
562 assert!(l0.a < std::f32::EPSILON);
563 assert!((l1.a - 1.0).abs() < std::f32::EPSILON);
564
565 assert!(l0.to_rgb() == RGB::named(BLACK));
566 assert!(l1.to_rgb() == RGB::named(BLACK));
567 }
568}