1use std::{
2 fmt,
3 hash::{Hash, Hasher},
4};
5
6#[derive(Clone, Copy, Debug, PartialEq)]
12pub enum Color {
13 Rgb(f64, f64, f64),
16 Cmy(f64, f64, f64),
17 Cmyk(f64, f64, f64, f64),
18 Hsb(f64, f64, f64),
19 Gray(f64),
20}
21
22pub const BLACK: Color = Color::Rgb(0.0, 0.0, 0.0);
24pub const WHITE: Color = Color::Rgb(1.0, 1.0, 1.0);
26
27impl Eq for Color {}
29
30impl Hash for Color {
31 fn hash<H: Hasher>(&self, hasher: &mut H) {
32 std::mem::discriminant(self).hash(hasher);
33 for c in self.components() {
34 ((c * 100000.0) as i64).hash(hasher);
35 }
36 }
37}
38
39impl fmt::Display for Color {
41 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
42 let model = self.model();
43 let comps: Vec<String> = self
44 .components()
45 .iter()
46 .map(|c| format_component(*c))
47 .collect();
48 write!(f, "{model}({})", comps.join(","))
49 }
50}
51
52impl Color {
53 pub fn model(&self) -> &'static str {
55 match self {
56 Color::Rgb(..) => "rgb",
57 Color::Cmy(..) => "cmy",
58 Color::Cmyk(..) => "cmyk",
59 Color::Hsb(..) => "hsb",
60 Color::Gray(..) => "gray",
61 }
62 }
63
64 pub fn components(&self) -> Vec<f64> {
66 match self {
67 Color::Rgb(r, g, b) => vec![*r, *g, *b],
68 Color::Cmy(c, m, y) => vec![*c, *m, *y],
69 Color::Cmyk(c, m, y, k) => vec![*c, *m, *y, *k],
70 Color::Hsb(h, s, b) => vec![*h, *s, *b],
71 Color::Gray(g) => vec![*g],
72 }
73 }
74
75 pub fn to_rgb(&self) -> Color {
77 match self {
78 Color::Rgb(..) => *self,
79 Color::Cmy(c, m, y) => Color::Rgb(1.0 - c, 1.0 - m, 1.0 - y),
80 Color::Cmyk(..) => self.to_cmy().to_rgb(),
81 Color::Hsb(h, s, b) => {
82 let i = (6.0 * h) as i32;
83 let f = 6.0 * h - i as f64;
84 let u = b * (1.0 - s * (1.0 - f));
85 let v = b * (1.0 - s * f);
86 let w = b * (1.0 - s);
87 match i {
88 0 => Color::Rgb(*b, u, w),
89 1 => Color::Rgb(v, *b, w),
90 2 => Color::Rgb(w, *b, u),
91 3 => Color::Rgb(w, v, *b),
92 4 => Color::Rgb(u, w, *b),
93 5 => Color::Rgb(*b, w, v),
94 6 => Color::Rgb(*b, w, w),
95 _ => Color::Rgb(*b, w, w), }
97 },
98 Color::Gray(g) => Color::Rgb(*g, *g, *g),
99 }
100 }
101
102 pub fn to_cmy(&self) -> Color {
104 match self {
105 Color::Cmy(..) => *self,
106 Color::Rgb(r, g, b) => Color::Cmy(1.0 - r, 1.0 - g, 1.0 - b),
107 Color::Cmyk(c, m, y, k) => Color::Cmy((c + k).min(1.0), (m + k).min(1.0), (y + k).min(1.0)),
108 Color::Hsb(..) => self.to_rgb().to_cmy(),
109 Color::Gray(g) => Color::Cmy(1.0 - g, 1.0 - g, 1.0 - g),
110 }
111 }
112
113 pub fn to_cmyk(&self) -> Color {
115 match self {
116 Color::Cmyk(..) => *self,
117 Color::Cmy(c, m, y) => {
118 let k = c.min(*m).min(*y);
120 Color::Cmyk(
121 (c - k).clamp(0.0, 1.0),
122 (m - k).clamp(0.0, 1.0),
123 (y - k).clamp(0.0, 1.0),
124 k,
125 )
126 },
127 Color::Rgb(..) => self.to_cmy().to_cmyk(),
128 Color::Hsb(..) => self.to_rgb().to_cmyk(),
129 Color::Gray(g) => Color::Cmyk(0.0, 0.0, 0.0, 1.0 - g),
130 }
131 }
132
133 pub fn to_hsb(&self) -> Color {
135 match self {
136 Color::Hsb(..) => *self,
137 Color::Rgb(r, g, b) => {
138 let i = 4 * (if *r >= *g { 1 } else { 0 })
140 + 2 * (if *g >= *b { 1 } else { 0 })
141 + (if *b >= *r { 1 } else { 0 });
142 match i {
143 1 => phi(*b, *g, *r, 3.0, 1.0),
144 2 => phi(*g, *r, *b, 1.0, 1.0),
145 3 => phi(*g, *b, *r, 3.0, -1.0),
146 4 => phi(*r, *b, *g, 5.0, 1.0),
147 5 => phi(*b, *r, *g, 5.0, -1.0),
148 6 => phi(*r, *g, *b, 1.0, -1.0),
149 7 => Color::Hsb(0.0, 0.0, *b),
150 _ => Color::Hsb(0.0, 0.0, 0.0),
151 }
152 },
153 Color::Cmy(..) => self.to_rgb().to_hsb(),
154 Color::Cmyk(..) => self.to_cmy().to_hsb(),
155 Color::Gray(g) => Color::Hsb(0.0, 0.0, *g),
156 }
157 }
158
159 pub fn to_gray(&self) -> Color {
161 match self {
162 Color::Gray(..) => *self,
163 Color::Rgb(r, g, b) => Color::Gray(0.3 * r + 0.59 * g + 0.11 * b),
164 Color::Cmy(c, m, y) => Color::Gray(1.0 - (0.3 * c + 0.59 * m + 0.11 * y)),
165 Color::Cmyk(c, m, y, k) => Color::Gray(1.0 - (0.3 * c + 0.59 * m + 0.11 * y + k).min(1.0)),
166 Color::Hsb(..) => self.to_rgb().to_gray(),
167 }
168 }
169
170 pub fn convert(&self, to_model: &str) -> Color {
174 match to_model {
175 "rgb" => self.to_rgb(),
176 "cmy" => self.to_cmy(),
177 "cmyk" => self.to_cmyk(),
178 "hsb" => self.to_hsb(),
179 "gray" => self.to_gray(),
180 "HTML" | "RGB" => self.to_rgb(),
182 "Hsb" | "HSB" | "tHsb" => self.to_hsb(),
183 "Gray" => self.to_gray(),
184 _ => *self,
185 }
186 }
187
188 pub fn components_for_model(&self, model: &str) -> Vec<f64> {
193 let core = self.convert(model);
194 let comps = core.components();
195 match model {
196 "HTML" | "RGB" => comps.iter().map(|c| (c * 255.0).round()).collect(),
197 "Hsb" => vec![(comps[0] * 360.0).round(), comps[1], comps[2]],
198 "HSB" => comps.iter().map(|c| (c * 240.0).round()).collect(),
199 "Gray" => vec![(comps[0] * 15.0).round()],
200 _ => comps,
201 }
202 }
203
204 pub fn to_attribute(&self) -> String {
206 let rgb = self.to_rgb();
207 if let Color::Rgb(r, g, b) = rgb {
208 format!(
209 "#{:02X}{:02X}{:02X}",
210 component_to_u8(r),
211 component_to_u8(g),
212 component_to_u8(b)
213 )
214 } else {
215 unreachable!()
216 }
217 }
218
219 pub fn complement(&self) -> Color {
221 match self {
222 Color::Rgb(r, g, b) => Color::Rgb(1.0 - r, 1.0 - g, 1.0 - b),
223 Color::Cmy(c, m, y) => Color::Cmy(1.0 - c, 1.0 - m, 1.0 - y),
224 Color::Cmyk(..) => self.to_cmy().complement().to_cmyk(),
225 Color::Hsb(h, s, b) => {
226 let hp = if *h < 0.5 { h + 0.5 } else { h - 0.5 };
227 let bp = 1.0 - b * (1.0 - s);
228 let sp = if bp == 0.0 { 0.0 } else { b * s / bp };
229 Color::Hsb(hp, sp, bp)
230 },
231 Color::Gray(g) => Color::Gray(1.0 - g),
232 }
233 }
234
235 pub fn mix(&self, other: &Color, fraction: f64) -> Color {
237 let (base, other) = self.align_models(other);
238 if matches!(&base, Color::Hsb(..)) {
240 return base.to_rgb().mix(&other, fraction).to_hsb();
241 }
242 let a = base.components();
243 let b = other.components();
244 let mixed: Vec<f64> = a
248 .iter()
249 .zip(b.iter())
250 .map(|(ai, bi)| (fraction * ai + (1.0 - fraction) * bi).clamp(0.0, 1.0))
251 .collect();
252 from_model_components(base.model(), &mixed)
253 }
254
255 pub fn add(&self, other: &Color) -> Color {
257 let (base, other) = self.align_models(other);
258 let a = base.components();
259 let b = other.components();
260 let added: Vec<f64> = a.iter().zip(b.iter()).map(|(ai, bi)| ai + bi).collect();
261 from_model_components(base.model(), &added)
262 }
263
264 pub fn scale(&self, m: f64) -> Color {
266 let scaled: Vec<f64> = self.components().iter().map(|c| m * c).collect();
267 from_model_components(self.model(), &scaled)
268 }
269
270 pub fn multiply(&self, factors: &[f64]) -> Color {
272 let comps = self.components();
273 let result: Vec<f64> = comps
274 .iter()
275 .zip(factors.iter())
276 .map(|(c, f)| c * f)
277 .collect();
278 from_model_components(self.model(), &result)
279 }
280
281 fn align_models(&self, other: &Color) -> (Color, Color) {
284 if self.model() == other.model() {
285 return (*self, *other);
286 }
287 if self.model() == "gray" {
288 (self.convert(other.model()), *other)
289 } else {
290 (*self, other.convert(self.model()))
291 }
292 }
293
294 pub fn rgb_components_string(&self) -> String {
297 let rgb = self.to_rgb();
298 let comps = rgb.components();
299 comps
300 .iter()
301 .map(|c| format_component(*c))
302 .collect::<Vec<_>>()
303 .join(",")
304 }
305
306 pub fn to_stored(&self) -> String {
308 let model = self.model();
309 let comps: Vec<String> = self
310 .components()
311 .iter()
312 .map(|c| format_component(*c))
313 .collect();
314 format!("{model} {}", comps.join(" "))
315 }
316
317 pub fn from_stored(s: &str) -> Option<Color> {
319 let parts: Vec<&str> = s.split_whitespace().collect();
320 if parts.is_empty() {
321 return None;
322 }
323 let model = parts[0];
324 let comps: Vec<f64> = parts[1..]
325 .iter()
326 .filter_map(|p| p.parse::<f64>().ok())
327 .collect();
328 Some(from_model_components(model, &comps))
329 }
330}
331
332fn component_to_u8(v: f64) -> u8 {
335 let scaled = v.clamp(0.0, 1.0) * 255.0 * (1.0 + 100.0 * f64::EPSILON);
336 scaled.round() as u8
337}
338
339pub fn format_component(v: f64) -> String {
341 if (v - v.round()).abs() < 1e-10 {
342 format!("{}", v.round() as i64)
343 } else {
344 format!("{v}")
345 }
346}
347
348fn phi(x: f64, y: f64, z: f64, u: f64, v: f64) -> Color {
350 Color::Hsb(
351 (u * (x - z) + v * (x - y)) / (6.0 * (x - z)),
352 (x - z) / x,
353 x,
354 )
355}
356
357fn parse_components(spec: &str) -> Vec<f64> {
359 if spec.contains(',') {
363 spec
364 .split(',')
365 .flat_map(|s| s.split_whitespace())
366 .filter_map(|s| s.trim().parse::<f64>().ok())
367 .collect()
368 } else {
369 spec
370 .split_whitespace()
371 .filter_map(|s| s.parse::<f64>().ok())
372 .collect()
373 }
374}
375
376pub fn from_model_components(model: &str, comps: &[f64]) -> Color {
378 match model {
379 "rgb" if comps.len() >= 3 => Color::Rgb(comps[0], comps[1], comps[2]),
380 "cmy" if comps.len() >= 3 => Color::Cmy(comps[0], comps[1], comps[2]),
381 "cmyk" if comps.len() >= 4 => Color::Cmyk(comps[0], comps[1], comps[2], comps[3]),
382 "hsb" if comps.len() >= 3 => Color::Hsb(comps[0], comps[1], comps[2]),
383 "gray" if !comps.is_empty() => Color::Gray(comps[0]),
384 _ => BLACK,
385 }
386}
387
388pub fn color_from_model_spec(model: &str, spec: &str) -> Color {
391 let spec = spec.trim().trim_matches(|c| c == '{' || c == '}').trim();
392 let c = parse_components(spec);
393 from_model_components(model, &c)
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 fn eq_close(a: f64, b: f64) -> bool { (a - b).abs() < 1e-6 }
401
402 #[test]
403 fn model_names() {
404 assert_eq!(Color::Rgb(0.0, 0.0, 0.0).model(), "rgb");
405 assert_eq!(Color::Cmy(0.0, 0.0, 0.0).model(), "cmy");
406 assert_eq!(Color::Cmyk(0.0, 0.0, 0.0, 0.0).model(), "cmyk");
407 assert_eq!(Color::Hsb(0.0, 0.0, 0.0).model(), "hsb");
408 assert_eq!(Color::Gray(0.0).model(), "gray");
409 }
410
411 #[test]
412 fn components_match_variant() {
413 assert_eq!(Color::Rgb(0.1, 0.2, 0.3).components(), vec![0.1, 0.2, 0.3]);
414 assert_eq!(Color::Cmyk(0.1, 0.2, 0.3, 0.4).components(), vec![
415 0.1, 0.2, 0.3, 0.4
416 ]);
417 assert_eq!(Color::Gray(0.5).components(), vec![0.5]);
418 }
419
420 #[test]
421 fn black_and_white_constants() {
422 assert_eq!(BLACK, Color::Rgb(0.0, 0.0, 0.0));
423 assert_eq!(WHITE, Color::Rgb(1.0, 1.0, 1.0));
424 }
425
426 #[test]
427 fn to_rgb_idempotent() {
428 let c = Color::Rgb(0.25, 0.5, 0.75);
429 assert_eq!(c.to_rgb(), c);
430 }
431
432 #[test]
433 fn rgb_cmy_invert_components() {
434 let cmy = Color::Cmy(0.25, 0.5, 0.75);
435 let rgb = cmy.to_rgb();
436 if let Color::Rgb(r, g, b) = rgb {
437 assert!(
438 eq_close(r, 0.75) && eq_close(g, 0.5) && eq_close(b, 0.25),
439 "got {rgb:?}"
440 );
441 } else {
442 panic!("expected Rgb after cmy.to_rgb(), got {rgb:?}");
443 }
444 }
445
446 #[test]
447 fn complement_flips_rgb() {
448 let c = Color::Rgb(0.1, 0.2, 0.3);
449 let comp = c.complement();
450 if let Color::Rgb(r, g, b) = comp {
451 assert!(
452 eq_close(r, 0.9) && eq_close(g, 0.8) && eq_close(b, 0.7),
453 "got {comp:?}"
454 );
455 } else {
456 panic!("Rgb complement should return Rgb");
457 }
458 }
459
460 #[test]
461 fn display_format_parenthesized() {
462 let c = Color::Rgb(0.5, 0.5, 0.5);
463 let s = format!("{c}");
464 assert!(s.starts_with("rgb(") && s.ends_with(')'), "got {s:?}");
465 }
466
467 #[test]
468 fn from_model_components_rgb() {
469 let c = from_model_components("rgb", &[0.1, 0.2, 0.3]);
470 assert_eq!(c, Color::Rgb(0.1, 0.2, 0.3));
471 }
472
473 #[test]
474 fn from_model_components_gray_single() {
475 let c = from_model_components("gray", &[0.5]);
476 assert_eq!(c, Color::Gray(0.5));
477 }
478
479 #[test]
480 fn from_model_components_unknown_is_black() {
481 let c = from_model_components("nonesuch", &[1.0, 1.0, 1.0]);
483 assert_eq!(c, BLACK);
484 }
485
486 #[test]
487 fn from_model_components_insufficient_comps_is_black() {
488 let c = from_model_components("rgb", &[0.5]);
490 assert_eq!(c, BLACK);
491 }
492
493 #[test]
494 fn color_from_model_spec_parses_braced() {
495 let c = color_from_model_spec("rgb", "{0.1, 0.2, 0.3}");
497 assert_eq!(c, Color::Rgb(0.1, 0.2, 0.3));
498 }
499
500 #[test]
501 fn color_from_model_spec_parses_spaces() {
502 let c = color_from_model_spec("rgb", "0.1 0.2 0.3");
504 assert_eq!(c, Color::Rgb(0.1, 0.2, 0.3));
505 }
506
507 #[test]
508 fn color_inequality_across_models() {
509 assert_ne!(BLACK, Color::Cmyk(0.0, 0.0, 0.0, 1.0));
511 assert_ne!(WHITE, Color::Gray(1.0));
512 }
513}