use std::{fmt::Display, marker::PhantomData};
use num_traits::clamp;
use crate::{
color::{Hue, Lab, WhitePoint, D65},
math::FloatNumber,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LCHab<T, W = D65>
where
T: FloatNumber,
W: WhitePoint,
{
pub l: T,
pub c: T,
pub h: Hue<T>,
_marker: PhantomData<W>,
}
impl<T, W> LCHab<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 LCHab<T, W>
where
T: FloatNumber,
W: WhitePoint,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"LCH(ab)({:.2}, {:.2}, {:.2})",
self.l,
self.c,
self.h.to_degrees()
)
}
}
impl<T, W> From<&Lab<T, W>> for LCHab<T, W>
where
T: FloatNumber,
W: WhitePoint,
{
fn from(lab: &Lab<T, W>) -> Self {
let l = lab.l;
let c = (lab.a.powi(2) + lab.b.powi(2)).sqrt();
let h = lab.b.atan2(lab.a).to_degrees();
Self::new(l, c, h)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let actual = LCHab::<_>::new(54.617, 92.151, 27.756);
assert_eq!(
actual,
LCHab {
l: 54.617,
c: 92.151,
h: Hue::from_degrees(27.756),
_marker: PhantomData,
}
);
}
#[test]
fn test_fmt() {
let lchab = LCHab::<f32>::new(54.617, 92.151, 27.756);
let actual = format!("{}", lchab);
assert_eq!(actual, "LCH(ab)(54.62, 92.15, 27.76)");
}
#[test]
fn test_from_lab() {
let lab: Lab<f32> = Lab::new(54.617, 81.549, 42.915);
let actual = LCHab::from(&lab);
assert_eq!(actual.l, 54.617);
assert!((actual.c - 92.151).abs() < 1e-3);
assert!((actual.h.to_degrees() - 27.756).abs() < 1e-3);
}
}