1use ratatui::style::Color;
2use std::time::{Duration, Instant};
3
4pub fn lerp(start: f64, end: f64, t: f64) -> f64 {
13 start + (end - start) * t.clamp(0.0, 1.0)
14}
15
16fn ease_in_out_cubic(t: f64) -> f64 {
18 let t = t.clamp(0.0, 1.0);
19 if t < 0.5 {
20 4.0 * t * t * t
21 } else {
22 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
23 }
24}
25
26fn ease_out_cubic(t: f64) -> f64 {
28 let t = t.clamp(0.0, 1.0);
29 1.0 - (1.0 - t).powi(3)
30}
31
32pub fn ease_value(start: f64, end: f64, progress: f64) -> f64 {
35 let eased_progress = ease_in_out_cubic(progress);
36 lerp(start, end, eased_progress)
37}
38
39pub fn calc_progress(elapsed: Duration, total_duration: Duration) -> f64 {
41 if total_duration.as_millis() == 0 {
42 return 1.0;
43 }
44 (elapsed.as_millis() as f64 / total_duration.as_millis() as f64).clamp(0.0, 1.0)
45}
46
47pub fn pulse_opacity(elapsed: Duration, cycle_duration: Duration) -> f64 {
54 let t = (elapsed.as_millis() % cycle_duration.as_millis()) as f64
55 / cycle_duration.as_millis() as f64;
56
57 let sine_value = (t * 2.0 * std::f64::consts::PI).sin();
59 0.3 + (sine_value + 1.0) / 2.0 * 0.7
61}
62
63pub fn pulse_value(elapsed: Duration, cycle_duration: Duration, min: f64, max: f64) -> f64 {
65 let t = (elapsed.as_millis() % cycle_duration.as_millis()) as f64
66 / cycle_duration.as_millis() as f64;
67
68 let sine_value = (t * 2.0 * std::f64::consts::PI).sin();
69 min + (sine_value + 1.0) / 2.0 * (max - min)
70}
71
72pub fn breathe_opacity(elapsed: Duration) -> f64 {
74 pulse_opacity(elapsed, Duration::from_millis(2000))
75}
76
77pub fn slide_panel(start_pos: f64, target_pos: f64, progress: f64) -> f64 {
83 let eased_progress = ease_out_cubic(progress);
84 lerp(start_pos, target_pos, eased_progress)
85}
86
87pub fn animate_size(current: u16, target: u16, progress: f64) -> u16 {
89 ease_value(current as f64, target as f64, progress) as u16
90}
91
92pub fn lerp_color(start: Color, end: Color, progress: f64) -> Color {
98 match (start, end) {
99 (Color::Rgb(r1, g1, b1), Color::Rgb(r2, g2, b2)) => {
100 let r = lerp(r1 as f64, r2 as f64, progress) as u8;
101 let g = lerp(g1 as f64, g2 as f64, progress) as u8;
102 let b = lerp(b1 as f64, b2 as f64, progress) as u8;
103 Color::Rgb(r, g, b)
104 }
105 _ => end, }
107}
108
109pub fn pulse_color(
111 color1: Color,
112 color2: Color,
113 elapsed: Duration,
114 cycle_duration: Duration,
115) -> Color {
116 let progress = pulse_value(elapsed, cycle_duration, 0.0, 1.0);
117 lerp_color(color1, color2, progress)
118}
119
120#[derive(Clone)]
126pub struct Animation {
127 pub start_time: Instant,
128 pub duration: Duration,
129 pub start_value: f64,
130 pub end_value: f64,
131}
132
133impl Animation {
134 pub fn new(start_value: f64, end_value: f64, duration: Duration) -> Self {
135 Self {
136 start_time: Instant::now(),
137 duration,
138 start_value,
139 end_value,
140 }
141 }
142
143 pub fn current_value(&self) -> f64 {
145 let elapsed = self.start_time.elapsed();
146 let progress = calc_progress(elapsed, self.duration);
147 ease_value(self.start_value, self.end_value, progress)
148 }
149
150 pub fn is_complete(&self) -> bool {
152 self.start_time.elapsed() >= self.duration
153 }
154
155 pub fn progress(&self) -> f64 {
157 calc_progress(self.start_time.elapsed(), self.duration)
158 }
159
160 pub fn restart(&mut self, new_end_value: f64) {
162 self.start_value = self.current_value();
163 self.end_value = new_end_value;
164 self.start_time = Instant::now();
165 }
166}
167
168#[derive(Clone)]
170pub struct ViewTransition {
171 pub animation: Animation,
172 pub direction: TransitionDirection,
173}
174
175#[derive(Clone, Copy, PartialEq)]
176pub enum TransitionDirection {
177 SlideLeft,
178 SlideRight,
179 FadeIn,
180 FadeOut,
181}
182
183impl ViewTransition {
184 pub fn new(direction: TransitionDirection, duration: Duration) -> Self {
185 let (start, end) = match direction {
186 TransitionDirection::SlideLeft => (100.0, 0.0),
187 TransitionDirection::SlideRight => (-100.0, 0.0),
188 TransitionDirection::FadeIn => (0.0, 1.0),
189 TransitionDirection::FadeOut => (1.0, 0.0),
190 };
191
192 Self {
193 animation: Animation::new(start, end, duration),
194 direction,
195 }
196 }
197
198 pub fn current_offset(&self) -> f64 {
199 self.animation.current_value()
200 }
201
202 pub fn is_complete(&self) -> bool {
203 self.animation.is_complete()
204 }
205}
206
207pub struct PulsingIndicator {
209 pub start_time: Instant,
210 pub cycle_duration: Duration,
211 pub min_opacity: f64,
212 pub max_opacity: f64,
213}
214
215impl PulsingIndicator {
216 pub fn new() -> Self {
217 Self {
218 start_time: Instant::now(),
219 cycle_duration: Duration::from_millis(1500),
220 min_opacity: 0.3,
221 max_opacity: 1.0,
222 }
223 }
224
225 pub fn with_speed(mut self, cycle_duration: Duration) -> Self {
226 self.cycle_duration = cycle_duration;
227 self
228 }
229
230 pub fn current_opacity(&self) -> f64 {
231 pulse_value(
232 self.start_time.elapsed(),
233 self.cycle_duration,
234 self.min_opacity,
235 self.max_opacity,
236 )
237 }
238
239 pub fn reset(&mut self) {
240 self.start_time = Instant::now();
241 }
242}
243
244pub struct TokenStream {
246 pub full_text: String,
247 pub start_time: Instant,
248 pub chars_per_second: usize,
249}
250
251impl TokenStream {
252 pub fn new(text: String, chars_per_second: usize) -> Self {
253 Self {
254 full_text: text,
255 start_time: Instant::now(),
256 chars_per_second,
257 }
258 }
259
260 pub fn visible_text(&self) -> &str {
262 let elapsed = self.start_time.elapsed().as_millis() as usize;
263 let chars_to_show = (elapsed * self.chars_per_second / 1000).min(self.full_text.len());
264 &self.full_text[..chars_to_show]
265 }
266
267 pub fn is_complete(&self) -> bool {
269 let elapsed = self.start_time.elapsed().as_millis() as usize;
270 let chars_shown = elapsed * self.chars_per_second / 1000;
271 chars_shown >= self.full_text.len()
272 }
273
274 pub fn progress(&self) -> f64 {
276 let elapsed = self.start_time.elapsed().as_millis() as usize;
277 let chars_shown = elapsed * self.chars_per_second / 1000;
278 (chars_shown as f64 / self.full_text.len() as f64).min(1.0)
279 }
280}
281
282pub struct AnimatedSpinner {
288 frames: Vec<&'static str>,
289 current_frame: usize,
290 last_update: Instant,
291 frame_duration: Duration,
292}
293
294impl AnimatedSpinner {
295 pub fn braille() -> Self {
297 Self {
298 frames: vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
299 current_frame: 0,
300 last_update: Instant::now(),
301 frame_duration: Duration::from_millis(80),
302 }
303 }
304
305 pub fn dots() -> Self {
307 Self {
308 frames: vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
309 current_frame: 0,
310 last_update: Instant::now(),
311 frame_duration: Duration::from_millis(80),
312 }
313 }
314
315 pub fn circle() -> Self {
317 Self {
318 frames: vec!["◐", "◓", "◑", "◒"],
319 current_frame: 0,
320 last_update: Instant::now(),
321 frame_duration: Duration::from_millis(100),
322 }
323 }
324
325 pub fn arrow() -> Self {
327 Self {
328 frames: vec!["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"],
329 current_frame: 0,
330 last_update: Instant::now(),
331 frame_duration: Duration::from_millis(100),
332 }
333 }
334
335 pub fn tick(&mut self) {
336 if self.last_update.elapsed() >= self.frame_duration {
337 self.current_frame = (self.current_frame + 1) % self.frames.len();
338 self.last_update = Instant::now();
339 }
340 }
341
342 pub fn current(&self) -> &str {
343 self.frames[self.current_frame]
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350
351 #[test]
352 fn test_lerp() {
353 assert_eq!(lerp(0.0, 100.0, 0.0), 0.0);
354 assert_eq!(lerp(0.0, 100.0, 0.5), 50.0);
355 assert_eq!(lerp(0.0, 100.0, 1.0), 100.0);
356 }
357
358 #[test]
359 fn test_calc_progress() {
360 let total = Duration::from_secs(1);
361 assert_eq!(calc_progress(Duration::from_millis(0), total), 0.0);
362 assert_eq!(calc_progress(Duration::from_millis(500), total), 0.5);
363 assert_eq!(calc_progress(Duration::from_millis(1000), total), 1.0);
364 }
365
366 #[test]
367 fn test_animation() {
368 let mut anim = Animation::new(0.0, 100.0, Duration::from_millis(100));
369 assert!(!anim.is_complete());
370 assert!(anim.current_value() >= 0.0 && anim.current_value() <= 100.0);
371 }
372}