Skip to main content

armas_basic/animation/
staggered.rs

1//! Staggered animations for choreographed entrance effects
2//!
3//! Provides tools for creating aceternity-style staggered animations where
4//! multiple elements animate with sequential delays.
5
6use super::{Animation, AnimationState, EasingFunction, Interpolate};
7
8/// Staggered animation system for multiple items
9///
10/// Creates a choreographed animation where items animate in sequence
11/// with configurable delays between them.
12///
13/// # Example
14///
15/// ```rust
16/// use armas_basic::animation::StaggeredAnimation;
17///
18/// // Fade in 5 items, each delayed by 0.1s, taking 0.3s each
19/// let mut anim = StaggeredAnimation::new(0.0_f32, 1.0, 5, 0.1, 0.3);
20/// anim.update(0.2);
21/// let opacity_0 = anim.opacity(0); // mostly visible
22/// let opacity_4 = anim.opacity(4); // not started yet
23/// ```
24#[derive(Debug, Clone)]
25pub struct StaggeredAnimation<T: Interpolate> {
26    /// Base delay before first item starts (seconds)
27    pub base_delay: f32,
28    /// Delay between each item (seconds)
29    pub stagger_delay: f32,
30    /// Duration of each item's animation (seconds)
31    pub duration: f32,
32    /// Easing function for each item
33    pub easing: EasingFunction,
34    /// Total elapsed time
35    elapsed: f32,
36    /// Number of items
37    item_count: usize,
38    /// Start and end values
39    start: T,
40    end: T,
41}
42
43impl<T: Interpolate> StaggeredAnimation<T> {
44    /// Create a new staggered animation
45    pub const fn new(
46        start: T,
47        end: T,
48        item_count: usize,
49        stagger_delay: f32,
50        duration: f32,
51    ) -> Self {
52        Self {
53            base_delay: 0.0,
54            stagger_delay,
55            duration,
56            easing: EasingFunction::EaseOut,
57            elapsed: 0.0,
58            item_count,
59            start,
60            end,
61        }
62    }
63
64    /// Set the base delay before first item
65    #[must_use]
66    pub const fn base_delay(mut self, delay: f32) -> Self {
67        self.base_delay = delay;
68        self
69    }
70
71    /// Set the easing function
72    #[must_use]
73    pub const fn easing(mut self, easing: EasingFunction) -> Self {
74        self.easing = easing;
75        self
76    }
77
78    /// Update the animation
79    pub fn update(&mut self, dt: f32) {
80        self.elapsed += dt;
81    }
82
83    /// Reset the animation
84    pub const fn reset(&mut self) {
85        self.elapsed = 0.0;
86    }
87
88    /// Get the value for a specific item index
89    pub fn value(&self, index: usize) -> T {
90        let item_start_time = self.base_delay + (index as f32 * self.stagger_delay);
91        let item_end_time = item_start_time + self.duration;
92
93        if self.elapsed < item_start_time {
94            // Not started yet
95            return self.start.clone();
96        }
97
98        if self.elapsed >= item_end_time {
99            // Completed
100            return self.end.clone();
101        }
102
103        // In progress
104        let item_elapsed = self.elapsed - item_start_time;
105        let t = (item_elapsed / self.duration).clamp(0.0, 1.0);
106        let eased_t = self.easing.apply(t);
107
108        self.start.interpolate(&self.end, eased_t)
109    }
110
111    /// Get the progress (0.0 to 1.0) for a specific item
112    pub fn progress(&self, index: usize) -> f32 {
113        let item_start_time = self.base_delay + (index as f32 * self.stagger_delay);
114        let item_end_time = item_start_time + self.duration;
115
116        if self.elapsed < item_start_time {
117            0.0
118        } else if self.elapsed >= item_end_time {
119            1.0
120        } else {
121            ((self.elapsed - item_start_time) / self.duration).clamp(0.0, 1.0)
122        }
123    }
124
125    /// Check if all items have completed
126    pub fn is_complete(&self) -> bool {
127        let last_item_end =
128            self.base_delay + ((self.item_count - 1) as f32 * self.stagger_delay) + self.duration;
129        self.elapsed >= last_item_end
130    }
131
132    /// Get opacity for item (useful for fade-in effects)
133    pub fn opacity(&self, index: usize) -> f32 {
134        self.progress(index)
135    }
136
137    /// Get scale for item (useful for scale-in effects)
138    pub fn scale(&self, index: usize) -> f32 {
139        let t = self.progress(index);
140        0.8 + 0.2 * t // Scale from 0.8 to 1.0
141    }
142
143    /// Get Y offset for item (useful for slide-in effects)
144    pub fn y_offset(&self, index: usize, distance: f32) -> f32 {
145        let t = self.progress(index);
146        distance * (1.0 - t)
147    }
148}
149
150/// Animation sequence that runs animations one after another
151///
152/// # Example
153///
154/// ```rust
155/// use armas_basic::animation::{Animation, AnimationSequence, EasingFunction};
156///
157/// let seq = AnimationSequence::new()
158///     .then(Animation::new(0.0_f32, 1.0, 0.3), 0.0)
159///     .then(Animation::new(1.0_f32, 0.5, 0.2), 0.1);
160/// ```
161#[derive(Debug)]
162pub struct AnimationSequence<T: Interpolate> {
163    animations: Vec<SequenceStep<T>>,
164    current_step: usize,
165    elapsed: f32,
166}
167
168#[derive(Debug)]
169struct SequenceStep<T: Interpolate> {
170    delay: f32,
171    animation: Animation<T>,
172}
173
174impl<T: Interpolate> AnimationSequence<T> {
175    /// Create a new empty sequence
176    #[must_use]
177    pub const fn new() -> Self {
178        Self {
179            animations: Vec::new(),
180            current_step: 0,
181            elapsed: 0.0,
182        }
183    }
184
185    /// Add an animation step with optional delay
186    #[must_use]
187    pub fn then(mut self, animation: Animation<T>, delay: f32) -> Self {
188        self.animations.push(SequenceStep { delay, animation });
189        self
190    }
191
192    /// Update the sequence
193    pub fn update(&mut self, dt: f32) {
194        if self.current_step >= self.animations.len() {
195            return;
196        }
197
198        self.elapsed += dt;
199        let step = &mut self.animations[self.current_step];
200
201        // Wait for delay
202        if self.elapsed < step.delay {
203            return;
204        }
205
206        // Update animation
207        let animation_dt = self.elapsed - step.delay;
208        step.animation.elapsed = animation_dt;
209
210        if animation_dt >= step.animation.duration {
211            // Move to next step
212            self.current_step += 1;
213            self.elapsed = 0.0;
214        }
215    }
216
217    /// Get the current value
218    #[must_use]
219    pub fn value(&self) -> T {
220        if self.current_step >= self.animations.len() {
221            // Return the last animation's end value
222            if let Some(last) = self.animations.last() {
223                return last.animation.end.clone();
224            }
225        }
226
227        self.animations.get(self.current_step).map_or_else(
228            || self.animations[0].animation.start.clone(),
229            |step| step.animation.value(),
230        )
231    }
232
233    /// Check if the entire sequence is complete
234    #[must_use]
235    pub const fn is_complete(&self) -> bool {
236        self.current_step >= self.animations.len()
237    }
238
239    /// Reset the sequence
240    pub fn reset(&mut self) {
241        self.current_step = 0;
242        self.elapsed = 0.0;
243        for step in &mut self.animations {
244            step.animation.reset();
245        }
246    }
247}
248
249impl<T: Interpolate> Default for AnimationSequence<T> {
250    fn default() -> Self {
251        Self::new()
252    }
253}
254
255/// Loop mode for repeating animations
256#[derive(Debug, Clone, Copy, PartialEq, Eq)]
257pub enum LoopMode {
258    /// Play once and stop
259    Once,
260    /// Loop infinitely
261    Loop,
262    /// Ping-pong (forward then backward)
263    PingPong,
264}
265
266/// Animation with loop support
267///
268/// # Example
269///
270/// ```rust
271/// use armas_basic::animation::{LoopingAnimation, LoopMode};
272///
273/// let mut anim = LoopingAnimation::new(0.0_f32, 1.0, 1.0, LoopMode::PingPong);
274/// anim.update(0.5);
275/// let value = anim.value();
276/// ```
277#[derive(Debug, Clone)]
278pub struct LoopingAnimation<T: Interpolate> {
279    animation: Animation<T>,
280    mode: LoopMode,
281    forward: bool,
282}
283
284impl<T: Interpolate> LoopingAnimation<T> {
285    /// Create a new looping animation
286    pub const fn new(start: T, end: T, duration: f32, mode: LoopMode) -> Self {
287        Self {
288            animation: Animation::new(start, end, duration),
289            mode,
290            forward: true,
291        }
292    }
293
294    /// Set easing function
295    #[must_use]
296    pub const fn easing(mut self, easing: EasingFunction) -> Self {
297        self.animation.easing = easing;
298        self
299    }
300
301    /// Update the animation
302    pub fn update(&mut self, dt: f32) {
303        self.animation.update(dt);
304
305        if self.animation.is_complete() {
306            match self.mode {
307                LoopMode::Once => {
308                    // Stay at end
309                }
310                LoopMode::Loop => {
311                    // Restart from beginning
312                    self.animation.reset();
313                    self.animation.start();
314                }
315                LoopMode::PingPong => {
316                    // Reverse direction
317                    self.forward = !self.forward;
318                    std::mem::swap(&mut self.animation.start, &mut self.animation.end);
319                    self.animation.reset();
320                    self.animation.start();
321                }
322            }
323        } else if self.animation.state == AnimationState::NotStarted {
324            self.animation.start();
325        }
326    }
327
328    /// Get current value
329    pub fn value(&self) -> T {
330        self.animation.value()
331    }
332
333    /// Get progress
334    pub fn progress(&self) -> f32 {
335        self.animation.progress()
336    }
337
338    /// Reset the animation
339    pub const fn reset(&mut self) {
340        self.animation.reset();
341        self.forward = true;
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn test_staggered_animation() {
351        let mut anim = StaggeredAnimation::new(0.0_f32, 1.0, 3, 0.1, 0.3);
352
353        // At start, all items should be at 0
354        assert_eq!(anim.value(0), 0.0);
355        assert_eq!(anim.value(1), 0.0);
356        assert_eq!(anim.value(2), 0.0);
357
358        // After 0.15s, first item should be halfway, second just started
359        anim.elapsed = 0.15;
360        assert!(anim.progress(0) > 0.0);
361        assert!(anim.progress(1) < 0.5);
362        assert_eq!(anim.progress(2), 0.0);
363    }
364
365    #[test]
366    fn test_looping_animation() {
367        let mut anim = LoopingAnimation::new(0.0_f32, 1.0, 1.0, LoopMode::Loop);
368
369        // First update starts the animation
370        anim.update(0.0);
371        anim.update(0.5);
372        assert!(anim.progress() > 0.0 && anim.progress() < 1.0);
373
374        anim.update(0.6); // Should loop
375        assert!(anim.progress() < 0.5);
376    }
377}