A Rust library for colorizing console output with cute gradients
at main 8.3 kB view raw
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}