use ratatui::style::Color;
use std::time::{Duration, Instant};
pub fn lerp(start: f64, end: f64, t: f64) -> f64 {
start + (end - start) * t.clamp(0.0, 1.0)
}
fn ease_in_out_cubic(t: f64) -> f64 {
let t = t.clamp(0.0, 1.0);
if t < 0.5 {
4.0 * t * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
}
}
fn ease_out_cubic(t: f64) -> f64 {
let t = t.clamp(0.0, 1.0);
1.0 - (1.0 - t).powi(3)
}
pub fn ease_value(start: f64, end: f64, progress: f64) -> f64 {
let eased_progress = ease_in_out_cubic(progress);
lerp(start, end, eased_progress)
}
pub fn calc_progress(elapsed: Duration, total_duration: Duration) -> f64 {
if total_duration.as_millis() == 0 {
return 1.0;
}
(elapsed.as_millis() as f64 / total_duration.as_millis() as f64).clamp(0.0, 1.0)
}
pub fn pulse_opacity(elapsed: Duration, cycle_duration: Duration) -> f64 {
let t = (elapsed.as_millis() % cycle_duration.as_millis()) as f64
/ cycle_duration.as_millis() as f64;
let sine_value = (t * 2.0 * std::f64::consts::PI).sin();
0.3 + (sine_value + 1.0) / 2.0 * 0.7
}
pub fn pulse_value(elapsed: Duration, cycle_duration: Duration, min: f64, max: f64) -> f64 {
let t = (elapsed.as_millis() % cycle_duration.as_millis()) as f64
/ cycle_duration.as_millis() as f64;
let sine_value = (t * 2.0 * std::f64::consts::PI).sin();
min + (sine_value + 1.0) / 2.0 * (max - min)
}
pub fn breathe_opacity(elapsed: Duration) -> f64 {
pulse_opacity(elapsed, Duration::from_millis(2000))
}
pub fn slide_panel(start_pos: f64, target_pos: f64, progress: f64) -> f64 {
let eased_progress = ease_out_cubic(progress);
lerp(start_pos, target_pos, eased_progress)
}
pub fn animate_size(current: u16, target: u16, progress: f64) -> u16 {
ease_value(current as f64, target as f64, progress) as u16
}
pub fn lerp_color(start: Color, end: Color, progress: f64) -> Color {
match (start, end) {
(Color::Rgb(r1, g1, b1), Color::Rgb(r2, g2, b2)) => {
let r = lerp(r1 as f64, r2 as f64, progress) as u8;
let g = lerp(g1 as f64, g2 as f64, progress) as u8;
let b = lerp(b1 as f64, b2 as f64, progress) as u8;
Color::Rgb(r, g, b)
}
_ => end, }
}
pub fn pulse_color(
color1: Color,
color2: Color,
elapsed: Duration,
cycle_duration: Duration,
) -> Color {
let progress = pulse_value(elapsed, cycle_duration, 0.0, 1.0);
lerp_color(color1, color2, progress)
}
#[derive(Clone)]
pub struct Animation {
pub start_time: Instant,
pub duration: Duration,
pub start_value: f64,
pub end_value: f64,
}
impl Animation {
pub fn new(start_value: f64, end_value: f64, duration: Duration) -> Self {
Self {
start_time: Instant::now(),
duration,
start_value,
end_value,
}
}
pub fn current_value(&self) -> f64 {
let elapsed = self.start_time.elapsed();
let progress = calc_progress(elapsed, self.duration);
ease_value(self.start_value, self.end_value, progress)
}
pub fn is_complete(&self) -> bool {
self.start_time.elapsed() >= self.duration
}
pub fn progress(&self) -> f64 {
calc_progress(self.start_time.elapsed(), self.duration)
}
pub fn restart(&mut self, new_end_value: f64) {
self.start_value = self.current_value();
self.end_value = new_end_value;
self.start_time = Instant::now();
}
}
#[derive(Clone)]
pub struct ViewTransition {
pub animation: Animation,
pub direction: TransitionDirection,
}
#[derive(Clone, Copy, PartialEq)]
pub enum TransitionDirection {
SlideLeft,
SlideRight,
FadeIn,
FadeOut,
}
impl ViewTransition {
pub fn new(direction: TransitionDirection, duration: Duration) -> Self {
let (start, end) = match direction {
TransitionDirection::SlideLeft => (100.0, 0.0),
TransitionDirection::SlideRight => (-100.0, 0.0),
TransitionDirection::FadeIn => (0.0, 1.0),
TransitionDirection::FadeOut => (1.0, 0.0),
};
Self {
animation: Animation::new(start, end, duration),
direction,
}
}
pub fn current_offset(&self) -> f64 {
self.animation.current_value()
}
pub fn is_complete(&self) -> bool {
self.animation.is_complete()
}
}
pub struct PulsingIndicator {
pub start_time: Instant,
pub cycle_duration: Duration,
pub min_opacity: f64,
pub max_opacity: f64,
}
impl PulsingIndicator {
pub fn new() -> Self {
Self {
start_time: Instant::now(),
cycle_duration: Duration::from_millis(1500),
min_opacity: 0.3,
max_opacity: 1.0,
}
}
pub fn with_speed(mut self, cycle_duration: Duration) -> Self {
self.cycle_duration = cycle_duration;
self
}
pub fn current_opacity(&self) -> f64 {
pulse_value(
self.start_time.elapsed(),
self.cycle_duration,
self.min_opacity,
self.max_opacity,
)
}
pub fn reset(&mut self) {
self.start_time = Instant::now();
}
}
pub struct TokenStream {
pub full_text: String,
pub start_time: Instant,
pub chars_per_second: usize,
}
impl TokenStream {
pub fn new(text: String, chars_per_second: usize) -> Self {
Self {
full_text: text,
start_time: Instant::now(),
chars_per_second,
}
}
pub fn visible_text(&self) -> &str {
let elapsed = self.start_time.elapsed().as_millis() as usize;
let chars_to_show = (elapsed * self.chars_per_second / 1000).min(self.full_text.len());
&self.full_text[..chars_to_show]
}
pub fn is_complete(&self) -> bool {
let elapsed = self.start_time.elapsed().as_millis() as usize;
let chars_shown = elapsed * self.chars_per_second / 1000;
chars_shown >= self.full_text.len()
}
pub fn progress(&self) -> f64 {
let elapsed = self.start_time.elapsed().as_millis() as usize;
let chars_shown = elapsed * self.chars_per_second / 1000;
(chars_shown as f64 / self.full_text.len() as f64).min(1.0)
}
}
pub struct AnimatedSpinner {
frames: Vec<&'static str>,
current_frame: usize,
last_update: Instant,
frame_duration: Duration,
}
impl AnimatedSpinner {
pub fn braille() -> Self {
Self {
frames: vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
current_frame: 0,
last_update: Instant::now(),
frame_duration: Duration::from_millis(80),
}
}
pub fn dots() -> Self {
Self {
frames: vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
current_frame: 0,
last_update: Instant::now(),
frame_duration: Duration::from_millis(80),
}
}
pub fn circle() -> Self {
Self {
frames: vec!["◐", "◓", "◑", "◒"],
current_frame: 0,
last_update: Instant::now(),
frame_duration: Duration::from_millis(100),
}
}
pub fn arrow() -> Self {
Self {
frames: vec!["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"],
current_frame: 0,
last_update: Instant::now(),
frame_duration: Duration::from_millis(100),
}
}
pub fn tick(&mut self) {
if self.last_update.elapsed() >= self.frame_duration {
self.current_frame = (self.current_frame + 1) % self.frames.len();
self.last_update = Instant::now();
}
}
pub fn current(&self) -> &str {
self.frames[self.current_frame]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lerp() {
assert_eq!(lerp(0.0, 100.0, 0.0), 0.0);
assert_eq!(lerp(0.0, 100.0, 0.5), 50.0);
assert_eq!(lerp(0.0, 100.0, 1.0), 100.0);
}
#[test]
fn test_calc_progress() {
let total = Duration::from_secs(1);
assert_eq!(calc_progress(Duration::from_millis(0), total), 0.0);
assert_eq!(calc_progress(Duration::from_millis(500), total), 0.5);
assert_eq!(calc_progress(Duration::from_millis(1000), total), 1.0);
}
#[test]
fn test_animation() {
let mut anim = Animation::new(0.0, 100.0, Duration::from_millis(100));
assert!(!anim.is_complete());
assert!(anim.current_value() >= 0.0 && anim.current_value() <= 100.0);
}
}