1use core::hash::{Hash, Hasher};
4use std::hash::DefaultHasher;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct RGB {
9 pub r: u8,
11 pub g: u8,
13 pub b: u8,
15}
16
17impl RGB {
18 pub fn new(r: u8, g: u8, b: u8) -> Self {
20 Self { r, g, b }
21 }
22
23 pub fn write_fg<W: core::fmt::Write>(&self, f: &mut W) -> core::fmt::Result {
25 write!(f, "\x1b[38;2;{};{};{}m", self.r, self.g, self.b)
26 }
27
28 pub fn write_bg<W: core::fmt::Write>(&self, f: &mut W) -> core::fmt::Result {
30 write!(f, "\x1b[48;2;{};{};{}m", self.r, self.g, self.b)
31 }
32}
33
34pub struct ColorGenerator {
36 base_hue: f32,
37 saturation: f32,
38 lightness: f32,
39}
40
41impl Default for ColorGenerator {
42 fn default() -> Self {
43 Self {
44 base_hue: 210.0,
45 saturation: 0.7,
46 lightness: 0.6,
47 }
48 }
49}
50
51impl ColorGenerator {
52 pub fn new() -> Self {
54 Self::default()
55 }
56
57 pub fn with_base_hue(mut self, hue: f32) -> Self {
59 self.base_hue = hue;
60 self
61 }
62
63 pub fn with_saturation(mut self, saturation: f32) -> Self {
65 self.saturation = saturation.clamp(0.0, 1.0);
66 self
67 }
68
69 pub fn with_lightness(mut self, lightness: f32) -> Self {
71 self.lightness = lightness.clamp(0.0, 1.0);
72 self
73 }
74
75 pub fn generate_color(&self, hash: u64) -> RGB {
77 let hue_offset = (hash % 360) as f32;
79 let hue = (self.base_hue + hue_offset) % 360.0;
80
81 self.hsl_to_rgb(hue, self.saturation, self.lightness)
83 }
84
85 pub fn generate_color_for<T: Hash>(&self, value: &T) -> RGB {
87 let mut hasher = DefaultHasher::new();
88 value.hash(&mut hasher);
89 let hash = hasher.finish();
90 self.generate_color(hash)
91 }
92
93 fn hsl_to_rgb(&self, h: f32, s: f32, l: f32) -> RGB {
95 let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
96 let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
97 let m = l - c / 2.0;
98
99 let (r, g, b) = match h as u32 {
100 0..=59 => (c, x, 0.0),
101 60..=119 => (x, c, 0.0),
102 120..=179 => (0.0, c, x),
103 180..=239 => (0.0, x, c),
104 240..=299 => (x, 0.0, c),
105 _ => (c, 0.0, x),
106 };
107
108 RGB::new(
109 ((r + m) * 255.0) as u8,
110 ((g + m) * 255.0) as u8,
111 ((b + m) * 255.0) as u8,
112 )
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn test_color_generator_default() {
122 let generator = ColorGenerator::default();
123 assert_eq!(generator.base_hue, 210.0);
124 assert_eq!(generator.saturation, 0.7);
125 assert_eq!(generator.lightness, 0.6);
126 }
127
128 #[test]
129 fn test_color_generator_with_methods() {
130 let generator = ColorGenerator::new()
131 .with_base_hue(180.0)
132 .with_saturation(0.5)
133 .with_lightness(0.7);
134
135 assert_eq!(generator.base_hue, 180.0);
136 assert_eq!(generator.saturation, 0.5);
137 assert_eq!(generator.lightness, 0.7);
138 }
139
140 #[test]
141 fn test_saturation_clamping() {
142 let generator = ColorGenerator::new().with_saturation(1.5);
143 assert_eq!(generator.saturation, 1.0);
144
145 let generator = ColorGenerator::new().with_saturation(-0.5);
146 assert_eq!(generator.saturation, 0.0);
147 }
148
149 #[test]
150 fn test_lightness_clamping() {
151 let generator = ColorGenerator::new().with_lightness(1.5);
152 assert_eq!(generator.lightness, 1.0);
153
154 let generator = ColorGenerator::new().with_lightness(-0.5);
155 assert_eq!(generator.lightness, 0.0);
156 }
157
158 #[test]
159 fn test_generate_color() {
160 let generator = ColorGenerator::default();
161
162 let color1 = generator.generate_color(42);
164 let color2 = generator.generate_color(42);
165 assert_eq!(color1, color2);
166
167 let color3 = generator.generate_color(100);
169 assert_ne!(color1, color3);
170 }
171
172 #[test]
173 fn test_generate_color_for() {
174 let generator = ColorGenerator::default();
175
176 let color1 = generator.generate_color_for(&"test");
178 let color2 = generator.generate_color_for(&"test");
179 assert_eq!(color1, color2);
180
181 let color3 = generator.generate_color_for(&"other");
183 assert_ne!(color1, color3);
184 }
185}