use num_traits::clamp;
#[cfg(feature = "wasm")]
use serde::{Deserialize, Serialize};
use crate::{
color::{oklch::Oklch, XYZ},
math::FloatNumber,
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "wasm", derive(Serialize, Deserialize))]
pub struct Oklab<T = f64>
where
T: FloatNumber,
{
pub l: T,
pub a: T,
pub b: T,
}
impl<T> Oklab<T>
where
T: FloatNumber,
{
pub fn new(l: T, a: T, b: T) -> Self {
Self {
l: clamp(l, T::zero(), T::one()),
a,
b,
}
}
}
impl<T> std::fmt::Display for Oklab<T>
where
T: FloatNumber,
{
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Oklab({:.2}, {:.2}, {:.2})", self.l, self.a, self.b)
}
}
impl<T> From<&XYZ<T>> for Oklab<T>
where
T: FloatNumber,
{
fn from(xyz: &XYZ<T>) -> Self {
let l = T::from_f64(0.818_933_010_1) * xyz.x + T::from_f64(0.361_866_742_4) * xyz.y
- T::from_f64(0.128_859_713_7) * xyz.z;
let m = T::from_f64(0.032_984_543_6) * xyz.x
+ T::from_f64(0.929_311_871_5) * xyz.y
+ T::from_f64(0.036_145_638_7) * xyz.z;
let s = T::from_f64(0.048_200_301_8) * xyz.x
+ T::from_f64(0.264_366_269_1) * xyz.y
+ T::from_f64(0.633_851_707_0) * xyz.z;
let l_prime = l.cbrt();
let m_prime = m.cbrt();
let s_prime = s.cbrt();
let l = T::from_f64(0.210_454_255_3) * l_prime + T::from_f64(0.793_617_785_0) * m_prime
- T::from_f64(0.004_072_046_8) * s_prime;
let a = T::from_f64(1.977_998_495_1) * l_prime - T::from_f64(2.428_592_205_0) * m_prime
+ T::from_f64(0.450_593_709_9) * s_prime;
let b = T::from_f64(0.025_904_037_1) * l_prime + T::from_f64(0.782_771_766_2) * m_prime
- T::from_f64(0.808_675_766_0) * s_prime;
Self::new(l, a, b)
}
}
impl<T> From<&Oklch<T>> for Oklab<T>
where
T: FloatNumber,
{
fn from(oklch: &Oklch<T>) -> Self {
let l = oklch.l;
let c = oklch.c;
let h = oklch.h.to_radians();
let a = c * h.cos();
let b = c * h.sin();
Self::new(l, a, b)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
#[cfg(feature = "wasm")]
use serde_test::{assert_de_tokens, assert_tokens, Token};
use super::*;
use crate::assert_approx_eq;
#[test]
fn test_new() {
let actual: Oklab<f32> = Oklab::new(0.607, -0.118, 0.028);
assert_eq!(
actual,
Oklab {
l: 0.607,
a: -0.118,
b: 0.028
}
);
}
#[test]
#[cfg(feature = "wasm")]
fn test_serialize() {
let oklab = Oklab::new(0.607, -0.118, 0.028);
assert_tokens(
&oklab,
&[
Token::Struct {
name: "Oklab",
len: 3,
},
Token::Str("l"),
Token::F64(0.607),
Token::Str("a"),
Token::F64(-0.118),
Token::Str("b"),
Token::F64(0.028),
Token::StructEnd,
],
);
}
#[test]
#[cfg(feature = "wasm")]
fn test_deserialize() {
let oklab = Oklab::new(0.70, -0.08, 0.05);
assert_de_tokens(
&oklab,
&[
Token::Struct {
name: "Oklab",
len: 3,
},
Token::Str("l"),
Token::F64(0.70),
Token::Str("a"),
Token::F64(-0.08),
Token::Str("b"),
Token::F64(0.05),
Token::StructEnd,
],
);
}
#[test]
fn test_fmt() {
let oklab: Oklab<f32> = Oklab::new(0.607, -0.118, 0.028);
let actual = format!("{}", oklab);
assert_eq!(actual, "Oklab(0.61, -0.12, 0.03)");
}
#[rstest]
#[case((0.147, 0.241, 0.198), (0.607, -0.118, 0.028))]
#[case((0.950, 1.000, 1.089), (1.000, -0.000, 0.000))]
#[case((1.000, 0.000, 0.000), (0.442, 1.215, -0.019))]
#[case((0.000, 1.000, 0.000), (0.922, -0.671, 0.263))]
#[case((0.000, 0.000, 1.000), (0.153, -1.415, -0.449))]
fn test_from_xyz(#[case] xyz: (f32, f32, f32), #[case] expected: (f32, f32, f32)) {
let xyz: XYZ<f32> = XYZ::new(xyz.0, xyz.1, xyz.2);
let actual = Oklab::from(&xyz);
assert_approx_eq!(actual.l, expected.0, 1e-3);
assert_approx_eq!(actual.a, expected.1, 1e-3);
assert_approx_eq!(actual.b, expected.2, 1e-3);
}
#[test]
fn test_from_oklch() {
let oklch: Oklch<f64> = Oklch::new(0.607, 0.121, 166.651);
let actual = Oklab::from(&oklch);
assert_approx_eq!(actual.l, 0.607);
assert_approx_eq!(actual.a, -0.117730);
assert_approx_eq!(actual.b, 0.0279367);
}
}