use std::{
f64::consts::{FRAC_PI_3, TAU},
hint::unreachable_unchecked,
marker::PhantomData,
};
use glam::{dvec3, swizzles::*, DVec3};
use crate::{
color::{Color, ParamColor},
space::{ColorVecSpace, OpponentSpace, Srgb},
};
pub trait Parameterize {
type Value: Sized;
}
pub trait FromParameterized<P: Parameterize + ?Sized>: ColorVecSpace {
fn from_param(param: ParamColor<P>) -> Color<Self>;
}
pub trait IntoParameterized<P: Parameterize + ?Sized>: ColorVecSpace {
fn into_param(clr: Color<Self>) -> ParamColor<P>;
}
pub trait FromColor<S: IntoParameterized<Self>>: Parameterize {
fn from_color(clr: Color<S>) -> ParamColor<Self>;
}
impl<T: Parameterize, S: IntoParameterized<Self>> FromColor<S> for T {
fn from_color(clr: Color<S>) -> ParamColor<Self> { S::into_param(clr) }
}
#[derive(Debug)]
pub struct Hsv;
#[derive(Debug)]
pub struct Lch<S: OpponentSpace>(PhantomData<S>);
#[derive(Debug)]
pub struct Lsh<S: OpponentSpace>(PhantomData<S>);
impl Parameterize for Hsv {
type Value = DVec3;
}
impl<S: OpponentSpace> Parameterize for Lch<S> {
type Value = DVec3;
}
impl<S: OpponentSpace> Parameterize for Lsh<S> {
type Value = DVec3;
}
impl FromParameterized<Hsv> for Srgb {
fn from_param(param: ParamColor<Hsv>) -> Color<Self> {
let hsv = param.into_inner();
let h = hsv.x.to_degrees().rem_euclid(360.0) / 60.0;
let c = hsv.y * hsv.z;
let lmh = dvec3(
hsv.z - c,
hsv.z - (1.0 - h.rem_euclid(2.0)).abs() * c,
hsv.z,
);
#[allow(clippy::cast_possible_truncation)]
{
debug_assert!((0..6).contains(&(h as i16)));
}
Color::new(match unsafe { h.to_int_unchecked() } {
0_u8 => lmh.zyx(), 1 => lmh.yzx(), 2 => lmh.xzy(), 3 => lmh.xyz(), 4 => lmh.yxz(), 5 => lmh.zxy(), _ => unsafe { unreachable_unchecked() },
})
}
}
impl IntoParameterized<Hsv> for Srgb {
fn into_param(clr: Color<Self>) -> ParamColor<Hsv> {
let rgb = clr.into_inner();
let lmh = {
let mut vec = rgb;
vec.as_mut().sort_by(|a, b| a.partial_cmp(b).unwrap());
vec
};
let v = lmh.z;
let c = v - lmh.x;
let mid_pct = if c.abs() > 1e-7 {
(lmh.y - lmh.x) / c
} else {
0.0
};
debug_assert!((0.0..=1.0).contains(&mid_pct), "mid_pct was {:?}", mid_pct);
#[allow(clippy::float_cmp)]
let h = if rgb.x == lmh.z {
if rgb.z == lmh.x {
debug_assert_eq!(rgb.y, lmh.y);
mid_pct
} else {
debug_assert_eq!(rgb.y, lmh.x);
debug_assert_eq!(rgb.z, lmh.y);
6.0 - mid_pct
}
} else if rgb.x == lmh.x {
if rgb.y == lmh.z {
debug_assert_eq!(rgb.z, lmh.y);
2.0 + mid_pct
} else {
debug_assert_eq!(rgb.y, lmh.y);
debug_assert_eq!(rgb.z, lmh.z);
4.0 - mid_pct
}
} else {
debug_assert_eq!(rgb.x, lmh.y);
if rgb.y == lmh.z {
debug_assert_eq!(rgb.z, lmh.x);
2.0 - mid_pct
} else {
debug_assert_eq!(rgb.y, lmh.x);
debug_assert_eq!(rgb.z, lmh.z);
4.0 + mid_pct
}
} * FRAC_PI_3;
debug_assert!((0.0..360.0).contains(&h.to_degrees()));
ParamColor::new(dvec3(h, if v > 1e-7 { c / v } else { 0.0 }, v))
}
}
fn atan3(y: f64, x: f64) -> f64 {
let theta = y.atan2(x);
if theta < 0.0 {
TAU + theta
} else {
theta
}
}
impl<S: OpponentSpace> FromParameterized<Lch<S>> for S {
fn from_param(param: ParamColor<Lch<Self>>) -> Color<Self> {
let lch = param.into_inner();
S::from_lxy(dvec3(lch.x, lch.y * lch.z.cos(), lch.y * lch.z.sin()))
}
}
impl<S: OpponentSpace> IntoParameterized<Lch<S>> for S {
fn into_param(clr: Color<Self>) -> ParamColor<Lch<Self>> {
let lxy = S::into_lxy(clr);
ParamColor::new(dvec3(lxy.x, lxy.yz().length(), atan3(lxy.z, lxy.y)))
}
}
impl<S: OpponentSpace> FromParameterized<Lsh<S>> for S {
fn from_param(param: ParamColor<Lsh<Self>>) -> Color<Self> {
let lsh = param.into_inner();
let c = lsh.x * lsh.y;
S::from_lxy(dvec3(lsh.x, c * lsh.z.cos(), c * lsh.z.sin()))
}
}
impl<S: OpponentSpace> IntoParameterized<Lsh<S>> for S {
fn into_param(clr: Color<Self>) -> ParamColor<Lsh<Self>> {
let lxy = S::into_lxy(clr);
ParamColor::new(dvec3(
lxy.x,
if lxy.x.abs() > 1e-7 {
lxy.yz().length() / lxy.x
} else {
0.0
},
atan3(lxy.z, lxy.y),
))
}
}
#[cfg(test)]
mod test {
use std::f64::consts::SQRT_2;
use proptest::prelude::*;
use super::*;
const EPSILON: f64 = 1e-7;
macro_rules! assert_eqv {
(@prop $a:expr, $b:expr) => {
assert_eqv!(@prop $a, $b, EPSILON);
};
($a:expr, $b:expr) => {
assert_eqv!($a, $b, EPSILON);
};
($a:expr, $b:expr, $max:expr) => {
assert!(
$a.abs_diff_eq($b, $max),
"Vectors were not equal by {}: {:?}, {:?}",
$max,
&$a,
&$b
);
};
(@prop $a:expr, $b:expr, $max:expr) => {
prop_assert!(
$a.abs_diff_eq($b, $max),
"Vectors were not equal by {}: {:?}, {:?}",
$max,
&$a,
&$b
);
};
}
#[derive(Debug)]
struct Lxy;
impl ColorVecSpace for Lxy {
type Vector = DVec3;
}
impl OpponentSpace for Lxy {
fn from_lxy(lxy: DVec3) -> Color<Self> { Color::new(lxy) }
fn into_lxy(clr: Color<Self>) -> DVec3 { clr.into_inner() }
}
fn r(f: f64) -> f64 { f.to_radians() }
#[test]
fn test_hsv() {
type C = Color<Srgb>;
type H = ParamColor<Hsv>;
let red = C::new(dvec3(1.0, 0.0, 0.0));
let green = C::new(dvec3(0.0, 1.0, 0.0));
let blue = C::new(dvec3(0.0, 0.0, 1.0));
let yellow = C::new(dvec3(1.0, 1.0, 0.0));
let cyan = C::new(dvec3(0.0, 1.0, 1.0));
let magenta = C::new(dvec3(1.0, 0.0, 1.0));
let red_h = H::new(dvec3(r(0.0), 1.0, 1.0));
let green_h = H::new(dvec3(r(120.0), 1.0, 1.0));
let blue_h = H::new(dvec3(r(240.0), 1.0, 1.0));
let yellow_h = H::new(dvec3(r(60.0), 1.0, 1.0));
let cyan_h = H::new(dvec3(r(180.0), 1.0, 1.0));
let magenta_h = H::new(dvec3(r(300.0), 1.0, 1.0));
assert_eqv!(red.into_param::<Hsv>(), *red_h);
assert_eqv!(green.into_param::<Hsv>(), *green_h);
assert_eqv!(blue.into_param::<Hsv>(), *blue_h);
assert_eqv!(yellow.into_param::<Hsv>(), *yellow_h);
assert_eqv!(cyan.into_param::<Hsv>(), *cyan_h);
assert_eqv!(magenta.into_param::<Hsv>(), *magenta_h);
assert_eqv!(red_h.into_color::<Srgb>(), *red);
assert_eqv!(green_h.into_color::<Srgb>(), *green);
assert_eqv!(blue_h.into_color::<Srgb>(), *blue);
assert_eqv!(yellow_h.into_color::<Srgb>(), *yellow);
assert_eqv!(cyan_h.into_color::<Srgb>(), *cyan);
assert_eqv!(magenta_h.into_color::<Srgb>(), *magenta);
let redf = C::new(dvec3(0.75, 0.25, 0.25));
let redf_h = H::new(dvec3(r(0.0), 0.5 / 0.75, 0.75));
assert_eqv!(redf.into_param::<Hsv>(), *redf_h);
assert_eqv!(redf_h.into_color::<Srgb>(), *redf);
}
#[test]
fn test_lch_lsh() {
type C = Color<Lxy>;
type Lch = super::Lch<Lxy>;
type Lsh = super::Lsh<Lxy>;
type Lc = ParamColor<Lch>;
type Ls = ParamColor<Lsh>;
let a = C::new(dvec3(1.0, 1.0, 0.0));
let b = C::new(dvec3(1.0, 0.0, 1.0));
let c = C::new(dvec3(0.5, 1.0, -1.0));
let d = C::new(dvec3(0.5, -1.0, 1.0));
let a_lc = Lc::new(dvec3(1.0, 1.0, r(0.0)));
let b_lc = Lc::new(dvec3(1.0, 1.0, r(90.0)));
let c_lc = Lc::new(dvec3(0.5, SQRT_2, r(315.0)));
let d_lc = Lc::new(dvec3(0.5, SQRT_2, r(135.0)));
let a_ls = Ls::new(dvec3(1.0, 1.0, r(0.0)));
let b_ls = Ls::new(dvec3(1.0, 1.0, r(90.0)));
let c_ls = Ls::new(dvec3(0.5, 2.0 * SQRT_2, r(315.0)));
let d_ls = Ls::new(dvec3(0.5, 2.0 * SQRT_2, r(135.0)));
assert_eqv!(a.into_param::<Lch>(), *a_lc);
assert_eqv!(b.into_param::<Lch>(), *b_lc);
assert_eqv!(c.into_param::<Lch>(), *c_lc);
assert_eqv!(d.into_param::<Lch>(), *d_lc);
assert_eqv!(a.into_param::<Lsh>(), *a_ls);
assert_eqv!(b.into_param::<Lsh>(), *b_ls);
assert_eqv!(c.into_param::<Lsh>(), *c_ls);
assert_eqv!(d.into_param::<Lsh>(), *d_ls);
}
proptest! {
#[test]
fn test_hsv_roundtrip(r in 0.0..=1.0, g in 0.0..=1.0, b in 0.0..=1.0) {
let clr = Color::<Srgb>::new(dvec3(r, g, b));
assert_eqv!(@prop clr, *clr.into_param::<Hsv>().into_color::<Srgb>());
}
#[test]
fn test_lch_roundtrip(l in 0.0..=1.0, x in -1.0..=1.0, y in -1.0..=1.0) {
let clr = Color::<Lxy>::new(dvec3(l, x, y));
assert_eqv!(@prop clr, *clr.into_param::<Lch<Lxy>>().into_color::<Lxy>());
}
#[test]
fn test_lch_lsh_correlation(l in 0.0..=1.0, x in -1.0..=1.0, y in -1.0..=1.0) {
let clr = Color::<Lxy>::new(dvec3(l, x, y));
let lch = clr.into_param::<Lch<Lxy>>();
let lsh = clr.into_param::<Lsh<Lxy>>();
assert_eq!(lch.x, clr.x);
assert_eq!(lch.x, lsh.x);
assert!((lsh.x * lsh.y - lch.y).abs() < EPSILON);
assert_eq!(lch.z, lsh.z);
}
}
}