use core::ops::{Add, BitAnd, BitOr, Div, Mul};
use crate::{
angle::RealAngle,
bool_mask::{HasBoolMask, LazySelect},
convert::IntoColorUnclamped,
num::{
Abs, Arithmetics, Exp, Hypot, MinMax, One, PartialCmp, Powf, Powi, Real, Sqrt,
Trigonometry, Zero,
},
white_point::D65,
Lab, Lch, LinLuma,
};
#[deprecated(
since = "0.7.2",
note = "replaced by `palette::color_difference::Ciede2000`"
)]
pub trait ColorDifference {
type Scalar;
#[must_use]
fn get_color_difference(self, other: Self) -> Self::Scalar;
}
#[doc(alias = "ColorDifference")]
pub trait Ciede2000 {
type Scalar;
#[must_use]
fn difference(self, other: Self) -> Self::Scalar;
}
pub trait ImprovedCiede2000: Ciede2000 {
#[must_use]
fn improved_difference(self, other: Self) -> Self::Scalar;
}
impl<C> ImprovedCiede2000 for C
where
C: Ciede2000,
C::Scalar: Real + Mul<C::Scalar, Output = C::Scalar> + Powf,
{
#[inline]
fn improved_difference(self, other: Self) -> Self::Scalar {
C::Scalar::from_f64(1.43) * self.difference(other).powf(C::Scalar::from_f64(0.7))
}
}
pub(crate) struct LabColorDiff<T> {
pub l: T,
pub a: T,
pub b: T,
pub chroma: T,
}
impl<Wp, T> From<Lab<Wp, T>> for LabColorDiff<T>
where
T: Hypot + Clone,
{
#[inline]
fn from(color: Lab<Wp, T>) -> Self {
LabColorDiff {
l: color.l,
a: color.a.clone(),
b: color.b.clone(),
chroma: color.a.hypot(color.b),
}
}
}
impl<Wp, T> From<Lch<Wp, T>> for LabColorDiff<T>
where
T: Clone,
Lch<Wp, T>: IntoColorUnclamped<Lab<Wp, T>>,
{
#[inline]
fn from(color: Lch<Wp, T>) -> Self {
let chroma = color.chroma.clone();
let Lab { l, a, b, .. } = color.into_color_unclamped();
LabColorDiff { l, a, b, chroma }
}
}
#[rustfmt::skip]
pub(crate) fn get_ciede2000_difference<T>(this: LabColorDiff<T>, other: LabColorDiff<T>) -> T
where
T: Real
+ RealAngle
+ One
+ Zero
+ Trigonometry
+ Abs
+ Sqrt
+ Powi
+ Exp
+ Arithmetics
+ PartialCmp
+ Clone,
T::Mask: LazySelect<T> + BitAnd<Output = T::Mask> + BitOr<Output = T::Mask>
{
let c_bar = (this.chroma + other.chroma) / T::from_f64(2.0);
let c_bar_pow_seven = c_bar.powi(7);
let twenty_five_pow_seven = T::from_f64(6103515625.0);
let pi_over_180 = T::from_f64(core::f64::consts::PI / 180.0);
let g = T::from_f64(0.5)
* (T::one() - (c_bar_pow_seven.clone() / (c_bar_pow_seven + &twenty_five_pow_seven)).sqrt());
let a_one_prime = this.a * (T::one() + &g);
let a_two_prime = other.a * (T::one() + g);
let c_one_prime = (a_one_prime.clone() * &a_one_prime + this.b.clone() * &this.b).sqrt();
let c_two_prime = (a_two_prime.clone() * &a_two_prime + other.b.clone() * &other.b).sqrt();
let calc_h_prime = |b: T, a_prime: T| -> T {
lazy_select! {
if b.eq(&T::zero()) & a_prime.eq(&T::zero()) => T::zero(),
else => {
let result = T::radians_to_degrees(b.atan2(a_prime));
lazy_select! {
if result.lt(&T::zero()) => result.clone() + T::from_f64(360.0),
else => result.clone(),
}
},
}
};
let h_one_prime = calc_h_prime(this.b, a_one_prime);
let h_two_prime = calc_h_prime(other.b, a_two_prime);
let h_prime_diff = h_two_prime.clone() - &h_one_prime;
let h_prime_abs_diff = h_prime_diff.clone().abs();
let delta_h_prime: T = lazy_select! {
if c_one_prime.eq(&T::zero()) | c_two_prime.eq(&T::zero()) => T::zero(),
if h_prime_abs_diff.lt_eq(&T::from_f64(180.0)) => h_prime_diff.clone(),
if h_two_prime.lt_eq(&h_one_prime) => h_prime_diff.clone() + T::from_f64(360.0),
else => h_prime_diff.clone() - T::from_f64(360.0),
};
let delta_big_h_prime = T::from_f64(2.0)
* (c_one_prime.clone() * &c_two_prime).sqrt()
* (delta_h_prime / T::from_f64(2.0) * &pi_over_180).sin();
let h_prime_sum = h_one_prime + h_two_prime;
let h_bar_prime = lazy_select! {
if c_one_prime.eq(&T::zero()) | c_two_prime.eq(&T::zero()) => h_prime_sum.clone(),
if h_prime_abs_diff.gt(&T::from_f64(180.0)) => {
(h_prime_sum.clone() + T::from_f64(360.0)) / T::from_f64(2.0)
},
else => h_prime_sum.clone() / T::from_f64(2.0),
};
let l_bar = (this.l.clone() + &other.l) / T::from_f64(2.0);
let c_bar_prime = (c_one_prime.clone() + &c_two_prime) / T::from_f64(2.0);
let t: T = T::one()
- T::from_f64(0.17) * ((h_bar_prime.clone() - T::from_f64(30.0)) * &pi_over_180).cos()
+ T::from_f64(0.24) * ((h_bar_prime.clone() * T::from_f64(2.0)) * &pi_over_180).cos()
+ T::from_f64(0.32) * ((h_bar_prime.clone() * T::from_f64(3.0) + T::from_f64(6.0)) * &pi_over_180).cos()
- T::from_f64(0.20) * ((h_bar_prime.clone() * T::from_f64(4.0) - T::from_f64(63.0)) * &pi_over_180).cos();
let s_l = T::one()
+ ((T::from_f64(0.015) * (l_bar.clone() - T::from_f64(50.0)) * (l_bar.clone() - T::from_f64(50.0)))
/ ((l_bar.clone() - T::from_f64(50.0)) * (l_bar - T::from_f64(50.0)) + T::from_f64(20.0)).sqrt());
let s_c = T::one() + T::from_f64(0.045) * &c_bar_prime;
let s_h = T::one() + T::from_f64(0.015) * &c_bar_prime * t;
let delta_theta = T::from_f64(30.0)
* (-(((h_bar_prime.clone() - T::from_f64(275.0)) / T::from_f64(25.0))
* ((h_bar_prime - T::from_f64(275.0)) / T::from_f64(25.0))))
.exp();
let c_bar_prime_pow_seven = c_bar_prime.powi(7);
let r_c: T = T::from_f64(2.0)
* (c_bar_prime_pow_seven.clone() / (c_bar_prime_pow_seven + twenty_five_pow_seven)).sqrt();
let r_t = -r_c * (T::from_f64(2.0) * delta_theta * pi_over_180).sin();
let k_l = T::one();
let k_c = T::one();
let k_h = T::one();
let delta_l_prime = other.l - this.l;
let delta_c_prime = c_two_prime - c_one_prime;
((delta_l_prime.clone() / (k_l.clone() * &s_l)) * (delta_l_prime / (k_l * s_l))
+ (delta_c_prime.clone() / (k_c.clone() * &s_c)) * (delta_c_prime.clone() / (k_c.clone() * &s_c))
+ (delta_big_h_prime.clone() / (k_h.clone() * &s_h)) * (delta_big_h_prime.clone() / (k_h.clone() * &s_h))
+ (r_t * delta_c_prime * delta_big_h_prime) / (k_c * s_c * k_h * s_h))
.sqrt()
}
pub trait EuclideanDistance: Sized {
type Scalar;
#[must_use]
fn distance(self, other: Self) -> Self::Scalar
where
Self::Scalar: Sqrt,
{
self.distance_squared(other).sqrt()
}
#[must_use]
fn distance_squared(self, other: Self) -> Self::Scalar;
}
pub trait Wcag21RelativeContrast: Sized {
type Scalar: Real
+ Add<Self::Scalar, Output = Self::Scalar>
+ Div<Self::Scalar, Output = Self::Scalar>
+ PartialCmp
+ MinMax;
#[must_use]
fn relative_luminance(self) -> LinLuma<D65, Self::Scalar>;
#[must_use]
#[inline]
fn relative_contrast(self, other: Self) -> Self::Scalar {
let (min_luma, max_luma) = self
.relative_luminance()
.luma
.min_max(other.relative_luminance().luma);
(Self::Scalar::from_f64(0.05) + max_luma) / (Self::Scalar::from_f64(0.05) + min_luma)
}
#[must_use]
#[inline]
fn has_min_contrast_text(self, other: Self) -> <Self::Scalar as HasBoolMask>::Mask {
self.relative_contrast(other)
.gt_eq(&Self::Scalar::from_f64(4.5))
}
#[must_use]
#[inline]
fn has_min_contrast_large_text(self, other: Self) -> <Self::Scalar as HasBoolMask>::Mask {
self.relative_contrast(other)
.gt_eq(&Self::Scalar::from_f64(3.0))
}
#[must_use]
#[inline]
fn has_enhanced_contrast_text(self, other: Self) -> <Self::Scalar as HasBoolMask>::Mask {
self.relative_contrast(other)
.gt_eq(&Self::Scalar::from_f64(7.0))
}
#[must_use]
#[inline]
fn has_enhanced_contrast_large_text(self, other: Self) -> <Self::Scalar as HasBoolMask>::Mask {
self.relative_contrast(other)
.gt_eq(&Self::Scalar::from_f64(4.5))
}
#[must_use]
#[inline]
fn has_min_contrast_graphics(self, other: Self) -> <Self::Scalar as HasBoolMask>::Mask {
self.relative_contrast(other)
.gt_eq(&Self::Scalar::from_f64(3.0))
}
}
pub trait HyAb {
type Scalar;
#[must_use]
fn hybrid_distance(self, other: Self) -> Self::Scalar;
}
pub trait DeltaE {
type Scalar;
#[must_use]
fn delta_e(self, other: Self) -> Self::Scalar;
}
pub trait ImprovedDeltaE: DeltaE {
#[must_use]
fn improved_delta_e(self, other: Self) -> Self::Scalar;
}
#[cfg(feature = "approx")]
#[cfg(test)]
mod test {
use core::str::FromStr;
use super::{HyAb, Wcag21RelativeContrast};
use crate::{FromColor, Lab, Srgb};
#[test]
fn relative_contrast() {
let white = Srgb::new(1.0f32, 1.0, 1.0);
let black = Srgb::new(0.0, 0.0, 0.0);
assert_relative_eq!(white.relative_contrast(white), 1.0);
assert_relative_eq!(white.relative_contrast(black), 21.0);
assert_relative_eq!(
white.relative_contrast(black),
black.relative_contrast(white)
);
let c1 = Srgb::from_str("#600").unwrap().into_format();
assert_relative_eq!(c1.relative_contrast(white), 13.41, epsilon = 0.01);
assert_relative_eq!(c1.relative_contrast(black), 1.56, epsilon = 0.01);
assert!(c1.has_min_contrast_text(white));
assert!(c1.has_min_contrast_large_text(white));
assert!(c1.has_enhanced_contrast_text(white));
assert!(c1.has_enhanced_contrast_large_text(white));
assert!(c1.has_min_contrast_graphics(white));
assert!(!c1.has_min_contrast_text(black));
assert!(!c1.has_min_contrast_large_text(black));
assert!(!c1.has_enhanced_contrast_text(black));
assert!(!c1.has_enhanced_contrast_large_text(black));
assert!(!c1.has_min_contrast_graphics(black));
let c1 = Srgb::from_str("#066").unwrap().into_format();
assert_relative_eq!(c1.relative_contrast(white), 6.79, epsilon = 0.01);
assert_relative_eq!(c1.relative_contrast(black), 3.09, epsilon = 0.01);
let c1 = Srgb::from_str("#9f9").unwrap().into_format();
assert_relative_eq!(c1.relative_contrast(white), 1.22, epsilon = 0.01);
assert_relative_eq!(c1.relative_contrast(black), 17.11, epsilon = 0.01);
}
#[test]
fn hyab() {
let red = Lab::<_, f64>::from_color(Srgb::from(0xff0000).into_linear());
let green = Lab::<_, f64>::from_color(Srgb::from(0x008000).into_linear());
assert_relative_eq!(
red.hybrid_distance(green),
139.93576718451553,
epsilon = 0.000001
);
}
}