A Rust library for colorizing console output with cute gradients
1use crossterm::style::Color;
2use rand::Rng;
3
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub enum GradientDirection {
6 Horizontal,
7 Vertical,
8 Diagonal,
9 Angle(f32),
10}
11
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum HueRange {
14 Full,
15 Reds,
16 Oranges,
17 Yellows,
18 Greens,
19 Cyans,
20 Blues,
21 Purples,
22 Pinks,
23 Warm,
24 Cool,
25 Custom(f32, f32),
26}
27
28impl HueRange {
29 pub fn range(&self) -> (f32, f32) {
30 match self {
31 HueRange::Full => (0.0, 360.0),
32 HueRange::Reds => (330.0, 30.0),
33 HueRange::Oranges => (30.0, 60.0),
34 HueRange::Yellows => (60.0, 90.0),
35 HueRange::Greens => (90.0, 150.0),
36 HueRange::Cyans => (150.0, 210.0),
37 HueRange::Blues => (210.0, 270.0),
38 HueRange::Purples => (270.0, 330.0),
39 HueRange::Pinks => (300.0, 360.0),
40 HueRange::Warm => (330.0, 90.0),
41 HueRange::Cool => (150.0, 330.0),
42 HueRange::Custom(start, end) => (*start, *end),
43 }
44 }
45
46 pub fn contains(&self, hue: f32) -> bool {
47 let (start, end) = self.range();
48 let normalized_hue = hue.rem_euclid(360.0);
49
50 if start > end {
51 normalized_hue >= start || normalized_hue <= end
52 } else {
53 normalized_hue >= start && normalized_hue <= end
54 }
55 }
56
57 pub fn constrain(&self, hue: f32) -> f32 {
58 let (start, end) = self.range();
59 let range_size = if start > end {
60 (360.0 - start) + end
61 } else {
62 end - start
63 };
64
65 let offset = if start > end {
66 let normalized = hue.rem_euclid(360.0);
67 if normalized >= start {
68 normalized - start
69 } else {
70 (360.0 - start) + normalized
71 }
72 } else {
73 (hue - start).rem_euclid(360.0)
74 };
75
76 let cycle = (offset / range_size).floor();
77 let position = offset % range_size;
78 let ping_pong = if cycle as i32 % 2 == 0 {
79 position
80 } else {
81 range_size - position
82 };
83
84 let result = start + ping_pong;
85 if start > end && result >= 360.0 {
86 result - 360.0
87 } else {
88 result
89 }
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq)]
94pub enum ColorPalette {
95 Rainbow,
96 Pastel,
97 Neon,
98 Earth,
99 Ocean,
100 Fire,
101 Sunset,
102 Forest,
103 Lavender,
104 CherryBlossom,
105}
106
107impl ColorPalette {
108 pub fn config(&self) -> (HueRange, f32, f32) {
109 match self {
110 ColorPalette::Rainbow => (HueRange::Full, 0.6, 0.7),
111 ColorPalette::Pastel => (HueRange::Full, 0.3, 0.85),
112 ColorPalette::Neon => (HueRange::Full, 0.9, 0.6),
113 ColorPalette::Earth => (HueRange::Custom(20.0, 60.0), 0.4, 0.5),
114 ColorPalette::Ocean => (HueRange::Custom(180.0, 240.0), 0.5, 0.6),
115 ColorPalette::Fire => (HueRange::Custom(0.0, 60.0), 0.8, 0.6),
116 ColorPalette::Sunset => (HueRange::Custom(300.0, 60.0), 0.6, 0.65),
117 ColorPalette::Forest => (HueRange::Greens, 0.5, 0.4),
118 ColorPalette::Lavender => (HueRange::Purples, 0.35, 0.8),
119 ColorPalette::CherryBlossom => (HueRange::Pinks, 0.4, 0.85),
120 }
121 }
122}
123
124#[derive(Debug, Clone)]
125pub struct GradientConfig {
126 pub direction: GradientDirection,
127 pub hue_range: HueRange,
128 pub saturation: f32,
129 pub lightness: f32,
130 pub hue_shift: f32,
131 pub base_hue: Option<f32>,
132 pub reverse: bool,
133 pub scale: f32,
134 pub step: f32,
135}
136
137impl Default for GradientConfig {
138 fn default() -> Self {
139 let (start, end) = HueRange::Full.range();
140 Self {
141 direction: GradientDirection::Horizontal,
142 hue_range: HueRange::Full,
143 saturation: 0.35,
144 lightness: 0.8,
145 hue_shift: 12.0,
146 base_hue: Some(generate_random_hue(start, end)),
147 reverse: false,
148 scale: 0.0,
149 step: 1.0,
150 }
151 }
152}
153
154fn generate_random_hue(start: f32, end: f32) -> f32 {
155 let mut rng = rand::rng();
156 if start > end {
157 let range_size = (360.0 - start) + end;
158 let random_offset = rng.random_range(0.0..range_size);
159 (start + random_offset).rem_euclid(360.0)
160 } else {
161 rng.random_range(start..end)
162 }
163}
164
165impl GradientConfig {
166 pub fn direction(mut self, direction: GradientDirection) -> Self {
167 self.direction = direction;
168 self
169 }
170
171 pub fn hue_range(mut self, range: HueRange) -> Self {
172 self.hue_range = range;
173 let (start, end) = range.range();
174 self.base_hue = Some(generate_random_hue(start, end));
175 self
176 }
177
178 pub fn saturation(mut self, saturation: f32) -> Self {
179 self.saturation = saturation.clamp(0.0, 1.0);
180 self
181 }
182
183 pub fn lightness(mut self, lightness: f32) -> Self {
184 self.lightness = lightness.clamp(0.0, 1.0);
185 self
186 }
187
188 pub fn hue_shift(mut self, shift: f32) -> Self {
189 self.hue_shift = shift;
190 self
191 }
192
193 pub fn base_hue(mut self, hue: f32) -> Self {
194 self.base_hue = Some(hue.rem_euclid(360.0));
195 self
196 }
197
198 pub fn random_hue(mut self) -> Self {
199 let (start, end) = self.hue_range.range();
200 self.base_hue = Some(generate_random_hue(start, end));
201 self
202 }
203
204 pub fn palette(mut self, palette: ColorPalette) -> Self {
205 let (hue_range, saturation, lightness) = palette.config();
206 self.hue_range = hue_range;
207 self.saturation = saturation;
208 self.lightness = lightness;
209 let (start, end) = hue_range.range();
210 self.base_hue = Some(generate_random_hue(start, end));
211 self
212 }
213
214 pub fn reverse(mut self) -> Self {
215 self.reverse = !self.reverse;
216 self
217 }
218
219 pub fn scale(mut self, scale: f32) -> Self {
220 self.scale = if scale > 0.0 { scale } else { 0.0 };
221 self
222 }
223
224 pub fn step(mut self, step: f32) -> Self {
225 self.step = step.max(0.01);
226 self
227 }
228
229 pub fn get_base_hue(&self) -> f32 {
230 self.base_hue.unwrap_or_else(|| {
231 let (start, end) = self.hue_range.range();
232 generate_random_hue(start, end)
233 })
234 }
235
236 pub fn color_at_position(
237 &self,
238 position: usize,
239 row: usize,
240 _max_width: usize,
241 _max_height: usize,
242 ) -> Color {
243 let base_hue = self.get_base_hue();
244
245 let offset = match self.direction {
246 GradientDirection::Horizontal => position as f32,
247 GradientDirection::Vertical => row as f32,
248 GradientDirection::Diagonal => (position + row) as f32,
249 GradientDirection::Angle(angle) => {
250 let rad = angle.to_radians();
251 (position as f32) * rad.cos() + (row as f32) * rad.sin()
252 }
253 };
254
255 let (start, end) = self.hue_range.range();
256 let range_size = if start > end {
257 (360.0 - start) + end
258 } else {
259 end - start
260 };
261
262 let stepped_offset = (offset / self.step).floor();
263 let hue_shift_per_char = if self.scale > 0.0 {
264 range_size / self.scale
265 } else {
266 self.hue_shift
267 };
268
269 let effective_offset = if self.reverse {
270 -stepped_offset
271 } else {
272 stepped_offset
273 };
274 let hue_offset = effective_offset * hue_shift_per_char;
275 let hue = self.hue_range.constrain(base_hue + hue_offset);
276
277 hsl_to_rgb(hue, self.saturation, self.lightness)
278 }
279}
280
281macro_rules! to_u8 {
282 ($val:expr) => {
283 ($val * 255.0).round().clamp(0.0, 255.0) as u8
284 };
285}
286
287fn hsl_to_rgb(h: f32, s: f32, l: f32) -> Color {
288 let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
289 let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
290 let m = l - c / 2.0;
291
292 let (r_prime, g_prime, b_prime) = match h {
293 h if h < 60.0 => (c, x, 0.0),
294 h if h < 120.0 => (x, c, 0.0),
295 h if h < 180.0 => (0.0, c, x),
296 h if h < 240.0 => (0.0, x, c),
297 h if h < 300.0 => (x, 0.0, c),
298 _ => (c, 0.0, x),
299 };
300
301 Color::Rgb {
302 r: to_u8!(r_prime + m),
303 g: to_u8!(g_prime + m),
304 b: to_u8!(b_prime + m),
305 }
306}