palette/color_difference.rs
1//! Algorithms for calculating the difference between colors.
2//!
3//! ## Selecting an algorithm
4//!
5//! Different distance/difference algorithms and formulas are good for different
6//! situations. Some are faster but less accurate and some may only be suitable
7//! for certain color spaces. This table may help navigating the options a bit
8//! by summarizing the difference between the traits in this module.
9//!
10//! **Disclaimer:** _This is not an actual benchmark! It's always best to test
11//! and evaluate the differences in an actual application, when possible._
12//!
13//! Property explanations:
14//! - **Complexity:** Low complexity options are generally faster than high
15//! complexity options.
16//! - **Accuracy:** How the numerical difference compares to the perceived
17//! difference. May differ with the color space.
18//!
19//! | Trait | Complexity | Accuracy | Notes |
20//! |-------|------------|----------|-------|
21//! | [`Ciede2000`] | High | High for small differences, lower for large differences | The de-facto standard, but requires complex calculations to compensate for increased errors in certain areas of the CIE L\*a\*b\* (CIELAB) space.
22//! | [`ImprovedCiede2000`] | High | High for small differences, lower for large differences | A general improvement of [`Ciede2000`], using a formula by Huang et al.
23//! | [`DeltaE`] | Usually low | Medium to high | The formula differs between color spaces and may not always be the best. Other formulas, such as [`Ciede2000`], may be preferred for some spaces.
24//! | [`ImprovedDeltaE`] | Usually low to medium | Medium to high | A general improvement of [`DeltaE`], using a formula by Huang et al.
25//! | [`EuclideanDistance`] | Low | Medium to high for perceptually uniform spaces, otherwise low | Can be good enough for perceptually uniform spaces or as a "quick and dirty" check.
26//! | [`HyAb`] | Low | High accuracy for medium to large differences. Less accurate than CIEDE2000 for small differences, but still performs well and is much less computationally expensive. | Similar to Euclidean distance, but separates lightness and chroma more. Limited to Cartesian spaces with a lightness axis and a chroma plane.
27//! | [`Wcag21RelativeContrast`] | Low | Low and only compares lightness | Meant for checking contrasts in computer graphics (such as between text and background colors), assuming sRGB. Mostly useful as a hint or for checking WCAG 2.1 compliance, considering the criticism it has received.
28
29use core::ops::{Add, BitAnd, BitOr, Div, Mul};
30
31use crate::{
32 angle::RealAngle,
33 bool_mask::{HasBoolMask, LazySelect},
34 convert::IntoColorUnclamped,
35 num::{
36 Abs, Arithmetics, Exp, Hypot, MinMax, One, PartialCmp, Powf, Powi, Real, Sqrt,
37 Trigonometry, Zero,
38 },
39 white_point::D65,
40 Lab, Lch, LinLuma,
41};
42
43/// A trait for calculating the color difference between two colors.
44#[deprecated(
45 since = "0.7.2",
46 note = "replaced by `palette::color_difference::Ciede2000`"
47)]
48pub trait ColorDifference {
49 /// The type of the calculated color difference.
50 type Scalar;
51
52 /// Return the difference or distance between two colors.
53 #[must_use]
54 fn get_color_difference(self, other: Self) -> Self::Scalar;
55}
56
57/// Calculate the CIEDE2000 Δ*E\** (Delta E) color difference between two
58/// colors.
59///
60/// CIEDE2000 is a formula by the CIE that calculates a distance metric, Δ*E\**
61/// (also known as Delta E), as an estimate of perceived color distance or
62/// difference. CIEDE2000 is an improvement over Δ*E* (see [`DeltaE`]) for CIE
63/// L\*a\*b\* and CIE L\*C\*h° (see [`Lab`] and [`Lch`]).
64///
65/// There is a "just noticeable difference" between two colors when the Δ*E\**
66/// is roughly greater than 1. Thus, the color difference is more suited for
67/// calculating small distances between colors as opposed to large differences.
68#[doc(alias = "ColorDifference")]
69pub trait Ciede2000 {
70 /// The type for the Δ*E\** (Delta E).
71 type Scalar;
72
73 /// Calculate the CIEDE2000 Δ*E\** (Delta E) color difference between `self` and `other`.
74 #[must_use]
75 fn difference(self, other: Self) -> Self::Scalar;
76}
77
78/// Calculate the CIEDE2000 Δ*E'* (improved IEDE2000 Δ*E\**) color difference.
79///
80/// The "improved CIEDE2000" uses the output of [`Ciede2000`] and enhances it
81/// according to *Power functions improving the performance of color-difference
82/// formulas* by Huang et al.
83pub trait ImprovedCiede2000: Ciede2000 {
84 /// Calculate the CIEDE2000 Δ*E'* (improved IEDE2000 Δ*E\**) color
85 /// difference between `self` and `other`.
86 #[must_use]
87 fn improved_difference(self, other: Self) -> Self::Scalar;
88}
89
90impl<C> ImprovedCiede2000 for C
91where
92 C: Ciede2000,
93 C::Scalar: Real + Mul<C::Scalar, Output = C::Scalar> + Powf,
94{
95 #[inline]
96 fn improved_difference(self, other: Self) -> Self::Scalar {
97 // Coefficients from "Power functions improving the performance of
98 // color-difference formulas" by Huang et al.
99 // https://opg.optica.org/oe/fulltext.cfm?uri=oe-23-1-597&id=307643
100 C::Scalar::from_f64(1.43) * self.difference(other).powf(C::Scalar::from_f64(0.7))
101 }
102}
103
104/// Container of components necessary to calculate CIEDE color difference
105pub(crate) struct LabColorDiff<T> {
106 /// Lab color lightness
107 pub l: T,
108 /// Lab color a* value
109 pub a: T,
110 /// Lab color b* value
111 pub b: T,
112 /// Lab color chroma value
113 pub chroma: T,
114}
115
116impl<Wp, T> From<Lab<Wp, T>> for LabColorDiff<T>
117where
118 T: Hypot + Clone,
119{
120 #[inline]
121 fn from(color: Lab<Wp, T>) -> Self {
122 // Color difference calculation requires Lab and chroma components. This
123 // function handles the conversion into those components which are then
124 // passed to `get_ciede_difference()` where calculation is completed.
125 LabColorDiff {
126 l: color.l,
127 a: color.a.clone(),
128 b: color.b.clone(),
129 chroma: color.a.hypot(color.b),
130 }
131 }
132}
133
134impl<Wp, T> From<Lch<Wp, T>> for LabColorDiff<T>
135where
136 T: Clone,
137 Lch<Wp, T>: IntoColorUnclamped<Lab<Wp, T>>,
138{
139 #[inline]
140 fn from(color: Lch<Wp, T>) -> Self {
141 let chroma = color.chroma.clone();
142 let Lab { l, a, b, .. } = color.into_color_unclamped();
143
144 LabColorDiff { l, a, b, chroma }
145 }
146}
147
148/// Calculate the CIEDE2000 color difference for two colors in Lab color space.
149/// There is a "just noticeable difference" between two colors when the delta E
150/// is roughly greater than 1. Thus, the color difference is more suited for
151/// calculating small distances between colors as opposed to large differences.
152#[rustfmt::skip]
153pub(crate) fn get_ciede2000_difference<T>(this: LabColorDiff<T>, other: LabColorDiff<T>) -> T
154where
155 T: Real
156 + RealAngle
157 + One
158 + Zero
159 + Trigonometry
160 + Abs
161 + Sqrt
162 + Powi
163 + Exp
164 + Arithmetics
165 + PartialCmp
166 + Clone,
167 T::Mask: LazySelect<T> + BitAnd<Output = T::Mask> + BitOr<Output = T::Mask>
168{
169 let c_bar = (this.chroma + other.chroma) / T::from_f64(2.0);
170 let c_bar_pow_seven = c_bar.powi(7);
171 let twenty_five_pow_seven = T::from_f64(6103515625.0);
172 let pi_over_180 = T::from_f64(core::f64::consts::PI / 180.0);
173
174 let g = T::from_f64(0.5)
175 * (T::one() - (c_bar_pow_seven.clone() / (c_bar_pow_seven + &twenty_five_pow_seven)).sqrt());
176 let a_one_prime = this.a * (T::one() + &g);
177 let a_two_prime = other.a * (T::one() + g);
178 let c_one_prime = (a_one_prime.clone() * &a_one_prime + this.b.clone() * &this.b).sqrt();
179 let c_two_prime = (a_two_prime.clone() * &a_two_prime + other.b.clone() * &other.b).sqrt();
180
181 let calc_h_prime = |b: T, a_prime: T| -> T {
182 lazy_select! {
183 if b.eq(&T::zero()) & a_prime.eq(&T::zero()) => T::zero(),
184 else => {
185 let result = T::radians_to_degrees(b.atan2(a_prime));
186 lazy_select! {
187 if result.lt(&T::zero()) => result.clone() + T::from_f64(360.0),
188 else => result.clone(),
189 }
190 },
191 }
192 };
193 let h_one_prime = calc_h_prime(this.b, a_one_prime);
194 let h_two_prime = calc_h_prime(other.b, a_two_prime);
195
196 let h_prime_diff = h_two_prime.clone() - &h_one_prime;
197 let h_prime_abs_diff = h_prime_diff.clone().abs();
198
199 let delta_h_prime: T = lazy_select! {
200 if c_one_prime.eq(&T::zero()) | c_two_prime.eq(&T::zero()) => T::zero(),
201 if h_prime_abs_diff.lt_eq(&T::from_f64(180.0)) => h_prime_diff.clone(),
202 if h_two_prime.lt_eq(&h_one_prime) => h_prime_diff.clone() + T::from_f64(360.0),
203 else => h_prime_diff.clone() - T::from_f64(360.0),
204 };
205
206 let delta_big_h_prime = T::from_f64(2.0)
207 * (c_one_prime.clone() * &c_two_prime).sqrt()
208 * (delta_h_prime / T::from_f64(2.0) * &pi_over_180).sin();
209 let h_prime_sum = h_one_prime + h_two_prime;
210 let h_bar_prime = lazy_select! {
211 if c_one_prime.eq(&T::zero()) | c_two_prime.eq(&T::zero()) => h_prime_sum.clone(),
212 if h_prime_abs_diff.gt(&T::from_f64(180.0)) => {
213 (h_prime_sum.clone() + T::from_f64(360.0)) / T::from_f64(2.0)
214 },
215 else => h_prime_sum.clone() / T::from_f64(2.0),
216 };
217
218 let l_bar = (this.l.clone() + &other.l) / T::from_f64(2.0);
219 let c_bar_prime = (c_one_prime.clone() + &c_two_prime) / T::from_f64(2.0);
220
221 let t: T = T::one()
222 - T::from_f64(0.17) * ((h_bar_prime.clone() - T::from_f64(30.0)) * &pi_over_180).cos()
223 + T::from_f64(0.24) * ((h_bar_prime.clone() * T::from_f64(2.0)) * &pi_over_180).cos()
224 + T::from_f64(0.32) * ((h_bar_prime.clone() * T::from_f64(3.0) + T::from_f64(6.0)) * &pi_over_180).cos()
225 - T::from_f64(0.20) * ((h_bar_prime.clone() * T::from_f64(4.0) - T::from_f64(63.0)) * &pi_over_180).cos();
226 let s_l = T::one()
227 + ((T::from_f64(0.015) * (l_bar.clone() - T::from_f64(50.0)) * (l_bar.clone() - T::from_f64(50.0)))
228 / ((l_bar.clone() - T::from_f64(50.0)) * (l_bar - T::from_f64(50.0)) + T::from_f64(20.0)).sqrt());
229 let s_c = T::one() + T::from_f64(0.045) * &c_bar_prime;
230 let s_h = T::one() + T::from_f64(0.015) * &c_bar_prime * t;
231
232 let delta_theta = T::from_f64(30.0)
233 * (-(((h_bar_prime.clone() - T::from_f64(275.0)) / T::from_f64(25.0))
234 * ((h_bar_prime - T::from_f64(275.0)) / T::from_f64(25.0))))
235 .exp();
236 let c_bar_prime_pow_seven = c_bar_prime.powi(7);
237 let r_c: T = T::from_f64(2.0)
238 * (c_bar_prime_pow_seven.clone() / (c_bar_prime_pow_seven + twenty_five_pow_seven)).sqrt();
239 let r_t = -r_c * (T::from_f64(2.0) * delta_theta * pi_over_180).sin();
240
241 let k_l = T::one();
242 let k_c = T::one();
243 let k_h = T::one();
244 let delta_l_prime = other.l - this.l;
245 let delta_c_prime = c_two_prime - c_one_prime;
246
247 ((delta_l_prime.clone() / (k_l.clone() * &s_l)) * (delta_l_prime / (k_l * s_l))
248 + (delta_c_prime.clone() / (k_c.clone() * &s_c)) * (delta_c_prime.clone() / (k_c.clone() * &s_c))
249 + (delta_big_h_prime.clone() / (k_h.clone() * &s_h)) * (delta_big_h_prime.clone() / (k_h.clone() * &s_h))
250 + (r_t * delta_c_prime * delta_big_h_prime) / (k_c * s_c * k_h * s_h))
251 .sqrt()
252}
253
254/// Calculate the distance between two colors as if they were coordinates in
255/// Euclidean space.
256///
257/// Euclidean distance is not always a good measurement of visual color
258/// difference, depending on the color space. Some spaces, like [`Lab`] and
259/// [`Oklab`][crate::Oklab], will give a fairly uniform result, while other
260/// spaces, such as [`Rgb`][crate::rgb::Rgb], will give much less uniform
261/// results. Despite that, it's still appropriate for some applications.
262pub trait EuclideanDistance: Sized {
263 /// The type for the distance value.
264 type Scalar;
265
266 /// Calculate the Euclidean distance from `self` to `other`.
267 #[must_use]
268 fn distance(self, other: Self) -> Self::Scalar
269 where
270 Self::Scalar: Sqrt,
271 {
272 self.distance_squared(other).sqrt()
273 }
274
275 /// Calculate the squared Euclidean distance from `self` to `other`.
276 ///
277 /// This is typically a faster option than [`Self::distance`] for some
278 /// cases, such as when comparing two distances.
279 #[must_use]
280 fn distance_squared(self, other: Self) -> Self::Scalar;
281}
282
283/// Calculate and check the WCAG 2.1 relative contrast and relative luminance.
284///
285/// W3C's Web Content Accessibility Guidelines (WCAG) 2.1 suggest a method to
286/// calculate accessible contrast ratios of text and background colors for those
287/// with low vision or color vision deficiencies, and for contrast of colors
288/// used in user interface graphics objects.
289///
290/// These criteria come with a couple of caveats:
291/// * sRGB is assumed as the presentation color space, which is why it's only
292/// implemented for a limited set of [`Rgb`][crate::rgb::Rgb] and
293/// [`Luma`][crate::Luma] spaces.
294/// * The contrast ratio is not considered entirely consistent with the
295/// perceived contrast. WCAG 3.x is supposed to provide a better measurement.
296///
297/// Because of the inconsistency with perceived contrast, these methods are more
298/// suitable as hints and for mechanical verification of standards compliance,
299/// than for accurate analysis. Remember to not only rely on the numbers, but to
300/// also test your interfaces with actual people in actual situations for the
301/// best results.
302///
303/// The following example checks the contrast ratio of two colors in sRGB
304/// format:
305///
306/// ```rust
307/// use std::str::FromStr;
308/// use palette::{Srgb, color_difference::Wcag21RelativeContrast};
309/// # fn main() -> Result<(), palette::rgb::FromHexError> {
310///
311/// // the rustdoc "DARK" theme background and text colors
312/// let background: Srgb<f32> = Srgb::from(0x353535).into_format();
313/// let foreground = Srgb::from_str("#ddd")?.into_format();
314///
315/// assert!(background.has_enhanced_contrast_text(foreground));
316/// # Ok(())
317/// # }
318/// ```
319pub trait Wcag21RelativeContrast: Sized {
320 /// The scalar type used for luminance and contrast.
321 type Scalar: Real
322 + Add<Self::Scalar, Output = Self::Scalar>
323 + Div<Self::Scalar, Output = Self::Scalar>
324 + PartialCmp
325 + MinMax;
326
327 /// Returns the WCAG 2.1 [relative
328 /// luminance](https://www.w3.org/TR/WCAG21/#dfn-relative-luminance) of
329 /// `self`.
330 ///
331 /// The relative luminance is a value between 0 and 1, where 0 is the
332 /// darkest black and 1 is the lightest white. This is the same as clamped
333 /// [`LinLuma`], meaning that the typical implementation of this method
334 /// would be `self.into_color()`.
335 #[must_use]
336 fn relative_luminance(self) -> LinLuma<D65, Self::Scalar>;
337
338 /// Returns the WCAG 2.1 relative luminance contrast between `self` and
339 /// `other`.
340 ///
341 /// A return value of, for example, 4 represents a contrast ratio of 4:1
342 /// between the lightest and darkest of the two colors. The range is from
343 /// 1:1 to 21:1, and a higher contrast ratio is generally desirable.
344 ///
345 /// This method is independent of the order of the colors, so
346 /// `a.relative_contrast(b)` and `b.relative_contrast(a)` would return the
347 /// same value.
348 #[must_use]
349 #[inline]
350 fn relative_contrast(self, other: Self) -> Self::Scalar {
351 let (min_luma, max_luma) = self
352 .relative_luminance()
353 .luma
354 .min_max(other.relative_luminance().luma);
355
356 (Self::Scalar::from_f64(0.05) + max_luma) / (Self::Scalar::from_f64(0.05) + min_luma)
357 }
358
359 /// Verify the contrast between two colors satisfies SC 1.4.3. Contrast is
360 /// at least 4.5:1 (Level AA).
361 ///
362 /// This applies for meaningful text, such as body text. Font sizes of 18
363 /// points or lager, or 14 points when bold, are considered large and can be
364 /// checked with
365 /// [`has_min_contrast_large_text`][Wcag21RelativeContrast::has_min_contrast_large_text]
366 /// instead.
367 ///
368 /// [Success Criterion 1.4.3 Contrast (Minimum) (Level
369 /// AA)](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum)
370 #[must_use]
371 #[inline]
372 fn has_min_contrast_text(self, other: Self) -> <Self::Scalar as HasBoolMask>::Mask {
373 self.relative_contrast(other)
374 .gt_eq(&Self::Scalar::from_f64(4.5))
375 }
376
377 /// Verify the contrast between two colors satisfies SC 1.4.3 for large
378 /// text. Contrast is at least 3:1 (Level AA).
379 ///
380 /// This applies for meaningful large text, such as headings. Font sizes of
381 /// 18 points or lager, or 14 points when bold, are considered large.
382 ///
383 /// [Success Criterion 1.4.3 Contrast (Minimum) (Level
384 /// AA)](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum)
385 #[must_use]
386 #[inline]
387 fn has_min_contrast_large_text(self, other: Self) -> <Self::Scalar as HasBoolMask>::Mask {
388 self.relative_contrast(other)
389 .gt_eq(&Self::Scalar::from_f64(3.0))
390 }
391
392 /// Verify the contrast between two colors satisfies SC 1.4.6. Contrast is
393 /// at least 7:1 (Level AAA).
394 ///
395 /// This applies for meaningful text, such as body text. Font sizes of 18
396 /// points or lager, or 14 points when bold, are considered large and can be
397 /// checked with
398 /// [`has_enhanced_contrast_large_text`][Wcag21RelativeContrast::has_enhanced_contrast_large_text]
399 /// instead.
400 ///
401 /// [Success Criterion 1.4.6 Contrast (Enhanced) (Level
402 /// AAA)](https://www.w3.org/WAI/WCAG21/Understanding/contrast-enhanced)
403 #[must_use]
404 #[inline]
405 fn has_enhanced_contrast_text(self, other: Self) -> <Self::Scalar as HasBoolMask>::Mask {
406 self.relative_contrast(other)
407 .gt_eq(&Self::Scalar::from_f64(7.0))
408 }
409
410 /// Verify the contrast between two colors satisfies SC 1.4.6 for large
411 /// text. Contrast is at least 4.5:1 (Level AAA).
412 ///
413 /// This applies for meaningful large text, such as headings. Font sizes of
414 /// 18 points or lager, or 14 points when bold, are considered large.
415 ///
416 /// [Success Criterion 1.4.6 Contrast (Enhanced) (Level
417 /// AAA)](https://www.w3.org/WAI/WCAG21/Understanding/contrast-enhanced)
418 #[must_use]
419 #[inline]
420 fn has_enhanced_contrast_large_text(self, other: Self) -> <Self::Scalar as HasBoolMask>::Mask {
421 self.relative_contrast(other)
422 .gt_eq(&Self::Scalar::from_f64(4.5))
423 }
424
425 /// Verify the contrast between two colors satisfies SC 1.4.11 for graphical
426 /// objects. Contrast is at least 3:1 (Level AA).
427 ///
428 /// This applies for any graphical object that aren't text, such as
429 /// meaningful images and interactive user interface elements.
430 ///
431 /// [Success Criterion 1.4.11 Non-text Contrast (Level
432 /// AA)](https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast.html)
433 #[must_use]
434 #[inline]
435 fn has_min_contrast_graphics(self, other: Self) -> <Self::Scalar as HasBoolMask>::Mask {
436 self.relative_contrast(other)
437 .gt_eq(&Self::Scalar::from_f64(3.0))
438 }
439}
440
441/// Calculate a combination of Euclidean and Manhattan/City-block distance
442/// between two colors.
443///
444/// The HyAB distance was suggested as an alternative to CIEDE2000 for large
445/// color differences in [Distance metrics for very large color
446/// differences](http://markfairchild.org/PDFs/PAP40.pdf) (in [Color Res Appl.
447/// 2019;1–16](https://doi.org/10.1002/col.22451)) by Saeedeh Abasi, Mohammad
448/// Amani Tehran and Mark D. Fairchild. It's originally meant for [CIE L\*a\*b\*
449/// (CIELAB)][crate::Lab], but this trait is also implemented for other color
450/// spaces that have similar semantics, although **without the same quality
451/// guarantees**.
452///
453/// The hybrid distance is the sum of the absolute lightness difference and the
454/// distance on the chroma plane. This makes the lightness and chroma
455/// differences more independent from each other, which is meant to correspond
456/// more to how humans perceive the two qualities.
457pub trait HyAb {
458 /// The type for the distance value.
459 type Scalar;
460
461 /// Calculate the hybrid distance between `self` and `other`.
462 ///
463 /// This returns the sum of the absolute lightness difference and the
464 /// distance on the chroma plane.
465 #[must_use]
466 fn hybrid_distance(self, other: Self) -> Self::Scalar;
467}
468
469/// Calculate the Δ*E* color difference between two colors.
470///
471/// This represents the original Δ*E* formula for a color space. It's often a
472/// Euclidean distance for perceptually uniform color spaces and may not always
473/// be the best option. See the [`color_difference`](self) module for more
474/// details and options.
475pub trait DeltaE {
476 /// The type for the distance value.
477 type Scalar;
478
479 /// Calculate the Δ*E* color difference metric for `self` and `other`,
480 /// according to the color space's specification.
481 #[must_use]
482 fn delta_e(self, other: Self) -> Self::Scalar;
483}
484
485/// Calculate the Δ*E'* (improved Δ*E*) color difference between two colors.
486///
487/// The Δ*E'* uses the output of [`DeltaE`] and enhances it according to *Power
488/// functions improving the performance of color-difference formulas* by Huang
489/// et al. Only spaces with specified coefficients implement this trait.
490pub trait ImprovedDeltaE: DeltaE {
491 /// Calculate the Δ*E'* (improved Δ*E*) color difference metric for `self`
492 /// and `other`, according to the color space's specification and later
493 /// improvements by Huang et al.
494 #[must_use]
495 fn improved_delta_e(self, other: Self) -> Self::Scalar;
496}
497
498#[cfg(feature = "approx")]
499#[cfg(test)]
500mod test {
501 use core::str::FromStr;
502
503 use super::{HyAb, Wcag21RelativeContrast};
504 use crate::{FromColor, Lab, Srgb};
505
506 #[test]
507 fn relative_contrast() {
508 let white = Srgb::new(1.0f32, 1.0, 1.0);
509 let black = Srgb::new(0.0, 0.0, 0.0);
510
511 assert_relative_eq!(white.relative_contrast(white), 1.0);
512 assert_relative_eq!(white.relative_contrast(black), 21.0);
513 assert_relative_eq!(
514 white.relative_contrast(black),
515 black.relative_contrast(white)
516 );
517
518 let c1 = Srgb::from_str("#600").unwrap().into_format();
519
520 assert_relative_eq!(c1.relative_contrast(white), 13.41, epsilon = 0.01);
521 assert_relative_eq!(c1.relative_contrast(black), 1.56, epsilon = 0.01);
522
523 assert!(c1.has_min_contrast_text(white));
524 assert!(c1.has_min_contrast_large_text(white));
525 assert!(c1.has_enhanced_contrast_text(white));
526 assert!(c1.has_enhanced_contrast_large_text(white));
527 assert!(c1.has_min_contrast_graphics(white));
528
529 assert!(!c1.has_min_contrast_text(black));
530 assert!(!c1.has_min_contrast_large_text(black));
531 assert!(!c1.has_enhanced_contrast_text(black));
532 assert!(!c1.has_enhanced_contrast_large_text(black));
533 assert!(!c1.has_min_contrast_graphics(black));
534
535 let c1 = Srgb::from_str("#066").unwrap().into_format();
536
537 assert_relative_eq!(c1.relative_contrast(white), 6.79, epsilon = 0.01);
538 assert_relative_eq!(c1.relative_contrast(black), 3.09, epsilon = 0.01);
539
540 let c1 = Srgb::from_str("#9f9").unwrap().into_format();
541
542 assert_relative_eq!(c1.relative_contrast(white), 1.22, epsilon = 0.01);
543 assert_relative_eq!(c1.relative_contrast(black), 17.11, epsilon = 0.01);
544 }
545
546 #[test]
547 fn hyab() {
548 // From https://github.com/Evercoder/culori/blob/cd1fe08a12fa9ddfcf6b2e82914733d23ac117d0/test/difference.test.js#L186
549 let red = Lab::<_, f64>::from_color(Srgb::from(0xff0000).into_linear());
550 let green = Lab::<_, f64>::from_color(Srgb::from(0x008000).into_linear());
551 assert_relative_eq!(
552 red.hybrid_distance(green),
553 139.93576718451553,
554 epsilon = 0.000001
555 );
556 }
557}