diff --git a/palette/src/hsl.rs b/palette/src/hsl.rs index 2fbbd7845..8f3e96bf5 100644 --- a/palette/src/hsl.rs +++ b/palette/src/hsl.rs @@ -8,9 +8,11 @@ use core::ops::{Add, AddAssign, Sub, SubAssign}; use encoding::pixel::RawPixel; use encoding::{Linear, Srgb}; use rgb::{Rgb, RgbSpace}; +use {clamp, contrast_ratio, from_f64}; +use {Alpha, Hsv, RgbHue, Xyz}; use { - clamp, from_f64, Alpha, Component, FloatComponent, FromColor, FromF64, GetHue, Hsv, Hue, - IntoColor, Limited, Mix, Pixel, RgbHue, Saturate, Shade, Xyz, + Component, FloatComponent, FromColor, FromF64, GetHue, Hue, IntoColor, Limited, Mix, Pixel, + RelativeContrast, Saturate, Shade, }; /// Linear HSL with an alpha component. See the [`Hsla` implementation in @@ -622,6 +624,21 @@ where } } +impl RelativeContrast for Hsl +where + T: FloatComponent, + S: RgbSpace, +{ + type Scalar = T; + + fn get_contrast_ratio(&self, other: &Self) -> T { + let luma1 = self.into_luma(); + let luma2 = other.into_luma(); + + contrast_ratio(luma1.luma, luma2.luma) + } +} + #[cfg(test)] mod test { use super::Hsl; diff --git a/palette/src/hsv.rs b/palette/src/hsv.rs index 7296c5f03..8d7e4572f 100644 --- a/palette/src/hsv.rs +++ b/palette/src/hsv.rs @@ -8,11 +8,11 @@ use core::ops::{Add, AddAssign, Sub, SubAssign}; use encoding::pixel::RawPixel; use encoding::{Linear, Srgb}; use rgb::{Rgb, RgbSpace}; -use {clamp, from_f64}; -use {Alpha, Hsl, Hwb, Xyz}; +use {clamp, contrast_ratio, from_f64}; +use {Alpha, Hsl, Hwb, RgbHue, Xyz}; use { - Component, FloatComponent, FromColor, FromF64, GetHue, Hue, Limited, Mix, Pixel, RgbHue, - Saturate, Shade, + Component, FloatComponent, FromColor, FromF64, GetHue, Hue, IntoColor, Limited, Mix, Pixel, + RelativeContrast, Saturate, Shade, }; /// Linear HSV with an alpha component. See the [`Hsva` implementation in @@ -637,6 +637,21 @@ where } } +impl RelativeContrast for Hsv +where + T: FloatComponent, + S: RgbSpace, +{ + type Scalar = T; + + fn get_contrast_ratio(&self, other: &Self) -> T { + let luma1 = self.into_luma(); + let luma2 = other.into_luma(); + + contrast_ratio(luma1.luma, luma2.luma) + } +} + #[cfg(test)] mod test { use super::Hsv; diff --git a/palette/src/hwb.rs b/palette/src/hwb.rs index 042b47319..a4033c26d 100644 --- a/palette/src/hwb.rs +++ b/palette/src/hwb.rs @@ -8,9 +8,11 @@ use core::ops::{Add, AddAssign, Sub, SubAssign}; use encoding::pixel::RawPixel; use encoding::Srgb; use rgb::RgbSpace; +use {clamp, contrast_ratio}; +use {Alpha, Hsv, RgbHue, Xyz}; use { - clamp, Alpha, Component, FloatComponent, FromColor, FromF64, GetHue, Hsv, Hue, IntoColor, - Limited, Mix, Pixel, RgbHue, Shade, Xyz, + Component, FloatComponent, FromColor, FromF64, GetHue, Hue, IntoColor, Limited, Mix, Pixel, + RelativeContrast, Shade, }; /// Linear HWB with an alpha component. See the [`Hwba` implementation in @@ -583,6 +585,21 @@ where } } +impl RelativeContrast for Hwb +where + T: FloatComponent, + S: RgbSpace, +{ + type Scalar = T; + + fn get_contrast_ratio(&self, other: &Self) -> T { + let luma1 = self.into_luma(); + let luma2 = other.into_luma(); + + contrast_ratio(luma1.luma, luma2.luma) + } +} + #[cfg(test)] mod test { use super::Hwb; diff --git a/palette/src/lab.rs b/palette/src/lab.rs index d837cafc3..e6a6eefa4 100644 --- a/palette/src/lab.rs +++ b/palette/src/lab.rs @@ -3,9 +3,12 @@ use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; use encoding::pixel::RawPixel; use white_point::{WhitePoint, D65}; -use {clamp, from_f64}; +use {clamp, contrast_ratio, from_f64}; use {Alpha, LabHue, Lch, Xyz}; -use {Component, ComponentWise, FloatComponent, GetHue, Limited, Mix, Pixel, Shade}; +use { + Component, ComponentWise, FloatComponent, GetHue, IntoColor, Limited, Mix, Pixel, + RelativeContrast, Shade, +}; use color_difference::ColorDifference; use color_difference::{get_ciede_difference, LabColorDiff}; @@ -624,6 +627,21 @@ where } } +impl RelativeContrast for Lab +where + Wp: WhitePoint, + T: FloatComponent, +{ + type Scalar = T; + + fn get_contrast_ratio(&self, other: &Self) -> T { + let luma1 = self.into_luma(); + let luma2 = other.into_luma(); + + contrast_ratio(luma1.luma, luma2.luma) + } +} + #[cfg(test)] mod test { use super::Lab; diff --git a/palette/src/lch.rs b/palette/src/lch.rs index 140a633ca..f596f4a36 100644 --- a/palette/src/lch.rs +++ b/palette/src/lch.rs @@ -5,10 +5,11 @@ use color_difference::ColorDifference; use color_difference::{get_ciede_difference, LabColorDiff}; use encoding::pixel::RawPixel; use white_point::{WhitePoint, D65}; -use {clamp, from_f64}; -use {Alpha, Hue, Lab, LabHue, Xyz}; +use {clamp, contrast_ratio, from_f64}; +use {Alpha, Lab, LabHue, Xyz}; use { - Component, FloatComponent, FromColor, GetHue, IntoColor, Limited, Mix, Pixel, Saturate, Shade, + Component, FloatComponent, FromColor, GetHue, Hue, IntoColor, Limited, Mix, Pixel, + RelativeContrast, Saturate, Shade, }; /// CIE L\*C\*h° with an alpha component. See the [`Lcha` implementation in @@ -517,6 +518,21 @@ where } } +impl RelativeContrast for Lch +where + Wp: WhitePoint, + T: FloatComponent, +{ + type Scalar = T; + + fn get_contrast_ratio(&self, other: &Self) -> T { + let luma1 = self.into_luma(); + let luma2 = other.into_luma(); + + contrast_ratio(luma1.luma, luma2.luma) + } +} + #[cfg(test)] mod test { use white_point::D65; diff --git a/palette/src/lib.rs b/palette/src/lib.rs index 8f80a886e..705ef3d5b 100644 --- a/palette/src/lib.rs +++ b/palette/src/lib.rs @@ -189,6 +189,7 @@ pub use convert::{ConvertFrom, ConvertInto, FromColor, IntoColor, OutOfBounds}; pub use encoding::pixel::Pixel; pub use hues::{LabHue, RgbHue}; pub use matrix::Mat3; +pub use relative_contrast::{contrast_ratio, RelativeContrast}; //Helper macro for checking ranges and clamping. #[cfg(test)] @@ -371,6 +372,7 @@ mod convert; pub mod encoding; mod equality; mod matrix; +mod relative_contrast; pub mod white_point; pub mod float; diff --git a/palette/src/luma/luma.rs b/palette/src/luma/luma.rs index 35dc64075..c46e71e65 100644 --- a/palette/src/luma/luma.rs +++ b/palette/src/luma/luma.rs @@ -5,16 +5,16 @@ use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; use approx::{AbsDiffEq, RelativeEq, UlpsEq}; use blend::PreAlpha; -use clamp; use encoding::linear::LinearFn; use encoding::pixel::RawPixel; use encoding::{Linear, Srgb, TransferFn}; use luma::LumaStandard; use white_point::WhitePoint; +use {clamp, contrast_ratio}; use {Alpha, Xyz, Yxy}; use { Blend, Component, ComponentWise, FloatComponent, FromColor, FromComponent, IntoColor, Limited, - Mix, Pixel, Shade, + Mix, Pixel, RelativeContrast, Shade, }; /// Luminance with an alpha component. See the [`Lumaa` implementation @@ -725,6 +725,21 @@ where } } +impl RelativeContrast for Luma +where + T: FloatComponent, + S: LumaStandard, +{ + type Scalar = T; + + fn get_contrast_ratio(&self, other: &Self) -> T { + let luma1 = self.into_luma(); + let luma2 = other.into_luma(); + + contrast_ratio(luma1.luma, luma2.luma) + } +} + #[cfg(test)] mod test { use encoding::Srgb; diff --git a/palette/src/relative_contrast.rs b/palette/src/relative_contrast.rs new file mode 100644 index 000000000..66fed869f --- /dev/null +++ b/palette/src/relative_contrast.rs @@ -0,0 +1,147 @@ +use component::Component; +use core::ops::{Add, Div}; +use {from_f64, FromF64}; + +/// A trait for calculating relative contrast between two colors. +/// +/// W3C's Web Content Accessibility Guidelines (WCAG) 2.1 suggest a method +/// to calculate accessible contrast ratios of text and background colors for +/// those with low vision or color deficiencies, and for contrast of colors used +/// in user interface graphics objects. +/// +/// These criteria are recommendations, not hard and fast rules. Most +/// importantly, look at the colors in action and make sure they're clear and +/// comfortable to read. A pair of colors may pass contrast guidelines but still +/// be uncomfortable to look at. Favor readability over only satisfying the +/// contrast ratio metric. It is recommended to verify the contrast ratio +/// in the output format of the colors and not to assume the contrast ratio +/// remains exactly the same across color formats. The following example checks +/// the contrast ratio of two colors in RGB format. +/// +/// ```rust +/// #[macro_use] +/// extern crate approx; +/// +/// use std::str::FromStr; +/// use palette::{Srgb, RelativeContrast}; +/// +/// fn main() { +/// // the rustdoc "DARK" theme background and text colors +/// let my_background_rgb: Srgb = Srgb::from_str("#353535").unwrap().into_format(); +/// let my_foreground_rgb = Srgb::from_str("#ddd").unwrap().into_format(); +/// +/// assert!(my_background_rgb.has_enhanced_contrast_text(&my_foreground_rgb)); +/// } +/// ``` +/// +/// The possible range of contrast ratios is from 1:1 to 21:1. There is a +/// Success Criterion for Contrast (Minimum) and a Success Criterion for +/// Contrast (Enhanced), SC 1.4.3 and SC 1.4.6 respectively, which are concerned +/// with text and images of text. SC 1.4.11 is a Success Criterion for "non-text +/// contrast" such as user interface components and other graphics. The relative +/// contrast is calculated by `(L1 + 0.05) / (L2 + 0.05)`, where `L1` is the +/// luminance of the brighter color and `L2` is the luminance of the darker +/// color both in sRGB linear space. A higher contrast ratio is generally +/// desireable. +/// +/// For more details, visit the following links: +/// +/// [Success Criterion 1.4.3 Contrast (Minimum) (Level AA)](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum) +/// +/// [Success Criterion 1.4.6 Contrast (Enhanced) (Level AAA)](https://www.w3.org/WAI/WCAG21/Understanding/contrast-enhanced) +/// +/// [Success Criterion 1.4.11 Non-text Contrast (Level AA)](https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast.html) + +pub trait RelativeContrast { + /// The type of the contrast ratio. + type Scalar: FromF64 + PartialOrd; + + /// Calculate contrast ratio between two colors. + fn get_contrast_ratio(&self, other: &Self) -> Self::Scalar; + /// Verify the contrast between two colors satisfies SC 1.4.3. Contrast + /// is at least 4.5:1 (Level AA). + fn has_min_contrast_text(&self, other: &Self) -> bool { + self.get_contrast_ratio(other) >= from_f64(4.5) + } + /// Verify the contrast between two colors satisfies SC 1.4.3 for large + /// text. Contrast is at least 3:1 (Level AA). + fn has_min_contrast_large_text(&self, other: &Self) -> bool { + self.get_contrast_ratio(other) >= from_f64(3.0) + } + /// Verify the contrast between two colors satisfies SC 1.4.6. Contrast + /// is at least 7:1 (Level AAA). + fn has_enhanced_contrast_text(&self, other: &Self) -> bool { + self.get_contrast_ratio(other) >= from_f64(7.0) + } + /// Verify the contrast between two colors satisfies SC 1.4.6 for large + /// text. Contrast is at least 4.5:1 (Level AAA). + fn has_enhanced_contrast_large_text(&self, other: &Self) -> bool { + self.has_min_contrast_text(other) + } + /// Verify the contrast between two colors satisfies SC 1.4.11 for graphical + /// objects. Contrast is at least 3:1 (Level AA). + fn has_min_contrast_graphics(&self, other: &Self) -> bool { + self.has_min_contrast_large_text(other) + } +} + +/// Calculate a ratio between two `luma` values. +pub fn contrast_ratio(luma1: T, luma2: T) -> T +where + T: Add, + T: Div, + T: FromF64, + T: Component, +{ + if luma1 > luma2 { + (luma1 + from_f64(0.05)) / (luma2 + from_f64(0.05)) + } else { + (luma2 + from_f64(0.05)) / (luma1 + from_f64(0.05)) + } +} + +#[cfg(test)] +mod test { + use core::str::FromStr; + use RelativeContrast; + use Srgb; + + #[test] + fn relative_contrast() { + let white = Srgb::new(1.0, 1.0, 1.0); + let black = Srgb::new(0.0, 0.0, 0.0); + + assert_relative_eq!(white.get_contrast_ratio(&white), 1.0); + assert_relative_eq!(white.get_contrast_ratio(&black), 21.0); + assert_relative_eq!( + white.get_contrast_ratio(&black), + black.get_contrast_ratio(&white) + ); + + let c1 = Srgb::from_str("#600").unwrap().into_format(); + + assert_relative_eq!(c1.get_contrast_ratio(&white), 13.41, epsilon = 0.01); + assert_relative_eq!(c1.get_contrast_ratio(&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) == false); + assert!(c1.has_min_contrast_large_text(&black) == false); + assert!(c1.has_enhanced_contrast_text(&black) == false); + assert!(c1.has_enhanced_contrast_large_text(&black) == false); + assert!(c1.has_min_contrast_graphics(&black) == false); + + let c1 = Srgb::from_str("#066").unwrap().into_format(); + + assert_relative_eq!(c1.get_contrast_ratio(&white), 6.79, epsilon = 0.01); + assert_relative_eq!(c1.get_contrast_ratio(&black), 3.09, epsilon = 0.01); + + let c1 = Srgb::from_str("#9f9").unwrap().into_format(); + + assert_relative_eq!(c1.get_contrast_ratio(&white), 1.22, epsilon = 0.01); + assert_relative_eq!(c1.get_contrast_ratio(&black), 17.11, epsilon = 0.01); + } +} diff --git a/palette/src/rgb/rgb.rs b/palette/src/rgb/rgb.rs index 4f79323a4..f0f0add69 100644 --- a/palette/src/rgb/rgb.rs +++ b/palette/src/rgb/rgb.rs @@ -17,10 +17,10 @@ use luma::LumaStandard; use matrix::{matrix_inverse, multiply_xyz_to_rgb, rgb_to_xyz_matrix}; use rgb::{RgbSpace, RgbStandard, TransferFn}; use white_point::WhitePoint; -use {clamp, from_f64}; +use {clamp, contrast_ratio, from_f64}; use { Blend, Component, ComponentWise, FloatComponent, FromComponent, GetHue, Limited, Mix, Pixel, - Shade, + RelativeContrast, Shade, }; use {Hsl, Hsv, Hwb, Lab, Lch, Luma, RgbHue, Xyz, Yxy}; @@ -1041,6 +1041,21 @@ impl FromStr for Rgb { } } +impl RelativeContrast for Rgb +where + T: FloatComponent, + S: RgbStandard, +{ + type Scalar = T; + + fn get_contrast_ratio(&self, other: &Self) -> T { + let luma1 = self.into_luma(); + let luma2 = other.into_luma(); + + contrast_ratio(luma1.luma, luma2.luma) + } +} + #[cfg(test)] mod test { use super::Rgb; diff --git a/palette/src/xyz.rs b/palette/src/xyz.rs index 670d8bc0c..6ded18233 100644 --- a/palette/src/xyz.rs +++ b/palette/src/xyz.rs @@ -6,9 +6,9 @@ use luma::LumaStandard; use matrix::{multiply_rgb_to_xyz, rgb_to_xyz_matrix}; use rgb::{Rgb, RgbSpace, RgbStandard}; use white_point::{WhitePoint, D65}; -use {clamp, from_f64}; +use {clamp, contrast_ratio, from_f64}; use {Alpha, Lab, Luma, Yxy}; -use {Component, ComponentWise, FloatComponent, Limited, Mix, Pixel, Shade}; +use {Component, ComponentWise, FloatComponent, Limited, Mix, Pixel, RelativeContrast, Shade}; /// CIE 1931 XYZ with an alpha component. See the [`Xyza` implementation in /// `Alpha`](struct.Alpha.html#Xyza). @@ -600,6 +600,18 @@ where } } +impl RelativeContrast for Xyz +where + Wp: WhitePoint, + T: FloatComponent, +{ + type Scalar = T; + + fn get_contrast_ratio(&self, other: &Self) -> T { + contrast_ratio(self.y, other.y) + } +} + #[cfg(test)] mod test { use super::Xyz; diff --git a/palette/src/yxy.rs b/palette/src/yxy.rs index b414660d8..04c09735e 100644 --- a/palette/src/yxy.rs +++ b/palette/src/yxy.rs @@ -1,12 +1,15 @@ use core::marker::PhantomData; use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; -use clamp; use encoding::pixel::RawPixel; use luma::LumaStandard; use white_point::{WhitePoint, D65}; +use {clamp, contrast_ratio}; use {Alpha, Luma, Xyz}; -use {Component, ComponentWise, FloatComponent, IntoColor, Limited, Mix, Pixel, Shade}; +use { + Component, ComponentWise, FloatComponent, IntoColor, Limited, Mix, Pixel, RelativeContrast, + Shade, +}; /// CIE 1931 Yxy (xyY) with an alpha component. See the [`Yxya` implementation /// in `Alpha`](struct.Alpha.html#Yxya). @@ -566,6 +569,18 @@ where } } +impl RelativeContrast for Yxy +where + Wp: WhitePoint, + T: FloatComponent, +{ + type Scalar = T; + + fn get_contrast_ratio(&self, other: &Self) -> T { + contrast_ratio(self.luma, other.luma) + } +} + #[cfg(test)] mod test { use super::Yxy;