use std::{fmt::Display, marker::PhantomData};
use num_traits::clamp;
#[cfg(feature = "wasm")]
use serde::{Deserialize, Serialize};
use crate::{
color::{Hue, Luv, WhitePoint, D65},
math::FloatNumber,
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "wasm", derive(Serialize, Deserialize))]
pub struct LCHuv<T = f64, W = D65>
where
T: FloatNumber,
W: WhitePoint,
{
pub l: T,
pub c: T,
pub h: Hue<T>,
#[cfg_attr(feature = "wasm", serde(skip))]
_marker: PhantomData<W>,
}
impl<T, W> LCHuv<T, W>
where
T: FloatNumber,
W: WhitePoint,
{
#[must_use]
pub fn new(l: T, c: T, h: T) -> Self {
Self {
l: clamp(l, T::zero(), T::from_u32(100)),
c: clamp(c, T::zero(), T::from_u32(180)),
h: Hue::from_degrees(h),
_marker: PhantomData,
}
}
}
impl<T, W> Display for LCHuv<T, W>
where
T: FloatNumber,
W: WhitePoint,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"LCH(uv)({:.2}, {:.2}, {:.2})",
self.l,
self.c,
self.h.to_degrees()
)
}
}
impl<T, W> From<&Luv<T, W>> for LCHuv<T, W>
where
T: FloatNumber,
W: WhitePoint,
{
fn from(luv: &Luv<T, W>) -> Self {
let l = luv.l;
let c = (luv.u * luv.u + luv.v * luv.v).sqrt();
let h = luv.v.atan2(luv.u).to_degrees();
Self::new(l, c, h)
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "wasm")]
use serde_test::{assert_de_tokens, assert_ser_tokens, Token};
use super::*;
use crate::{assert_approx_eq, color::Luv};
#[test]
fn test_new() {
let actual: LCHuv<_> = LCHuv::new(56.232, 50.875, 154.710);
assert_eq!(
actual,
LCHuv {
l: 56.232,
c: 50.875,
h: Hue::from_degrees(154.710),
_marker: PhantomData,
}
);
}
#[test]
#[cfg(feature = "wasm")]
fn test_serialize() {
let lchuv: LCHuv<f64> = LCHuv::new(56.232, 50.875, 154.710);
assert_ser_tokens(
&lchuv,
&[
Token::Struct {
name: "LCHuv",
len: 3,
},
Token::Str("l"),
Token::F64(56.232),
Token::Str("c"),
Token::F64(50.875),
Token::Str("h"),
Token::F64(154.710),
Token::StructEnd,
],
)
}
#[test]
#[cfg(feature = "wasm")]
fn test_deserialize() {
let lchuv: LCHuv<f64> = LCHuv::new(56.232, 50.875, 154.710);
assert_de_tokens(
&lchuv,
&[
Token::Struct {
name: "LCHuv",
len: 3,
},
Token::Str("l"),
Token::F64(56.232),
Token::Str("c"),
Token::F64(50.875),
Token::Str("h"),
Token::F64(154.710),
Token::StructEnd,
],
)
}
#[test]
fn test_fmt() {
let lchuv: LCHuv<_> = LCHuv::new(56.232, 50.875, 154.710);
let actual = format!("{}", lchuv);
assert_eq!(actual, "LCH(uv)(56.23, 50.88, 154.71)");
}
#[test]
fn test_from_luv() {
let luv: Luv<f32> = Luv::new(56.232, -45.999, 21.734);
let actual = LCHuv::from(&luv);
assert_approx_eq!(actual.l, 56.231998);
assert_approx_eq!(actual.c, 50.875087);
assert_approx_eq!(actual.h.to_degrees(), 154.709808);
}
}