Skip to main content

gpui_kit_material_color/dynamic/
contrast.rs

1// Copyright 2022 Google LLC
2// SPDX-License-Identifier: Apache-2.0
3// Ported to Rust from material-color-utilities (typescript/contrast), modified
4// for this crate. See licenses/material-color-utilities.txt.
5
6//! Contrast ratios between tones, and tones that reach a given ratio.
7//!
8//! The safe functions return `None` where upstream returns -1.
9use super::utils::{clamp_double, lstar_from_y, y_from_lstar};
10
11/// The contrast ratio of two tones, from 1 to 21. Tones are clamped to 0..=100.
12pub fn ratio_of_tones(tone_a: f64, tone_b: f64) -> f64 {
13    ratio_of_ys(
14        y_from_lstar(clamp_double(0., 100., tone_a)),
15        y_from_lstar(clamp_double(0., 100., tone_b)),
16    )
17}
18
19/// The contrast ratio of two relative luminances.
20pub fn ratio_of_ys(y1: f64, y2: f64) -> f64 {
21    let lighter = if y1 > y2 { y1 } else { y2 };
22    let darker = if lighter == y2 { y1 } else { y2 };
23    (lighter + 5.) / (darker + 5.)
24}
25
26/// A tone at least as light as `tone` that reaches `ratio`, if one exists.
27pub fn lighter(tone: f64, ratio: f64) -> Option<f64> {
28    if !(0. ..=100.).contains(&tone) {
29        return None;
30    }
31    let dark_y = y_from_lstar(tone);
32    let light_y = ratio * (dark_y + 5.) - 5.;
33    let real_contrast = ratio_of_ys(light_y, dark_y);
34    if real_contrast < ratio && (real_contrast - ratio).abs() > 0.04 {
35        return None;
36    }
37    // Gamut mapping needs a range on tone, so overshoot slightly.
38    let value = lstar_from_y(light_y) + 0.4;
39    (0. ..=100.).contains(&value).then_some(value)
40}
41
42/// A tone at least as dark as `tone` that reaches `ratio`, if one exists.
43pub fn darker(tone: f64, ratio: f64) -> Option<f64> {
44    if !(0. ..=100.).contains(&tone) {
45        return None;
46    }
47    let light_y = y_from_lstar(tone);
48    let dark_y = (light_y + 5.) / ratio - 5.;
49    let real_contrast = ratio_of_ys(light_y, dark_y);
50    if real_contrast < ratio && (real_contrast - ratio).abs() > 0.04 {
51        return None;
52    }
53    let value = lstar_from_y(dark_y) - 0.4;
54    (0. ..=100.).contains(&value).then_some(value)
55}
56
57/// [`lighter`], or 100 when `ratio` cannot be reached.
58pub fn lighter_unsafe(tone: f64, ratio: f64) -> f64 {
59    lighter(tone, ratio).unwrap_or(100.)
60}
61
62/// [`darker`], or 0 when `ratio` cannot be reached.
63pub fn darker_unsafe(tone: f64, ratio: f64) -> f64 {
64    darker(tone, ratio).unwrap_or(0.)
65}