modcli/output/
gradient.rs

1use crossterm::style::{Color, Stylize};
2
3/// Generates a horizontal gradient between two colors.
4/// This can be used for printing rainbow text or gradual transitions.
5/// This function returns a single string with the gradient applied.
6/// The text is split into characters, and each character is colored
7/// according to its position in the gradient.
8/// The gradient is calculated by interpolating between the start and end colors
9/// based on the character's index.
10/// The result is a string where each character is styled with its corresponding color.
11pub fn generate(text: &str, start: Color, end: Color) -> String {
12    let chars: Vec<char> = text.chars().collect();
13    let steps = chars.len().max(1);
14    let mut result = String::with_capacity(text.len() * 10); // estimate
15
16    for (i, c) in chars.iter().enumerate() {
17        let r = interpolate(get_r(&start), get_r(&end), i, steps);
18        let g = interpolate(get_g(&start), get_g(&end), i, steps);
19        let b = interpolate(get_b(&start), get_b(&end), i, steps);
20        let color = Color::Rgb { r, g, b };
21        result.push_str(&c.to_string().with(color).to_string());
22    }
23
24    /// Common easing modes for gradients
25    #[allow(dead_code)]
26    #[derive(Clone, Copy)]
27    pub enum Easing {
28        Linear,
29        EaseIn,
30        EaseOut,
31        EaseInOut,
32    }
33
34    #[allow(dead_code)]
35    #[inline(always)]
36    fn apply_easing(t: f32, mode: Easing) -> f32 {
37        match mode {
38            Easing::Linear => t,
39            Easing::EaseIn => t * t,
40            Easing::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
41            Easing::EaseInOut => {
42                if t < 0.5 {
43                    2.0 * t * t
44                } else {
45                    1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
46                }
47            }
48        }
49    }
50
51    /// Multi-color gradient with easing across the full text length.
52    /// Easing is applied to the overall position before segment selection.
53    #[allow(dead_code)]
54    pub fn multi_color_eased(text: &str, colors: Vec<Color>, ease: Easing) -> String {
55        let chars: Vec<char> = text.chars().collect();
56        let steps = chars.len().max(1);
57        let segments = colors.len().saturating_sub(1).max(1);
58        let mut result = String::with_capacity(text.len() * 10);
59
60        for (i, c) in chars.iter().enumerate() {
61            let t_raw = i as f32 / (steps - 1).max(1) as f32;
62            let t = apply_easing(t_raw, ease).clamp(0.0, 1.0);
63            let seg_float = t * segments as f32;
64            let seg = seg_float.floor() as usize;
65            let seg_t = seg_float - seg as f32;
66
67            let from = colors.get(seg).unwrap_or(&colors[0]);
68            let to = colors.get(seg + 1).unwrap_or(from);
69
70            let r = interpolate(get_r(from), get_r(to), (seg_t * 100.0) as usize, 100);
71            let g = interpolate(get_g(from), get_g(to), (seg_t * 100.0) as usize, 100);
72            let b = interpolate(get_b(from), get_b(to), (seg_t * 100.0) as usize, 100);
73
74            let color = Color::Rgb { r, g, b };
75            result.push_str(&c.to_string().with(color).to_string());
76        }
77
78        result
79    }
80
81    /// A small set of palette stops for viridis (approximation)
82    #[allow(dead_code)]
83    pub fn palette_viridis() -> Vec<Color> {
84        vec![
85            Color::Rgb { r: 68, g: 1, b: 84 },
86            Color::Rgb {
87                r: 59,
88                g: 82,
89                b: 139,
90            },
91            Color::Rgb {
92                r: 33,
93                g: 145,
94                b: 140,
95            },
96            Color::Rgb {
97                r: 94,
98                g: 201,
99                b: 97,
100            },
101            Color::Rgb {
102                r: 253,
103                g: 231,
104                b: 37,
105            },
106        ]
107    }
108
109    /// A small set of palette stops for magma (approximation)
110    #[allow(dead_code)]
111    pub fn palette_magma() -> Vec<Color> {
112        vec![
113            Color::Rgb { r: 0, g: 0, b: 4 },
114            Color::Rgb {
115                r: 73,
116                g: 15,
117                b: 99,
118            },
119            Color::Rgb {
120                r: 187,
121                g: 55,
122                b: 84,
123            },
124            Color::Rgb {
125                r: 249,
126                g: 142,
127                b: 8,
128            },
129            Color::Rgb {
130                r: 252,
131                g: 253,
132                b: 191,
133            },
134        ]
135    }
136
137    result
138}
139
140/// Alias for generate()
141/// This function is a convenience function that calls the `generate` function
142/// with the provided text and colors.
143/// It returns the generated gradient string.
144#[inline(always)]
145pub fn two_color(text: &str, start: Color, end: Color) -> String {
146    generate(text, start, end)
147}
148
149/// Generates a 3-color gradient (start -> mid, mid -> end)
150/// This function creates a gradient that transitions from the start color to the mid color,
151/// and then from the mid color to the end color.
152pub fn three_color(text: &str, start: Color, mid: Color, end: Color) -> String {
153    let chars: Vec<char> = text.chars().collect();
154    let total = chars.len().max(1);
155    let midpoint = total / 2;
156    let mut result = String::with_capacity(text.len() * 10);
157
158    for (i, c) in chars.iter().enumerate() {
159        let (from, to, step, steps) = if i < midpoint {
160            (start, mid, i, midpoint)
161        } else {
162            (mid, end, i - midpoint, total - midpoint)
163        };
164
165        let r = interpolate(get_r(&from), get_r(&to), step, steps);
166        let g = interpolate(get_g(&from), get_g(&to), step, steps);
167        let b = interpolate(get_b(&from), get_b(&to), step, steps);
168        let color = Color::Rgb { r, g, b };
169        result.push_str(&c.to_string().with(color).to_string());
170    }
171
172    result
173}
174
175/// Generates a gradient from a vector of colors, distributed across text.
176/// This function creates a gradient that transitions through multiple colors.
177/// The colors are evenly distributed across the text.
178pub fn multi_color(text: &str, colors: Vec<Color>) -> String {
179    let chars: Vec<char> = text.chars().collect();
180    let steps = chars.len().max(1);
181    let segments = colors.len().saturating_sub(1).max(1);
182    let mut result = String::with_capacity(text.len() * 10);
183
184    for (i, c) in chars.iter().enumerate() {
185        let t = i as f32 / (steps - 1).max(1) as f32;
186        let seg_float = t * segments as f32;
187        let seg = seg_float.floor() as usize;
188        let seg_t = seg_float - seg as f32;
189
190        let from = colors.get(seg).unwrap_or(&colors[0]);
191        let to = colors.get(seg + 1).unwrap_or(from);
192
193        let r = interpolate(get_r(from), get_r(to), (seg_t * 100.0) as usize, 100);
194        let g = interpolate(get_g(from), get_g(to), (seg_t * 100.0) as usize, 100);
195        let b = interpolate(get_b(from), get_b(to), (seg_t * 100.0) as usize, 100);
196
197        let color = Color::Rgb { r, g, b };
198        result.push_str(&c.to_string().with(color).to_string());
199    }
200
201    result
202}
203
204// Internal RGB helpers
205/// Gets the red, green, and blue components of a color.
206/// These functions extract the respective color components from a Color.
207/// If the color is not an RGB color, it returns 255.
208/// This is useful for interpolating colors in the gradient.
209/// The functions use pattern matching to check the color type.
210/// Get the red component of a color.
211/// This function extracts the red component from a Color.
212#[inline(always)]
213fn get_r(c: &Color) -> u8 {
214    match c {
215        Color::Rgb { r, .. } => *r,
216        _ => 255,
217    }
218}
219
220/// Get the green component of a color.
221/// This function extracts the green component from a Color.
222#[inline(always)]
223fn get_g(c: &Color) -> u8 {
224    match c {
225        Color::Rgb { g, .. } => *g,
226        _ => 255,
227    }
228}
229
230/// Get the blue component of a color.
231/// This function extracts the blue component from a Color.
232#[inline(always)]
233fn get_b(c: &Color) -> u8 {
234    match c {
235        Color::Rgb { b, .. } => *b,
236        _ => 255,
237    }
238}
239
240/// Interpolates between two color components.
241/// This function calculates the interpolated value between two color components.
242/// It takes the start and end values, the current step, and the total number of steps.
243/// The interpolation is done using a linear formula.
244/// The result is rounded to the nearest integer and returned as a u8.
245#[inline(always)]
246fn interpolate(start: u8, end: u8, step: usize, total: usize) -> u8 {
247    let start_f = start as f32;
248    let end_f = end as f32;
249    let ratio = step as f32 / (total - 1).max(1) as f32;
250    (start_f + (end_f - start_f) * ratio).round() as u8
251}