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}