use std::{fmt, fmt::Display};
#[cfg(feature = "wasm")]
use serde::{Deserialize, Serialize};
use crate::{
color::{hsl::HSL, xyz::XYZ, HSV},
math::FloatNumber,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "wasm", derive(Serialize, Deserialize))]
pub struct RGB {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl RGB {
const MAX: u8 = 255;
#[must_use]
pub fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
#[inline]
#[must_use]
pub(crate) fn max_value<T>() -> T
where
T: FloatNumber,
{
T::from_u8(Self::MAX)
}
}
impl Display for RGB {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "RGB({}, {}, {})", self.r, self.g, self.b)
}
}
impl<T> From<&HSL<T>> for RGB
where
T: FloatNumber,
{
fn from(hsl: &HSL<T>) -> Self {
let hue = hsl.h.to_degrees();
let c = (T::one() - (T::from_u8(2) * hsl.l - T::one()).abs()) * hsl.s;
let x = (T::one() - (((hue / T::from_f32(60.0)) % T::from_u8(2)) - T::one()).abs()) * c;
let m = hsl.l - (c / T::from_u8(2));
let (r, g, b) = hue_to_rgb(hue, c, x);
Self {
r: clamp_to_u8(r + m),
g: clamp_to_u8(g + m),
b: clamp_to_u8(b + m),
}
}
}
impl<T> From<&HSV<T>> for RGB
where
T: FloatNumber,
{
fn from(hsv: &HSV<T>) -> Self {
let hue = hsv.h.to_degrees();
let c = hsv.v * hsv.s;
let x = (T::one() - (((hue / T::from_f32(60.0)) % T::from_u8(2)) - T::one()).abs()) * c;
let m = hsv.v - c;
let (r, g, b) = hue_to_rgb(hue, c, x);
Self {
r: clamp_to_u8(r + m),
g: clamp_to_u8(g + m),
b: clamp_to_u8(b + m),
}
}
}
impl<T> From<&XYZ<T>> for RGB
where
T: FloatNumber,
{
fn from(xyz: &XYZ<T>) -> Self {
let (r, g, b) = xyz_to_rgb(xyz.x, xyz.y, xyz.z);
Self { r, g, b }
}
}
#[inline]
#[must_use]
fn clamp_to_u8<T>(value: T) -> u8
where
T: FloatNumber,
{
let max = T::from_u8(RGB::MAX);
(value * max).round().trunc_to_u8()
}
#[inline]
#[must_use]
fn hue_to_rgb<T>(hue: T, c: T, x: T) -> (T, T, T)
where
T: FloatNumber,
{
if hue < T::from_f32(60.0) {
(c, x, T::zero())
} else if hue < T::from_f32(120.0) {
(x, c, T::zero())
} else if hue < T::from_f32(180.0) {
(T::zero(), c, x)
} else if hue < T::from_f32(240.0) {
(T::zero(), x, c)
} else if hue < T::from_f32(300.0) {
(x, T::zero(), c)
} else {
(c, T::zero(), x)
}
}
#[inline]
#[must_use]
pub fn xyz_to_rgb<T>(x: T, y: T, z: T) -> (u8, u8, u8)
where
T: FloatNumber,
{
let f = |t: T| -> T {
if t > T::from_f32(0.003_130_8) {
T::from_f32(1.055) * t.powf(T::from_f32(1.0 / 2.4)) - T::from_f32(0.055)
} else {
T::from_f32(12.92) * t
}
};
let r = f(T::from_f32(3.240_97) * x - T::from_f32(1.537_383) * y - T::from_f32(0.498_611) * z);
let g =
f(-T::from_f32(0.969_244) * x + T::from_f32(1.875_968) * y + T::from_f32(0.041_555) * z);
let b = f(T::from_f32(0.055_630) * x - T::from_f32(0.203_977) * y + T::from_f32(1.056_972) * z);
(clamp_to_u8(r), clamp_to_u8(g), clamp_to_u8(b))
}
#[cfg(test)]
mod tests {
use rstest::rstest;
#[cfg(feature = "wasm")]
use serde_test::{assert_de_tokens, assert_ser_tokens, Token};
use super::*;
#[test]
fn test_new() {
let actual = RGB::new(255, 0, 64);
assert_eq!(actual.r, 255);
assert_eq!(actual.g, 0);
assert_eq!(actual.b, 64);
}
#[test]
#[cfg(feature = "wasm")]
fn test_serialize() {
let rgb = RGB::new(255, 0, 64);
assert_ser_tokens(
&rgb,
&[
Token::Struct {
name: "RGB",
len: 3,
},
Token::Str("r"),
Token::U8(255),
Token::Str("g"),
Token::U8(0),
Token::Str("b"),
Token::U8(64),
Token::StructEnd,
],
);
}
#[test]
#[cfg(feature = "wasm")]
fn test_deserialize() {
let rgb = RGB::new(64, 255, 0);
assert_de_tokens(
&rgb,
&[
Token::Struct {
name: "RGB",
len: 3,
},
Token::Str("r"),
Token::U8(64),
Token::Str("g"),
Token::U8(255),
Token::Str("b"),
Token::U8(0),
Token::StructEnd,
],
);
}
#[test]
fn test_fmt() {
let rgb = RGB::new(255, 0, 64);
let actual = format!("{}", rgb);
assert_eq!(actual, "RGB(255, 0, 64)");
}
#[rstest]
#[case::black((0.0, 0.0, 0.0), (0, 0, 0))]
#[case::white((0.0, 0.0, 1.0), (255, 255, 255))]
#[case::red((0.0, 1.0, 0.5), (255, 0, 0))]
#[case::green((120.0, 1.0, 0.5), (0, 255, 0))]
#[case::blue((240.0, 1.0, 0.5), (0, 0, 255))]
#[case::yellow((60.0, 1.0, 0.5), (255, 255, 0))]
#[case::cyan((180.0, 1.0, 0.5), (0, 255, 255))]
#[case::magenta((300.0, 1.0, 0.5), (255, 0, 255))]
fn test_from_hsl(#[case] hsl: (f32, f32, f32), #[case] rgb: (u8, u8, u8)) {
let (h, s, l) = hsl;
let actual = RGB::from(&HSL::new(h, s, l));
assert_eq!(actual.r, rgb.0);
assert_eq!(actual.g, rgb.1);
assert_eq!(actual.b, rgb.2);
}
#[rstest]
#[case::black((0.0, 0.0, 0.0), (0, 0, 0))]
#[case::white((0.0, 0.0, 1.0), (255, 255, 255))]
#[case::red((0.0, 1.0, 1.0), (255, 0, 0))]
#[case::green((120.0, 1.0, 1.0), (0, 255, 0))]
#[case::blue((240.0, 1.0, 1.0), (0, 0, 255))]
#[case::yellow((60.0, 1.0, 1.0), (255, 255, 0))]
#[case::cyan((180.0, 1.0, 1.0), (0, 255, 255))]
#[case::magenta((300.0, 1.0, 1.0), (255, 0, 255))]
fn test_from_hsv(#[case] hsv: (f32, f32, f32), #[case] rgb: (u8, u8, u8)) {
let (h, s, v) = hsv;
let actual = RGB::from(&HSV::new(h, s, v));
assert_eq!(actual.r, rgb.0);
assert_eq!(actual.g, rgb.1);
assert_eq!(actual.b, rgb.2);
}
#[test]
fn test_from_xyz() {
let xyz = XYZ::new(0.3576, 0.7152, 0.119);
let actual = RGB::from(&xyz);
assert_eq!(actual.r, 0);
assert_eq!(actual.g, 255);
assert_eq!(actual.b, 0);
}
#[rstest]
#[case::black((0.0, 0.0, 0.0), (0, 0, 0))]
#[case::white((0.9505, 1.0000, 1.0886), (255, 255, 255))]
#[case::red((0.4125, 0.2127, 0.0193), (255, 0, 0))]
#[case::green((0.3576, 0.7152, 0.1192), (0, 255, 0))]
#[case::blue((0.1804, 0.0722, 0.9502), (0, 0, 255))]
#[case::cyan((0.53802, 0.7873, 1.0698), (0, 255, 255))]
#[case::magenta((0.5928, 0.2848, 0.9699), (255, 0, 255))]
#[case::yellow((0.7700, 0.9278, 0.1385), (255, 255, 0))]
fn test_xyz_to_rgb(#[case] xyz: (f32, f32, f32), #[case] rgb: (u8, u8, u8)) {
let (r, g, b) = xyz_to_rgb(xyz.0, xyz.1, xyz.2);
assert_eq!(r, rgb.0);
assert_eq!(g, rgb.1);
assert_eq!(b, rgb.2);
}
}