use std::fmt::Display;
use num_traits::clamp;
#[cfg(feature = "wasm")]
use serde::{Deserialize, Serialize};
use crate::{
color::{hue::Hue, rgb::RGB},
math::FloatNumber,
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "wasm", derive(Serialize, Deserialize))]
pub struct HSL<T = f64>
where
T: FloatNumber,
{
pub h: Hue<T>,
pub s: T,
pub l: T,
}
impl<T> HSL<T>
where
T: FloatNumber,
{
#[must_use]
pub fn new(h: T, s: T, l: T) -> Self {
Self {
h: Hue::from_degrees(h),
s: clamp(s, T::zero(), T::one()),
l: clamp(l, T::zero(), T::one()),
}
}
}
impl<T> Display for HSL<T>
where
T: FloatNumber,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "HSL({:.2}, {:.2}, {:.2})", self.h, self.s, self.l)
}
}
impl<T> From<&RGB> for HSL<T>
where
T: FloatNumber,
{
fn from(rgb: &RGB) -> Self {
let max = RGB::max_value::<T>();
let r = T::from_u8(rgb.r) / max;
let g = T::from_u8(rgb.g) / max;
let b = T::from_u8(rgb.b) / max;
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let delta = max - min;
let h = if delta.is_zero() {
T::zero()
} else if max == r {
T::from_f32(60.0) * (((g - b) / delta) % T::from_f32(6.0))
} else if max == g {
T::from_f32(60.0) * (((b - r) / delta) + T::from_f32(2.0))
} else {
T::from_f32(60.0) * (((r - g) / delta) + T::from_f32(4.0))
};
let l = (max + min) / T::from_f32(2.0);
let s = if delta.is_zero() {
T::zero()
} else {
delta / (T::one() - (T::from_f32(2.0) * l - T::one()).abs())
};
HSL::new(h, s, l)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
#[cfg(feature = "wasm")]
use serde_test::{assert_de_tokens, assert_ser_tokens, Token};
use super::*;
use crate::assert_approx_eq;
#[test]
fn test_new() {
let actual = HSL::new(60.0, 1.0, 0.5);
assert_eq!(
actual,
HSL {
h: Hue::from_degrees(60.0),
s: 1.0,
l: 0.5,
}
);
}
#[rstest]
#[case((-400.0, -1.0, -1.0), (320.0, 0.0, 0.0))]
#[case((400.0, 2.0, 2.0), (40.0, 1.0, 1.0))]
fn test_new_with_out_of_range(
#[case] input: (f32, f32, f32),
#[case] expected: (f32, f32, f32),
) {
let (h, s, l) = input;
let actual = HSL::new(h, s, l);
let (h, s, l) = expected;
assert_eq!(actual, HSL::new(h, s, l));
}
#[test]
#[cfg(feature = "wasm")]
fn test_serialize() {
let hsl = HSL::new(150.0, 0.3, 0.6);
assert_ser_tokens(
&hsl,
&[
Token::Struct {
name: "HSL",
len: 3,
},
Token::Str("h"),
Token::F64(150.0),
Token::Str("s"),
Token::F64(0.3),
Token::Str("l"),
Token::F64(0.6),
Token::StructEnd,
],
)
}
#[test]
#[cfg(feature = "wasm")]
fn test_deserialize() {
let hsl = HSL::new(120.0, 0.5, 0.8);
assert_de_tokens(
&hsl,
&[
Token::Struct {
name: "HSL",
len: 3,
},
Token::Str("h"),
Token::F64(120.0),
Token::Str("s"),
Token::F64(0.5),
Token::Str("l"),
Token::F64(0.8),
Token::StructEnd,
],
)
}
#[test]
fn test_fmt() {
let hsl = HSL::new(60.0, 1.0, 0.5);
let actual = format!("{}", hsl);
assert_eq!("HSL(60.00, 1.00, 0.50)", actual);
}
#[rstest]
#[case::black((0, 0, 0), (0.0, 0.0, 0.0))]
#[case::white((255, 255, 255), (0.0, 0.0, 1.0))]
#[case::red((255, 0, 0), (0.0, 1.0, 0.5))]
#[case::green((0, 255, 0), (120.0, 1.0, 0.5))]
#[case::blue((0, 0, 255), (240.0, 1.0, 0.5))]
#[case::yellow((255, 255, 0), (60.0, 1.0, 0.5))]
#[case::cyan((0, 255, 255), (180.0, 1.0, 0.5))]
#[case::magenta((255, 0, 255), (300.0, 1.0, 0.5))]
fn test_from_rgb(#[case] rgb: (u8, u8, u8), #[case] expected: (f32, f32, f32)) {
let rgb = RGB::new(rgb.0, rgb.1, rgb.2);
let actual = HSL::<f32>::from(&rgb);
assert_approx_eq!(actual.h.to_degrees(), expected.0, 1e-3);
assert_approx_eq!(actual.s, expected.1, 1e-3);
assert_approx_eq!(actual.l, expected.2, 1e-3);
}
}