use crate::core::Color;
#[derive(Debug, Clone)]
pub struct Gradient {
colors: Vec<Color>,
}
impl Gradient {
pub fn new(colors: Vec<Color>) -> Self {
Self { colors }
}
pub fn from_two(start: Color, end: Color) -> Self {
Self {
colors: vec![start, end],
}
}
pub fn rainbow() -> Self {
Self {
colors: vec![
Color::Red,
Color::Yellow,
Color::Green,
Color::Cyan,
Color::Blue,
Color::Magenta,
],
}
}
pub fn warm() -> Self {
Self {
colors: vec![
Color::Rgb(255, 0, 0),
Color::Rgb(255, 128, 0),
Color::Rgb(255, 255, 0),
],
}
}
pub fn cool() -> Self {
Self {
colors: vec![
Color::Rgb(0, 255, 255),
Color::Rgb(0, 128, 255),
Color::Rgb(128, 0, 255),
],
}
}
pub fn pastel() -> Self {
Self {
colors: vec![
Color::Rgb(255, 179, 186), Color::Rgb(255, 223, 186), Color::Rgb(255, 255, 186), Color::Rgb(186, 255, 201), Color::Rgb(186, 225, 255), Color::Rgb(218, 186, 255), ],
}
}
pub fn sunset() -> Self {
Self {
colors: vec![
Color::Rgb(255, 94, 77), Color::Rgb(255, 154, 0), Color::Rgb(255, 206, 84), ],
}
}
pub fn ocean() -> Self {
Self {
colors: vec![
Color::Rgb(0, 105, 148), Color::Rgb(0, 168, 198), Color::Rgb(127, 219, 255), ],
}
}
pub fn forest() -> Self {
Self {
colors: vec![
Color::Rgb(34, 139, 34), Color::Rgb(50, 205, 50), Color::Rgb(144, 238, 144), ],
}
}
pub fn color_at(&self, position: f32) -> Color {
if self.colors.is_empty() {
return Color::Reset;
}
if self.colors.len() == 1 {
return self.colors[0];
}
let position = position.clamp(0.0, 1.0);
let segment_count = self.colors.len() - 1;
let segment_size = 1.0 / segment_count as f32;
let segment_index = ((position / segment_size).floor() as usize).min(segment_count - 1);
let segment_position = (position - segment_index as f32 * segment_size) / segment_size;
let start_color = &self.colors[segment_index];
let end_color = &self.colors[segment_index + 1];
interpolate_color(start_color, end_color, segment_position)
}
pub fn apply(&self, text: &str) -> Vec<(char, Color)> {
let chars: Vec<char> = text.chars().collect();
let len = chars.len();
if len == 0 {
return vec![];
}
chars
.into_iter()
.enumerate()
.map(|(i, c)| {
let position = if len == 1 {
0.0
} else {
i as f32 / (len - 1) as f32
};
(c, self.color_at(position))
})
.collect()
}
pub fn render(&self, text: &str) -> String {
use std::fmt::Write;
let colored_chars = self.apply(text);
let mut result = String::new();
for (c, color) in colored_chars {
let ansi_color = color.to_ansi_fg();
let _ = write!(result, "{}{}", ansi_color, c);
}
result.push_str("\x1b[0m");
result
}
pub fn len(&self) -> usize {
self.colors.len()
}
pub fn is_empty(&self) -> bool {
self.colors.is_empty()
}
pub fn push(mut self, color: Color) -> Self {
self.colors.push(color);
self
}
pub fn reverse(mut self) -> Self {
self.colors.reverse();
self
}
}
impl Default for Gradient {
fn default() -> Self {
Self::rainbow()
}
}
fn interpolate_color(start: &Color, end: &Color, t: f32) -> Color {
let (r1, g1, b1) = color_to_rgb(start);
let (r2, g2, b2) = color_to_rgb(end);
let r = lerp(r1 as f32, r2 as f32, t) as u8;
let g = lerp(g1 as f32, g2 as f32, t) as u8;
let b = lerp(b1 as f32, b2 as f32, t) as u8;
Color::Rgb(r, g, b)
}
fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t
}
fn color_to_rgb(color: &Color) -> (u8, u8, u8) {
match color {
Color::Rgb(r, g, b) => (*r, *g, *b),
Color::Black => (0, 0, 0),
Color::Red => (205, 0, 0),
Color::Green => (0, 205, 0),
Color::Yellow => (205, 205, 0),
Color::Blue => (0, 0, 238),
Color::Magenta => (205, 0, 205),
Color::Cyan => (0, 205, 205),
Color::White => (229, 229, 229),
Color::BrightBlack => (127, 127, 127),
Color::BrightRed => (255, 0, 0),
Color::BrightGreen => (0, 255, 0),
Color::BrightYellow => (255, 255, 0),
Color::BrightBlue => (92, 92, 255),
Color::BrightMagenta => (255, 0, 255),
Color::BrightCyan => (0, 255, 255),
Color::BrightWhite => (255, 255, 255),
Color::Ansi256(code) => ansi256_to_rgb(*code),
Color::Reset => (255, 255, 255),
}
}
fn ansi256_to_rgb(code: u8) -> (u8, u8, u8) {
match code {
0..=15 => {
let standard = [
(0, 0, 0),
(128, 0, 0),
(0, 128, 0),
(128, 128, 0),
(0, 0, 128),
(128, 0, 128),
(0, 128, 128),
(192, 192, 192),
(128, 128, 128),
(255, 0, 0),
(0, 255, 0),
(255, 255, 0),
(0, 0, 255),
(255, 0, 255),
(0, 255, 255),
(255, 255, 255),
];
standard[code as usize]
}
16..=231 => {
let code = code - 16;
let r = (code / 36) % 6;
let g = (code / 6) % 6;
let b = code % 6;
let to_rgb = |v: u8| if v == 0 { 0 } else { 55 + v * 40 };
(to_rgb(r), to_rgb(g), to_rgb(b))
}
232..=255 => {
let gray = 8 + (code - 232) * 10;
(gray, gray, gray)
}
}
}
pub fn rainbow(text: &str) -> String {
Gradient::rainbow().render(text)
}
pub fn gradient(text: &str, start: Color, end: Color) -> String {
Gradient::from_two(start, end).render(text)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gradient_creation() {
let g = Gradient::new(vec![Color::Red, Color::Blue]);
assert_eq!(g.len(), 2);
}
#[test]
fn test_gradient_from_two() {
let g = Gradient::from_two(Color::Red, Color::Blue);
assert_eq!(g.len(), 2);
}
#[test]
fn test_gradient_rainbow() {
let g = Gradient::rainbow();
assert_eq!(g.len(), 6);
}
#[test]
fn test_gradient_presets() {
assert!(!Gradient::warm().is_empty());
assert!(!Gradient::cool().is_empty());
assert!(!Gradient::pastel().is_empty());
assert!(!Gradient::sunset().is_empty());
assert!(!Gradient::ocean().is_empty());
assert!(!Gradient::forest().is_empty());
}
#[test]
fn test_gradient_color_at() {
let g = Gradient::from_two(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255));
let start = g.color_at(0.0);
assert_eq!(start, Color::Rgb(0, 0, 0));
let end = g.color_at(1.0);
assert_eq!(end, Color::Rgb(255, 255, 255));
let mid = g.color_at(0.5);
if let Color::Rgb(r, g, b) = mid {
assert!((r as i32 - 127).abs() <= 1);
assert!((g as i32 - 127).abs() <= 1);
assert!((b as i32 - 127).abs() <= 1);
} else {
panic!("Expected RGB color");
}
}
#[test]
fn test_gradient_apply() {
let g = Gradient::from_two(Color::Red, Color::Blue);
let result = g.apply("ABC");
assert_eq!(result.len(), 3);
assert_eq!(result[0].0, 'A');
assert_eq!(result[1].0, 'B');
assert_eq!(result[2].0, 'C');
}
#[test]
fn test_gradient_apply_empty() {
let g = Gradient::rainbow();
let result = g.apply("");
assert!(result.is_empty());
}
#[test]
fn test_gradient_apply_single_char() {
let g = Gradient::rainbow();
let result = g.apply("X");
assert_eq!(result.len(), 1);
}
#[test]
fn test_gradient_render() {
let g = Gradient::from_two(Color::Red, Color::Blue);
let result = g.render("Hi");
assert!(result.contains("\x1b["));
assert!(result.ends_with("\x1b[0m"));
}
#[test]
fn test_gradient_push() {
let g = Gradient::new(vec![Color::Red]).push(Color::Blue);
assert_eq!(g.len(), 2);
}
#[test]
fn test_gradient_reverse() {
let g = Gradient::new(vec![Color::Red, Color::Blue]).reverse();
assert_eq!(g.colors[0], Color::Blue);
assert_eq!(g.colors[1], Color::Red);
}
#[test]
fn test_rainbow_function() {
let result = rainbow("Hello");
assert!(result.contains("\x1b["));
}
#[test]
fn test_gradient_function() {
let result = gradient("Hello", Color::Red, Color::Blue);
assert!(result.contains("\x1b["));
}
#[test]
fn test_interpolate_color() {
let start = Color::Rgb(0, 0, 0);
let end = Color::Rgb(100, 100, 100);
let mid = interpolate_color(&start, &end, 0.5);
if let Color::Rgb(r, g, b) = mid {
assert_eq!(r, 50);
assert_eq!(g, 50);
assert_eq!(b, 50);
}
}
#[test]
fn test_color_to_rgb() {
assert_eq!(color_to_rgb(&Color::Black), (0, 0, 0));
assert_eq!(color_to_rgb(&Color::Rgb(100, 150, 200)), (100, 150, 200));
}
#[test]
fn test_ansi256_to_rgb() {
assert_eq!(ansi256_to_rgb(0), (0, 0, 0));
assert_eq!(ansi256_to_rgb(15), (255, 255, 255));
let (r, g, b) = ansi256_to_rgb(240);
assert_eq!(r, g);
assert_eq!(g, b);
}
#[test]
fn test_empty_gradient() {
let g = Gradient::new(vec![]);
assert!(g.is_empty());
assert_eq!(g.color_at(0.5), Color::Reset);
}
#[test]
fn test_single_color_gradient() {
let g = Gradient::new(vec![Color::Red]);
assert_eq!(g.color_at(0.0), Color::Red);
assert_eq!(g.color_at(0.5), Color::Red);
assert_eq!(g.color_at(1.0), Color::Red);
}
}