use colored::Colorize;
use rand::Rng;
use crate::color::Color;
use crate::gradient::Gradient;
pub fn rainbow(text: &str, frame: usize) -> String {
let hue = (frame * 5 % 360) as f64;
let left = Color::from_hsv(hue, 1.0, 1.0);
let right = Color::from_hsv((hue + 1.0) % 360.0, 1.0, 1.0);
Gradient::new(vec![left, right]).long().multiline(text)
}
pub fn pulse(text: &str, frame: usize) -> String {
let cycle = (frame % 120) + 1;
let transition = 6;
let duration = 10;
let on = Color::new(0xff, 0x10, 0x10);
let off = Color::new(0xe6, 0xe6, 0xe6);
if cycle <= transition {
let progress = cycle as f64 / transition as f64;
let half = progress / 2.0;
let gradient = if progress <= 0.5 {
Gradient::new_with_positions(vec![
(off, 0.0),
(off, 0.5 - half),
(on, 0.5),
(off, 0.5 + half),
(off, 1.0),
])
} else {
Gradient::new_with_positions(vec![
(on, 0.0),
(off, 0.5 - half),
(on, 0.5),
(off, 0.5 + half),
(on, 1.0),
])
};
gradient.multiline(text)
} else if cycle <= transition + duration {
apply_solid(text, on)
} else if cycle <= transition * 2 + duration {
let progress = (transition * 2 + duration - cycle + 1) as f64 / transition as f64;
let half = progress / 2.0;
let gradient = if progress <= 0.5 {
Gradient::new_with_positions(vec![
(off, 0.0),
(off, 0.5 - half),
(on, 0.5),
(off, 0.5 + half),
(off, 1.0),
])
} else {
Gradient::new_with_positions(vec![
(on, 0.0),
(off, 0.5 - half),
(on, 0.5),
(off, 0.5 + half),
(on, 1.0),
])
};
gradient.multiline(text)
} else {
apply_solid(text, off)
}
}
pub fn glitch(text: &str, frame: usize) -> String {
let blackout =
(frame % 2) + (frame % 3) + (frame % 11) + (frame % 29) + (frame % 37) > 52;
if blackout {
return " ".repeat(text.len());
}
let mut rng = rand::rng();
let glitch_chars: &[u8] = b"x*0987654321[]0-~@#(____!!!!\\|?????....0000\t";
let chunk_size = (text.len() / 50).max(3);
let mut result = String::with_capacity(text.len());
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
while i < chars.len() {
if rng.random_range(0..100) < 2 && i + chunk_size < chars.len() {
result.extend(std::iter::repeat_n(' ', chunk_size));
i += chunk_size;
continue;
}
if rng.random_range(0..1000) < 5 {
let gc = glitch_chars[rng.random_range(0..glitch_chars.len())] as char;
result.push(gc);
i += 1;
continue;
}
if rng.random_range(0..1000) < 5 {
i += 1;
continue;
}
result.push(chars[i]);
i += 1;
}
if rng.random_range(0..100) < 1 {
result = result.to_uppercase();
} else if rng.random_range(0..100) < 1 {
result = result.to_lowercase();
}
result
}
pub fn radar(text: &str, frame: usize) -> String {
let len = text.chars().count();
if len == 0 {
return String::new();
}
let depth = (len as f64 * 0.2).floor() as usize;
let depth = depth.max(1).min(len);
let step = 255 / depth;
let global_pos = frame % (len + depth);
text.chars()
.enumerate()
.map(|(i, ch)| {
let dist_from_head = global_pos as isize - i as isize;
if dist_from_head >= 0 && (dist_from_head as usize) < depth {
let brightness = 255 - (dist_from_head as usize * step);
let b = brightness as u8;
ch.to_string().truecolor(b, b, b).to_string()
} else {
" ".to_string()
}
})
.collect()
}
pub fn neon(text: &str, frame: usize) -> String {
if frame % 2 == 0 {
apply_solid(text, Color::new(88, 80, 85))
} else {
let colored = text.truecolor(213, 70, 242).bold();
colored.to_string()
}
}
pub fn karaoke(text: &str, frame: usize) -> String {
let len = text.chars().count();
let cursor = (frame % (len + 20)) as isize - 10;
if cursor < 0 {
return apply_solid(text, Color::new(255, 255, 255));
}
let cursor = cursor as usize;
let mut result = String::new();
for (i, ch) in text.chars().enumerate() {
if i < cursor {
result.push_str(&ch.to_string().truecolor(255, 187, 0).bold().to_string());
} else {
result.push_str(&ch.to_string().truecolor(255, 255, 255).to_string());
}
}
result
}
pub fn flap(text: &str, frame: usize, settled: Color, flipping: Color) -> String {
const FLAP_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-.:";
let chars: Vec<char> = text.chars().collect();
if chars.is_empty() {
return String::new();
}
let max_word_len = text.split_whitespace().map(|w| w.len()).max().unwrap_or(1);
let flips_per_char = 4;
let stagger = 2;
let total_frames = max_word_len * stagger + flips_per_char;
let cycle_frame = frame.min(total_frames);
let mut rng = rand::rng();
let mut result = String::new();
let mut pos_in_word: usize = 0;
for &target in &chars {
if target.is_whitespace() {
result.push(target);
pos_in_word = 0;
continue;
}
let char_start = pos_in_word * stagger;
let frames_in = cycle_frame as isize - char_start as isize;
if frames_in < 0 || (frames_in as usize) < flips_per_char {
let rc = FLAP_CHARS[rng.random_range(0..FLAP_CHARS.len())] as char;
result.push_str(&rc.to_string().truecolor(flipping.r, flipping.g, flipping.b).to_string());
} else {
result.push_str(&target.to_string().truecolor(settled.r, settled.g, settled.b).to_string());
}
pos_in_word += 1;
}
result
}
pub fn fade_in(text: &str, frame: usize, total_frames: usize, target: Color) -> String {
let t = (frame as f64 / total_frames.max(1) as f64).min(1.0);
let c = Color::lerp_rgb(Color::new(0, 0, 0), target, t);
text.split('\n').map(|line| {
line.chars().map(|ch| {
ch.to_string().truecolor(c.r, c.g, c.b).to_string()
}).collect::<String>()
}).collect::<Vec<_>>().join("\n")
}
pub fn fade_out(text: &str, frame: usize, total_frames: usize, target: Color) -> String {
let t = (frame as f64 / total_frames.max(1) as f64).min(1.0);
let c = Color::lerp_rgb(target, Color::new(0, 0, 0), t);
text.split('\n').map(|line| {
line.chars().map(|ch| {
ch.to_string().truecolor(c.r, c.g, c.b).to_string()
}).collect::<String>()
}).collect::<Vec<_>>().join("\n")
}
fn apply_solid(text: &str, c: Color) -> String {
text.truecolor(c.r, c.g, c.b).to_string()
}