palette/
relative_contrast.rs

1use crate::{
2    bool_mask::{HasBoolMask, LazySelect},
3    num::{Arithmetics, PartialCmp, Real},
4};
5
6/// A trait for calculating relative contrast between two colors.
7///
8/// W3C's Web Content Accessibility Guidelines (WCAG) 2.1 suggest a method
9/// to calculate accessible contrast ratios of text and background colors for
10/// those with low vision or color deficiencies, and for contrast of colors used
11/// in user interface graphics objects.
12///
13/// These criteria are recommendations, not hard and fast rules. Most
14/// importantly, look at the colors in action and make sure they're clear and
15/// comfortable to read. A pair of colors may pass contrast guidelines but still
16/// be uncomfortable to look at. Favor readability over only satisfying the
17/// contrast ratio metric. It is recommended to verify the contrast ratio
18/// in the output format of the colors and not to assume the contrast ratio
19/// remains exactly the same across color formats. The following example checks
20/// the contrast ratio of two colors in RGB format.
21///
22/// ```rust
23/// use std::str::FromStr;
24/// use palette::{Srgb, RelativeContrast};
25/// # fn main() -> Result<(), palette::rgb::FromHexError> {
26///
27/// // the rustdoc "DARK" theme background and text colors
28/// let background: Srgb<f32> = Srgb::from(0x353535).into_format();
29/// let foreground = Srgb::from_str("#ddd")?.into_format();
30///
31/// assert!(background.has_enhanced_contrast_text(foreground));
32/// # Ok(())
33/// # }
34/// ```
35///
36/// The possible range of contrast ratios is from 1:1 to 21:1. There is a
37/// Success Criterion for Contrast (Minimum) and a Success Criterion for
38/// Contrast (Enhanced), SC 1.4.3 and SC 1.4.6 respectively, which are concerned
39/// with text and images of text. SC 1.4.11 is a Success Criterion for "non-text
40/// contrast" such as user interface components and other graphics. The relative
41/// contrast is calculated by `(L1 + 0.05) / (L2 + 0.05)`, where `L1` is the
42/// luminance of the brighter color and `L2` is the luminance of the darker
43/// color both in sRGB linear space. A higher contrast ratio is generally
44/// desirable.
45///
46/// For more details, visit the following links:
47///
48/// [Success Criterion 1.4.3 Contrast (Minimum) (Level AA)](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum)
49///
50/// [Success Criterion 1.4.6 Contrast (Enhanced) (Level AAA)](https://www.w3.org/WAI/WCAG21/Understanding/contrast-enhanced)
51///
52/// [Success Criterion 1.4.11 Non-text Contrast (Level AA)](https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast.html)
53#[doc(alias = "wcag")]
54#[deprecated(
55    since = "0.7.2",
56    note = "replaced by `palette::color_difference::Wcag21RelativeContrast`"
57)]
58pub trait RelativeContrast: Sized {
59    /// The type of the contrast ratio.
60    type Scalar: Real + PartialCmp;
61
62    /// Calculate the contrast ratio between two colors.
63    #[must_use]
64    fn get_contrast_ratio(self, other: Self) -> Self::Scalar;
65
66    /// Verify the contrast between two colors satisfies SC 1.4.3. Contrast
67    /// is at least 4.5:1 (Level AA).
68    #[must_use]
69    #[inline]
70    fn has_min_contrast_text(self, other: Self) -> <Self::Scalar as HasBoolMask>::Mask {
71        self.get_contrast_ratio(other)
72            .gt_eq(&Self::Scalar::from_f64(4.5))
73    }
74
75    /// Verify the contrast between two colors satisfies SC 1.4.3 for large
76    /// text. Contrast is at least 3:1 (Level AA).
77    #[must_use]
78    #[inline]
79    fn has_min_contrast_large_text(self, other: Self) -> <Self::Scalar as HasBoolMask>::Mask {
80        self.get_contrast_ratio(other)
81            .gt_eq(&Self::Scalar::from_f64(3.0))
82    }
83
84    /// Verify the contrast between two colors satisfies SC 1.4.6. Contrast
85    /// is at least 7:1 (Level AAA).
86    #[must_use]
87    #[inline]
88    fn has_enhanced_contrast_text(self, other: Self) -> <Self::Scalar as HasBoolMask>::Mask {
89        self.get_contrast_ratio(other)
90            .gt_eq(&Self::Scalar::from_f64(7.0))
91    }
92
93    /// Verify the contrast between two colors satisfies SC 1.4.6 for large
94    /// text. Contrast is at least 4.5:1 (Level AAA).
95    #[must_use]
96    #[inline]
97    fn has_enhanced_contrast_large_text(self, other: Self) -> <Self::Scalar as HasBoolMask>::Mask {
98        self.has_min_contrast_text(other)
99    }
100
101    /// Verify the contrast between two colors satisfies SC 1.4.11 for graphical
102    /// objects. Contrast is at least 3:1 (Level AA).
103    #[must_use]
104    #[inline]
105    fn has_min_contrast_graphics(self, other: Self) -> <Self::Scalar as HasBoolMask>::Mask {
106        self.has_min_contrast_large_text(other)
107    }
108}
109
110/// Calculate the ratio between two `luma` values.
111#[inline]
112#[deprecated(
113    since = "0.7.2",
114    note = "replaced by `LinLuma::relative_contrast`, via `Wcag21RelativeContrast`"
115)]
116pub fn contrast_ratio<T>(luma1: T, luma2: T) -> T
117where
118    T: Real + Arithmetics + PartialCmp,
119    T::Mask: LazySelect<T>,
120{
121    lazy_select! {
122        if luma1.gt(&luma2) => (T::from_f64(0.05) + &luma1) / (T::from_f64(0.05) + &luma2),
123        else => (T::from_f64(0.05) + &luma2) / (T::from_f64(0.05) + &luma1)
124    }
125}
126
127#[cfg(feature = "approx")]
128#[cfg(test)]
129#[allow(deprecated)]
130mod test {
131    use core::str::FromStr;
132
133    use crate::RelativeContrast;
134    use crate::Srgb;
135
136    #[test]
137    fn relative_contrast() {
138        let white = Srgb::new(1.0f32, 1.0, 1.0);
139        let black = Srgb::new(0.0, 0.0, 0.0);
140
141        assert_relative_eq!(white.get_contrast_ratio(white), 1.0);
142        assert_relative_eq!(white.get_contrast_ratio(black), 21.0);
143        assert_relative_eq!(
144            white.get_contrast_ratio(black),
145            black.get_contrast_ratio(white)
146        );
147
148        let c1 = Srgb::from_str("#600").unwrap().into_format();
149
150        assert_relative_eq!(c1.get_contrast_ratio(white), 13.41, epsilon = 0.01);
151        assert_relative_eq!(c1.get_contrast_ratio(black), 1.56, epsilon = 0.01);
152
153        assert!(c1.has_min_contrast_text(white));
154        assert!(c1.has_min_contrast_large_text(white));
155        assert!(c1.has_enhanced_contrast_text(white));
156        assert!(c1.has_enhanced_contrast_large_text(white));
157        assert!(c1.has_min_contrast_graphics(white));
158
159        assert!(!c1.has_min_contrast_text(black));
160        assert!(!c1.has_min_contrast_large_text(black));
161        assert!(!c1.has_enhanced_contrast_text(black));
162        assert!(!c1.has_enhanced_contrast_large_text(black));
163        assert!(!c1.has_min_contrast_graphics(black));
164
165        let c1 = Srgb::from_str("#066").unwrap().into_format();
166
167        assert_relative_eq!(c1.get_contrast_ratio(white), 6.79, epsilon = 0.01);
168        assert_relative_eq!(c1.get_contrast_ratio(black), 3.09, epsilon = 0.01);
169
170        let c1 = Srgb::from_str("#9f9").unwrap().into_format();
171
172        assert_relative_eq!(c1.get_contrast_ratio(white), 1.22, epsilon = 0.01);
173        assert_relative_eq!(c1.get_contrast_ratio(black), 17.11, epsilon = 0.01);
174    }
175}