1pub 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
6pub 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#[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 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 pub fn from_hex(hex: &str) -> Self {
83 let mut hex = hex.chars();
84 hex.next().unwrap(); Self(hex_u8!(hex), hex_u8!(hex), hex_u8!(hex))
86 }
87
88 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 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 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}