New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement WCAG contrast ratio calculations #164
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<f32> = 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<T>(luma1: T, luma2: T) -> T | ||
where | ||
T: Add<Output = T>, | ||
T: Div<Output = T>, | ||
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); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An alternative could be to force this to be linear. But I'm not sure that's the way to go. Maybe it's better to go the opposite way this time and implement this trait for anything that can be converted to linear
Luma
. I think a blanket implementation would be possible.