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
34#[derive(Clone)]
36pub struct ColorGenerator {
37 base_hue: f32,
38 saturation: f32,
39 lightness: f32,
40}
41
42impl Default for ColorGenerator {
43 fn default() -> Self {
44 Self {
45 base_hue: 210.0,
46 saturation: 0.7,
47 lightness: 0.6,
48 }
49 }
50}
51
52impl ColorGenerator {
53 pub fn new() -> Self {
55 Self::default()
56 }
57
58 pub fn with_base_hue(mut self, hue: f32) -> Self {
60 self.base_hue = hue;
61 self
62 }
63
64 pub fn with_saturation(mut self, saturation: f32) -> Self {
66 self.saturation = saturation.clamp(0.0, 1.0);
67 self
68 }
69
70 pub fn with_lightness(mut self, lightness: f32) -> Self {
72 self.lightness = lightness.clamp(0.0, 1.0);
73 self
74 }
75
76 pub fn generate_color(&self, hash: u64) -> RGB {
78 let hue_offset = (hash % 360) as f32;
80 let hue = (self.base_hue + hue_offset) % 360.0;
81
82 self.hsl_to_rgb(hue, self.saturation, self.lightness)
84 }
85
86 pub fn generate_color_for<T: Hash>(&self, value: &T) -> RGB {
88 let mut hasher = DefaultHasher::new();
89 value.hash(&mut hasher);
90 let hash = hasher.finish();
91 self.generate_color(hash)
92 }
93
94 fn hsl_to_rgb(&self, h: f32, s: f32, l: f32) -> RGB {
96 let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
97 let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
98 let m = l - c / 2.0;
99
100 let (r, g, b) = match h as u32 {
101 0..=59 => (c, x, 0.0),
102 60..=119 => (x, c, 0.0),
103 120..=179 => (0.0, c, x),
104 180..=239 => (0.0, x, c),
105 240..=299 => (x, 0.0, c),
106 _ => (c, 0.0, x),
107 };
108
109 RGB::new(
110 ((r + m) * 255.0) as u8,
111 ((g + m) * 255.0) as u8,
112 ((b + m) * 255.0) as u8,
113 )
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn test_color_generator_default() {
123 let generator = ColorGenerator::default();
124 assert_eq!(generator.base_hue, 210.0);
125 assert_eq!(generator.saturation, 0.7);
126 assert_eq!(generator.lightness, 0.6);
127 }
128
129 #[test]
130 fn test_color_generator_with_methods() {
131 let generator = ColorGenerator::new()
132 .with_base_hue(180.0)
133 .with_saturation(0.5)
134 .with_lightness(0.7);
135
136 assert_eq!(generator.base_hue, 180.0);
137 assert_eq!(generator.saturation, 0.5);
138 assert_eq!(generator.lightness, 0.7);
139 }
140
141 #[test]
142 fn test_saturation_clamping() {
143 let generator = ColorGenerator::new().with_saturation(1.5);
144 assert_eq!(generator.saturation, 1.0);
145
146 let generator = ColorGenerator::new().with_saturation(-0.5);
147 assert_eq!(generator.saturation, 0.0);
148 }
149
150 #[test]
151 fn test_lightness_clamping() {
152 let generator = ColorGenerator::new().with_lightness(1.5);
153 assert_eq!(generator.lightness, 1.0);
154
155 let generator = ColorGenerator::new().with_lightness(-0.5);
156 assert_eq!(generator.lightness, 0.0);
157 }
158
159 #[test]
160 fn test_generate_color() {
161 let generator = ColorGenerator::default();
162
163 let color1 = generator.generate_color(42);
165 let color2 = generator.generate_color(42);
166 assert_eq!(color1, color2);
167
168 let color3 = generator.generate_color(100);
170 assert_ne!(color1, color3);
171 }
172
173 #[test]
174 fn test_generate_color_for() {
175 let generator = ColorGenerator::default();
176
177 let color1 = generator.generate_color_for(&"test");
179 let color2 = generator.generate_color_for(&"test");
180 assert_eq!(color1, color2);
181
182 let color3 = generator.generate_color_for(&"other");
184 assert_ne!(color1, color3);
185 }
186}