Something went wrong. Try again.
A Rust library for colorizing console output with cute gradients
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307use crossterm::style::Color;use rand::Rng;
#[derive(Debug, Clone, Copy, PartialEq)]pub enum GradientDirection { Horizontal, Vertical, Diagonal, Angle(f32),}
#[derive(Debug, Clone, Copy, PartialEq)]pub enum HueRange { Full, Reds, Oranges, Yellows, Greens, Cyans, Blues, Purples, Pinks, Warm, Cool, Custom(f32, f32),}
impl HueRange { pub fn range(&self) -> (f32, f32) { match self { HueRange::Full => (0.0, 360.0), HueRange::Reds => (330.0, 30.0), HueRange::Oranges => (30.0, 60.0), HueRange::Yellows => (60.0, 90.0), HueRange::Greens => (90.0, 150.0), HueRange::Cyans => (150.0, 210.0), HueRange::Blues => (210.0, 270.0), HueRange::Purples => (270.0, 330.0), HueRange::Pinks => (300.0, 360.0), HueRange::Warm => (330.0, 90.0), HueRange::Cool => (150.0, 330.0), HueRange::Custom(start, end) => (*start, *end), } }
pub fn contains(&self, hue: f32) -> bool { let (start, end) = self.range(); let normalized_hue = hue.rem_euclid(360.0);
if start > end { normalized_hue >= start || normalized_hue <= end } else { normalized_hue >= start && normalized_hue <= end } }
pub fn constrain(&self, hue: f32) -> f32 { let (start, end) = self.range(); let range_size = if start > end { (360.0 - start) + end } else { end - start };
let offset = if start > end { let normalized = hue.rem_euclid(360.0); if normalized >= start { normalized - start } else { (360.0 - start) + normalized } } else { (hue - start).rem_euclid(360.0) };
let cycle = (offset / range_size).floor(); let position = offset % range_size; let ping_pong = if cycle as i32 % 2 == 0 { position } else { range_size - position };
let result = start + ping_pong; if start > end && result >= 360.0 { result - 360.0 } else { result } }}
#[derive(Debug, Clone, Copy, PartialEq)]pub enum ColorPalette { Rainbow, Pastel, Neon, Earth, Ocean, Fire, Sunset, Forest, Lavender, CherryBlossom,}
impl ColorPalette { pub fn config(&self) -> (HueRange, f32, f32) { match self { ColorPalette::Rainbow => (HueRange::Full, 0.6, 0.7), ColorPalette::Pastel => (HueRange::Full, 0.3, 0.85), ColorPalette::Neon => (HueRange::Full, 0.9, 0.6), ColorPalette::Earth => (HueRange::Custom(20.0, 60.0), 0.4, 0.5), ColorPalette::Ocean => (HueRange::Custom(180.0, 240.0), 0.5, 0.6), ColorPalette::Fire => (HueRange::Custom(0.0, 60.0), 0.8, 0.6), ColorPalette::Sunset => (HueRange::Custom(300.0, 60.0), 0.6, 0.65), ColorPalette::Forest => (HueRange::Greens, 0.5, 0.4), ColorPalette::Lavender => (HueRange::Purples, 0.35, 0.8), ColorPalette::CherryBlossom => (HueRange::Pinks, 0.4, 0.85), } }}
#[derive(Debug, Clone)]pub struct GradientConfig { pub direction: GradientDirection, pub hue_range: HueRange, pub saturation: f32, pub lightness: f32, pub hue_shift: f32, pub base_hue: Option<f32>, pub reverse: bool, pub scale: f32, pub step: f32,}
impl Default for GradientConfig { fn default() -> Self { let (start, end) = HueRange::Full.range(); Self { direction: GradientDirection::Horizontal, hue_range: HueRange::Full, saturation: 0.35, lightness: 0.8, hue_shift: 12.0, base_hue: Some(generate_random_hue(start, end)), reverse: false, scale: 0.0, step: 1.0, } }}
fn generate_random_hue(start: f32, end: f32) -> f32 { let mut rng = rand::rng(); if start > end { let range_size = (360.0 - start) + end; let random_offset = rng.random_range(0.0..range_size); (start + random_offset).rem_euclid(360.0) } else { rng.random_range(start..end) }}
impl GradientConfig { pub fn direction(mut self, direction: GradientDirection) -> Self { self.direction = direction; self }
pub fn hue_range(mut self, range: HueRange) -> Self { self.hue_range = range; let (start, end) = range.range(); self.base_hue = Some(generate_random_hue(start, end)); self }
pub fn saturation(mut self, saturation: f32) -> Self { self.saturation = saturation.clamp(0.0, 1.0); self }
pub fn lightness(mut self, lightness: f32) -> Self { self.lightness = lightness.clamp(0.0, 1.0); self }
pub fn hue_shift(mut self, shift: f32) -> Self { self.hue_shift = shift; self }
pub fn base_hue(mut self, hue: f32) -> Self { self.base_hue = Some(hue.rem_euclid(360.0)); self }
pub fn random_hue(mut self) -> Self { let (start, end) = self.hue_range.range(); self.base_hue = Some(generate_random_hue(start, end)); self }
pub fn palette(mut self, palette: ColorPalette) -> Self { let (hue_range, saturation, lightness) = palette.config(); self.hue_range = hue_range; self.saturation = saturation; self.lightness = lightness; let (start, end) = hue_range.range(); self.base_hue = Some(generate_random_hue(start, end)); self }
pub fn reverse(mut self) -> Self { self.reverse = !self.reverse; self }
pub fn scale(mut self, scale: f32) -> Self { self.scale = if scale > 0.0 { scale } else { 0.0 }; self }
pub fn step(mut self, step: f32) -> Self { self.step = step.max(0.01); self }
pub fn get_base_hue(&self) -> f32 { self.base_hue.unwrap_or_else(|| { let (start, end) = self.hue_range.range(); generate_random_hue(start, end) }) }
pub fn color_at_position( &self, position: usize, row: usize, _max_width: usize, _max_height: usize, ) -> Color { let base_hue = self.get_base_hue();
let offset = match self.direction { GradientDirection::Horizontal => position as f32, GradientDirection::Vertical => row as f32, GradientDirection::Diagonal => (position + row) as f32, GradientDirection::Angle(angle) => { let rad = angle.to_radians(); (position as f32) * rad.cos() + (row as f32) * rad.sin() } };
let (start, end) = self.hue_range.range(); let range_size = if start > end { (360.0 - start) + end } else { end - start };
let stepped_offset = (offset / self.step).floor(); let hue_shift_per_char = if self.scale > 0.0 { range_size / self.scale } else { self.hue_shift };
let effective_offset = if self.reverse { -stepped_offset } else { stepped_offset }; let hue_offset = effective_offset * hue_shift_per_char; let hue = self.hue_range.constrain(base_hue + hue_offset);
hsl_to_rgb(hue, self.saturation, self.lightness) }}
macro_rules! to_u8 { ($val:expr) => { ($val * 255.0).round().clamp(0.0, 255.0) as u8 };}
fn hsl_to_rgb(h: f32, s: f32, l: f32) -> Color { let c = (1.0 - (2.0 * l - 1.0).abs()) * s; let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs()); let m = l - c / 2.0;
let (r_prime, g_prime, b_prime) = match h { h if h < 60.0 => (c, x, 0.0), h if h < 120.0 => (x, c, 0.0), h if h < 180.0 => (0.0, c, x), h if h < 240.0 => (0.0, x, c), h if h < 300.0 => (x, 0.0, c), _ => (c, 0.0, x), };
Color::Rgb { r: to_u8!(r_prime + m), g: to_u8!(g_prime + m), b: to_u8!(b_prime + m), }}