Skip to main content

armas_basic/animation/
mod.rs

1//! Animation utilities for smooth, physics-based motion.
2//!
3//! Provides timed animations with easing, spring physics, staggered
4//! entrance effects, and looping support.
5
6/// Easing functions for smooth animations
7pub mod easing;
8/// Interpolation trait for animating values
9pub mod interpolate;
10pub mod momentum;
11pub mod staggered;
12pub mod velocity_drag;
13
14pub use easing::EasingFunction;
15pub use interpolate::Interpolate;
16pub use momentum::{
17    ContinuousWithMomentum, MomentumBehavior, MomentumPosition, SnapToPageBoundaries,
18};
19pub use staggered::{AnimationSequence, LoopMode, LoopingAnimation, StaggeredAnimation};
20pub use velocity_drag::{DoubleClickReset, DragMode, VelocityDrag, VelocityDragConfig};
21
22/// Animation state
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum AnimationState {
25    /// Animation hasn't started yet
26    NotStarted,
27    /// Animation is currently running
28    Running,
29    /// Animation is paused
30    Paused,
31    /// Animation has completed
32    Completed,
33}
34
35/// A generic animation that interpolates between two values over time
36///
37/// # Example
38///
39/// ```rust
40/// use armas_basic::animation::{Animation, EasingFunction};
41///
42/// let mut anim = Animation::new(0.0_f32, 1.0, 0.3)
43///     .easing(EasingFunction::EaseOut);
44/// anim.start();
45/// anim.update(0.15);
46/// let value = anim.value(); // interpolated between 0.0 and 1.0
47/// ```
48#[derive(Debug, Clone)]
49pub struct Animation<T: Interpolate> {
50    /// Starting value
51    pub start: T,
52    /// Target value
53    pub end: T,
54    /// Duration of the animation in seconds
55    pub duration: f32,
56    /// Elapsed time in seconds
57    pub elapsed: f32,
58    /// Easing function to apply
59    pub easing: EasingFunction,
60    /// Current state of the animation
61    pub state: AnimationState,
62}
63
64impl<T: Interpolate> Animation<T> {
65    /// Create a new animation from start to end over duration
66    pub const fn new(start: T, end: T, duration: f32) -> Self {
67        Self {
68            start,
69            end,
70            duration,
71            elapsed: 0.0,
72            easing: EasingFunction::EaseInOut,
73            state: AnimationState::NotStarted,
74        }
75    }
76
77    /// Set the easing function
78    #[must_use]
79    pub const fn easing(mut self, easing: EasingFunction) -> Self {
80        self.easing = easing;
81        self
82    }
83
84    /// Start the animation
85    pub const fn start(&mut self) {
86        self.state = AnimationState::Running;
87        self.elapsed = 0.0;
88    }
89
90    /// Pause the animation
91    pub fn pause(&mut self) {
92        if self.state == AnimationState::Running {
93            self.state = AnimationState::Paused;
94        }
95    }
96
97    /// Resume the animation
98    pub fn resume(&mut self) {
99        if self.state == AnimationState::Paused {
100            self.state = AnimationState::Running;
101        }
102    }
103
104    /// Reset the animation to the beginning
105    pub const fn reset(&mut self) {
106        self.elapsed = 0.0;
107        self.state = AnimationState::NotStarted;
108    }
109
110    /// Update the animation with delta time
111    pub fn update(&mut self, dt: f32) {
112        if self.state != AnimationState::Running {
113            return;
114        }
115
116        self.elapsed += dt;
117        if self.elapsed >= self.duration {
118            self.elapsed = self.duration;
119            self.state = AnimationState::Completed;
120        }
121    }
122
123    /// Get the current value of the animation
124    pub fn value(&self) -> T {
125        let t = if self.duration <= 0.0 {
126            1.0
127        } else {
128            (self.elapsed / self.duration).clamp(0.0, 1.0)
129        };
130
131        let eased_t = self.easing.apply(t);
132        self.start.interpolate(&self.end, eased_t)
133    }
134
135    /// Get the normalized progress (0.0 to 1.0)
136    pub fn progress(&self) -> f32 {
137        if self.duration <= 0.0 {
138            1.0
139        } else {
140            (self.elapsed / self.duration).clamp(0.0, 1.0)
141        }
142    }
143
144    /// Check if the animation is complete
145    pub fn is_complete(&self) -> bool {
146        self.state == AnimationState::Completed
147    }
148
149    /// Check if the animation is running
150    pub fn is_running(&self) -> bool {
151        self.state == AnimationState::Running
152    }
153}
154
155/// Spring-based animation for smooth, physics-based motion
156///
157/// This uses a damped spring physics model for natural-looking animations.
158/// Unlike timed animations, springs don't have a fixed duration - they settle over time.
159///
160/// # Example
161///
162/// ```rust
163/// use armas_basic::animation::SpringAnimation;
164///
165/// let mut spring = SpringAnimation::new(0.0, 1.0)
166///     .params(200.0, 20.0);
167/// spring.update(0.016); // one frame at 60fps
168/// let current = spring.value;
169/// ```
170#[derive(Debug, Clone)]
171pub struct SpringAnimation {
172    /// Current value
173    pub value: f32,
174    /// Current velocity
175    pub velocity: f32,
176    /// Target value
177    pub target: f32,
178    /// Spring stiffness (higher = faster oscillation, typical: 100-300)
179    pub stiffness: f32,
180    /// Spring damping (higher = less oscillation, typical: 10-30)
181    pub damping: f32,
182}
183
184impl SpringAnimation {
185    /// Create a new spring animation
186    #[must_use]
187    pub const fn new(initial: f32, target: f32) -> Self {
188        Self {
189            value: initial,
190            velocity: 0.0,
191            target,
192            stiffness: 200.0,
193            damping: 20.0,
194        }
195    }
196
197    /// Set spring parameters
198    #[must_use]
199    pub const fn params(mut self, stiffness: f32, damping: f32) -> Self {
200        self.stiffness = stiffness;
201        self.damping = damping;
202        self
203    }
204
205    /// Update the spring simulation using semi-implicit Euler integration
206    pub fn update(&mut self, dt: f32) {
207        // Spring force: F = -k * (x - target)
208        let spring_force = -self.stiffness * (self.value - self.target);
209
210        // Damping force: F = -c * v
211        let damping_force = -self.damping * self.velocity;
212
213        // Total force and acceleration (assuming mass = 1)
214        let acceleration = spring_force + damping_force;
215
216        // Semi-implicit Euler integration (more stable than explicit Euler)
217        self.velocity += acceleration * dt;
218        self.value += self.velocity * dt;
219    }
220
221    /// Set a new target value
222    pub const fn set_target(&mut self, target: f32) {
223        self.target = target;
224    }
225
226    /// Check if the spring has approximately settled at the target
227    #[must_use]
228    pub fn is_settled(&self, position_threshold: f32, velocity_threshold: f32) -> bool {
229        let position_error = (self.value - self.target).abs();
230        let velocity_mag = self.velocity.abs();
231
232        position_error < position_threshold && velocity_mag < velocity_threshold
233    }
234
235    /// Reset the spring to a new position with zero velocity
236    pub const fn reset(&mut self, value: f32, target: f32) {
237        self.value = value;
238        self.target = target;
239        self.velocity = 0.0;
240    }
241}