contrasted/
lib.rs

1// pub const LUMINANCE_THRESHOLD: f64 = 0.03928;
2pub const LUMINANCE_THRESHOLD: f64 = 0.04045;
3pub const MINIMUM_CONTRAST_THRESHOLD: f64 = 4.5;
4pub const MINIMUM_CONTRAST_THRESHOLD_LARGE_TEXT: f64 = 3.0;
5
6// Y = 0.2126R + 0.7152G + 0.0722B
7// Y: relative luminance
8pub const RED: f64 = 0.2126;
9pub const GREEN: f64 = 0.7152;
10pub const BLUE: f64 = 0.0722;
11pub const GAMMA: f64 = 2.4;
12
13macro_rules! hex_u8 {
14    ($hex:ident) => {{
15        let c1 = $hex.next().unwrap_or('f');
16        let c2 = $hex.next().unwrap_or('f');
17
18        let mut c = String::new();
19        c.push(c1);
20        c.push(c2);
21
22        match u8::from_str_radix(&c, 16) {
23            Ok(u) => u,
24            Err(_) => 0,
25        }
26    }};
27}
28
29/// An RGB color representation.
30#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord)]
31pub struct Color(pub(crate) u8, pub(crate) u8, pub(crate) u8);
32
33impl From<&str> for Color {
34    fn from(value: &str) -> Self {
35        if value.starts_with("#") {
36            Self::from_hex(value)
37        } else if value.starts_with("rgb(") {
38            Self::from_css_rgb(value)
39        } else {
40            Self(0, 0, 0)
41        }
42    }
43}
44
45impl Color {
46    /// Get a color from a CSS rgb function.
47    pub fn from_css_rgb(rgb: &str) -> Self {
48        let chars = rgb.chars().into_iter().skip(4);
49        let mut color = Self(0, 0, 0);
50        let mut color_str = String::new();
51        let mut idx: usize = 0;
52
53        for char in chars {
54            if char == ' ' {
55                continue;
56            }
57
58            if char == ')' {
59                break;
60            }
61
62            if char == ',' {
63                if idx == 0 {
64                    color.0 = color_str.parse::<u8>().unwrap_or(0);
65                } else if idx == 1 {
66                    color.1 = color_str.parse::<u8>().unwrap_or(0);
67                } else {
68                    color.2 = color_str.parse::<u8>().unwrap_or(0);
69                }
70
71                idx += 1;
72                color_str = String::new();
73            } else {
74                color_str.push(char);
75            }
76        }
77
78        color
79    }
80
81    /// Get a color from a hex string. (hashtag sign included)
82    pub fn from_hex(hex: &str) -> Self {
83        let mut hex = hex.chars();
84        hex.next().unwrap(); // remove hashtag
85        Self(hex_u8!(hex), hex_u8!(hex), hex_u8!(hex))
86    }
87
88    /// Get the luminance of a single color value.
89    pub fn srgb_luminance(x: u8) -> f64 {
90        let srgb: f64 = x as f64 / 255.0;
91
92        if srgb <= LUMINANCE_THRESHOLD {
93            srgb / 12.92
94        } else {
95            ((srgb as f64 + 0.055) / 1.055).powf(GAMMA)
96        }
97    }
98
99    /// Get the luminance of the whole color.
100    pub fn luminance(&self) -> f64 {
101        Self::srgb_luminance(self.0) * RED
102            + Self::srgb_luminance(self.1) * GREEN
103            + Self::srgb_luminance(self.2) * BLUE
104    }
105
106    /// Get the contrast ratio between this color and another color.
107    pub fn contrast(&self, other: &Self) -> f64 {
108        let s_lum = self.luminance();
109        let o_lum = other.luminance();
110
111        let bright = s_lum.max(o_lum);
112        let dark = s_lum.min(o_lum);
113
114        (bright + 0.05) / (dark + 0.05)
115    }
116}
117
118#[cfg(test)]
119mod test {
120    use crate::Color;
121
122    #[test]
123    pub fn from_css_rgb() {
124        assert_eq!(Color::from("rgb(255,255, 0)"), Color(255, 255, 0))
125    }
126
127    #[test]
128    pub fn from_hex() {
129        assert_eq!(Color::from("#ffff00"), Color(255, 255, 0))
130    }
131
132    #[test]
133    pub fn black_on_white() {
134        let c1 = Color(255, 255, 255);
135        let c2 = Color(0, 0, 0);
136
137        assert_eq!(c1.contrast(&c2), 21.0);
138
139        let c3 = Color::from_hex("#ffffff");
140        let c4 = Color::from_hex("#000000");
141
142        assert_eq!(c1, c3);
143        assert_eq!(c2, c4);
144
145        assert_eq!(c3.contrast(&c4), 21.0);
146    }
147
148    #[test]
149    pub fn yellow_on_white() {
150        let c1 = Color(255, 255, 255);
151        let c2 = Color(255, 255, 0);
152
153        assert_eq!(c1.contrast(&c2), 1.0738392309265699);
154    }
155}