1use std::fmt;
4
5#[derive(Debug, Clone, Copy, PartialEq)]
7pub struct Color {
8 pub r: f64,
9 pub g: f64,
10 pub b: f64,
11 pub a: f64,
12}
13
14impl Color {
15 pub fn new(r: f64, g: f64, b: f64, a: f64) -> Color {
16 Color { r, g, b, a }
17 }
18
19 pub fn from_rgba8(r: f64, g: f64, b: f64, a: f64) -> Color {
21 Color {
22 r: r / 255.0,
23 g: g / 255.0,
24 b: b / 255.0,
25 a,
26 }
27 }
28
29 pub fn to_rgba_unit(self) -> [f64; 4] {
34 [self.r * self.a, self.g * self.a, self.b * self.a, self.a]
35 }
36
37 pub fn to_rgba255(self) -> [f64; 4] {
40 [self.r * 255.0, self.g * 255.0, self.b * 255.0, self.a]
41 }
42
43 pub fn to_lab(self) -> [f64; 4] {
45 rgb_to_lab([self.r, self.g, self.b, self.a])
46 }
47
48 pub fn from_lab(lab: [f64; 4]) -> Color {
50 let [r, g, b, a] = lab_to_rgb(lab);
51 Color::new(r, g, b, a)
52 }
53
54 pub fn to_hcl(self) -> [f64; 4] {
56 rgb_to_hcl([self.r, self.g, self.b, self.a])
57 }
58
59 pub fn from_hcl(hcl: [f64; 4]) -> Color {
61 let [r, g, b, a] = hcl_to_rgb(hcl);
62 Color::new(r, g, b, a)
63 }
64
65 pub fn parse(input: &str) -> Option<Color> {
68 let s = input.trim();
69 if let Some(hex) = s.strip_prefix('#') {
70 return parse_hex(hex);
71 }
72 if let Some(c) = parse_functional(s) {
73 return Some(c);
74 }
75 named(s)
76 }
77}
78
79impl fmt::Display for Color {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 write!(
82 f,
83 "rgba({},{},{},{})",
84 (self.r * 255.0).round() as u8,
85 (self.g * 255.0).round() as u8,
86 (self.b * 255.0).round() as u8,
87 self.a
88 )
89 }
90}
91
92fn parse_hex(hex: &str) -> Option<Color> {
93 let bytes = hex.as_bytes();
94 let expand = |c: u8| {
95 let v = (c as char).to_digit(16)? as f64;
96 Some(v * 16.0 + v)
97 };
98 match hex.len() {
99 3 => Some(Color::from_rgba8(
100 expand(bytes[0])?,
101 expand(bytes[1])?,
102 expand(bytes[2])?,
103 1.0,
104 )),
105 4 => Some(Color::from_rgba8(
106 expand(bytes[0])?,
107 expand(bytes[1])?,
108 expand(bytes[2])?,
109 expand(bytes[3])? / 255.0,
110 )),
111 6 => Some(Color::from_rgba8(
112 hexpair(&hex[0..2])?,
113 hexpair(&hex[2..4])?,
114 hexpair(&hex[4..6])?,
115 1.0,
116 )),
117 8 => Some(Color::from_rgba8(
118 hexpair(&hex[0..2])?,
119 hexpair(&hex[2..4])?,
120 hexpair(&hex[4..6])?,
121 hexpair(&hex[6..8])? / 255.0,
122 )),
123 _ => None,
124 }
125}
126
127fn hexpair(s: &str) -> Option<f64> {
128 u8::from_str_radix(s, 16).ok().map(|v| v as f64)
129}
130
131fn parse_functional(s: &str) -> Option<Color> {
132 let open = s.find('(')?;
133 let name = s[..open].trim().to_ascii_lowercase();
134 let inner = s[open + 1..].strip_suffix(')')?;
135
136 let (body, alpha_tok) = match inner.split_once('/') {
139 Some((body, alpha)) => (body, Some(alpha.trim())),
140 None => (inner, None),
141 };
142 let parts: Vec<&str> = body
143 .split(|c: char| c == ',' || c.is_whitespace())
144 .filter(|t| !t.is_empty())
145 .collect();
146 if parts.len() < 3 {
147 return None;
148 }
149 let alpha_tok = alpha_tok.or_else(|| parts.get(3).copied());
150 let a = match alpha_tok {
151 Some(t) => alpha(t)?,
152 None => 1.0,
153 };
154
155 match name.as_str() {
156 "rgb" | "rgba" => Some(Color::from_rgba8(
157 channel(parts[0])?,
158 channel(parts[1])?,
159 channel(parts[2])?,
160 a,
161 )),
162 "hsl" | "hsla" => {
163 let h = parts[0].trim_end_matches("deg").parse::<f64>().ok()?;
164 let (r, g, b) = hsl_to_rgb(h, percent(parts[1])?, percent(parts[2])?);
165 Some(Color::new(r, g, b, a))
166 }
167 _ => None,
168 }
169}
170
171fn alpha(s: &str) -> Option<f64> {
173 if let Some(p) = s.strip_suffix('%') {
174 Some(p.trim().parse::<f64>().ok()? / 100.0)
175 } else {
176 s.parse::<f64>().ok()
177 }
178}
179
180fn channel(s: &str) -> Option<f64> {
181 if let Some(p) = s.strip_suffix('%') {
182 Some(p.trim().parse::<f64>().ok()? / 100.0 * 255.0)
183 } else {
184 s.parse::<f64>().ok()
185 }
186}
187
188fn percent(s: &str) -> Option<f64> {
189 s.strip_suffix('%')?
190 .trim()
191 .parse::<f64>()
192 .ok()
193 .map(|v| v / 100.0)
194}
195
196fn hsl_to_rgb(h: f64, s: f64, l: f64) -> (f64, f64, f64) {
197 let h = ((h % 360.0) + 360.0) % 360.0 / 360.0;
198 if s == 0.0 {
199 return (l, l, l);
200 }
201 let q = if l < 0.5 {
202 l * (1.0 + s)
203 } else {
204 l + s - l * s
205 };
206 let p = 2.0 * l - q;
207 (
208 hue_to_rgb(p, q, h + 1.0 / 3.0),
209 hue_to_rgb(p, q, h),
210 hue_to_rgb(p, q, h - 1.0 / 3.0),
211 )
212}
213
214fn hue_to_rgb(p: f64, q: f64, t: f64) -> f64 {
215 let t = if t < 0.0 {
216 t + 1.0
217 } else if t > 1.0 {
218 t - 1.0
219 } else {
220 t
221 };
222 if t < 1.0 / 6.0 {
223 p + (q - p) * 6.0 * t
224 } else if t < 1.0 / 2.0 {
225 q
226 } else if t < 2.0 / 3.0 {
227 p + (q - p) * (2.0 / 3.0 - t) * 6.0
228 } else {
229 p
230 }
231}
232
233const XN: f64 = 0.96422;
240const YN: f64 = 1.0;
241const ZN: f64 = 0.82521;
242const T0: f64 = 4.0 / 29.0;
243const T1: f64 = 6.0 / 29.0;
244const T2: f64 = 3.0 * T1 * T1;
245const T3: f64 = T1 * T1 * T1;
246
247fn rgb_to_lab([r, g, b, alpha]: [f64; 4]) -> [f64; 4] {
248 let r = rgb2xyz(r);
249 let g = rgb2xyz(g);
250 let b = rgb2xyz(b);
251 let y = xyz2lab((0.2225045 * r + 0.7168786 * g + 0.0606169 * b) / YN);
252 let (x, z) = if r == g && g == b {
253 (y, y)
254 } else {
255 (
256 xyz2lab((0.4360747 * r + 0.3850649 * g + 0.1430804 * b) / XN),
257 xyz2lab((0.0139322 * r + 0.0971045 * g + 0.7141733 * b) / ZN),
258 )
259 };
260 let l = 116.0 * y - 16.0;
261 [
262 if l < 0.0 { 0.0 } else { l },
263 500.0 * (x - y),
264 200.0 * (y - z),
265 alpha,
266 ]
267}
268
269fn lab_to_rgb([l, a, b, alpha]: [f64; 4]) -> [f64; 4] {
270 let y = (l + 16.0) / 116.0;
271 let x = if a.is_nan() { y } else { y + a / 500.0 };
272 let z = if b.is_nan() { y } else { y - b / 200.0 };
273 let y = YN * lab2xyz(y);
274 let x = XN * lab2xyz(x);
275 let z = ZN * lab2xyz(z);
276 [
277 xyz2rgb(3.1338561 * x - 1.6168667 * y - 0.4906146 * z),
278 xyz2rgb(-0.9787684 * x + 1.9161415 * y + 0.033454 * z),
279 xyz2rgb(0.0719453 * x - 0.2289914 * y + 1.4052427 * z),
280 alpha,
281 ]
282}
283
284fn rgb2xyz(x: f64) -> f64 {
285 if x <= 0.04045 {
286 x / 12.92
287 } else {
288 ((x + 0.055) / 1.055).powf(2.4)
289 }
290}
291
292fn xyz2lab(t: f64) -> f64 {
293 if t > T3 {
294 t.cbrt()
295 } else {
296 t / T2 + T0
297 }
298}
299
300fn lab2xyz(t: f64) -> f64 {
301 if t > T1 {
302 t * t * t
303 } else {
304 T2 * (t - T0)
305 }
306}
307
308fn xyz2rgb(x: f64) -> f64 {
309 let x = if x <= 0.00304 {
310 12.92 * x
311 } else {
312 1.055 * x.powf(1.0 / 2.4) - 0.055
313 };
314 x.clamp(0.0, 1.0)
315}
316
317fn constrain_angle(angle: f64) -> f64 {
318 let a = angle % 360.0;
319 if a < 0.0 {
320 a + 360.0
321 } else {
322 a
323 }
324}
325
326fn rgb_to_hcl(rgb: [f64; 4]) -> [f64; 4] {
327 let [l, a, b, alpha] = rgb_to_lab(rgb);
328 let c = (a * a + b * b).sqrt();
329 let h = if (c * 10000.0).round() != 0.0 {
330 constrain_angle(b.atan2(a).to_degrees())
331 } else {
332 f64::NAN
333 };
334 [h, c, l, alpha]
335}
336
337fn hcl_to_rgb([h, c, l, alpha]: [f64; 4]) -> [f64; 4] {
338 let h = if h.is_nan() { 0.0 } else { h.to_radians() };
339 lab_to_rgb([l, h.cos() * c, h.sin() * c, alpha])
340}
341
342fn named(s: &str) -> Option<Color> {
344 let rgb = match s.to_ascii_lowercase().as_str() {
345 "transparent" => return Some(Color::new(0.0, 0.0, 0.0, 0.0)),
346 "black" => (0, 0, 0),
347 "white" => (255, 255, 255),
348 "red" => (255, 0, 0),
349 "green" => (0, 128, 0),
350 "lime" => (0, 255, 0),
351 "blue" => (0, 0, 255),
352 "yellow" => (255, 255, 0),
353 "cyan" | "aqua" => (0, 255, 255),
354 "magenta" | "fuchsia" => (255, 0, 255),
355 "gray" | "grey" => (128, 128, 128),
356 "silver" => (192, 192, 192),
357 "maroon" => (128, 0, 0),
358 "olive" => (128, 128, 0),
359 "navy" => (0, 0, 128),
360 "purple" => (128, 0, 128),
361 "teal" => (0, 128, 128),
362 "orange" => (255, 165, 0),
363 _ => return None,
364 };
365 Some(Color::from_rgba8(
366 rgb.0 as f64,
367 rgb.1 as f64,
368 rgb.2 as f64,
369 1.0,
370 ))
371}