Skip to main content

eazy_tweener/
stagger.rs

1//! Stagger system for cascading animations.
2//!
3//! Staggers offset the start time of multiple animations to create
4//! cascading effects like domino falls or wave animations.
5
6use eazy_core::Curve;
7use eazy_core::Easing;
8
9/// Direction from which staggering originates.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum StaggerFrom {
12  /// Stagger from first to last (index 0 starts first).
13  #[default]
14  Start,
15  /// Stagger from last to first (last index starts first).
16  End,
17  /// Stagger from center outward (middle starts first).
18  Center,
19  /// Stagger from edges inward (first and last start first).
20  Edges,
21  /// Random stagger order.
22  Random,
23}
24
25impl StaggerFrom {
26  /// Calculate the stagger index for a given position.
27  ///
28  /// Returns a value from 0.0 to 1.0 representing the relative
29  /// position in the stagger sequence.
30  pub fn index_factor(&self, index: usize, total: usize) -> f32 {
31    if total <= 1 {
32      return 0.0;
33    }
34
35    let i = index as f32;
36    let n = (total - 1) as f32;
37
38    match self {
39      Self::Start => i / n,
40      Self::End => (n - i) / n,
41      Self::Center => {
42        let center = n / 2.0;
43        let distance = (i - center).abs();
44
45        distance / center
46      }
47      Self::Edges => {
48        let center = n / 2.0;
49        let distance = (i - center).abs();
50
51        1.0 - (distance / center)
52      }
53      Self::Random => {
54        // For deterministic "random", use a simple hash
55        let hash = ((index as u32).wrapping_mul(2654435761)) as f32;
56
57        hash / u32::MAX as f32
58      }
59    }
60  }
61}
62
63/// Configuration for staggering multiple animations.
64///
65/// # Examples
66///
67/// ```rust
68/// use eazy_tweener::{Stagger, StaggerFrom};
69///
70/// // Each animation starts 0.1s after the previous
71/// let stagger = Stagger::each(0.1);
72///
73/// // Stagger from center outward
74/// let stagger = Stagger::each(0.1).from(StaggerFrom::Center);
75///
76/// // Apply easing to the stagger distribution
77/// let stagger = Stagger::each(0.1).ease(eazy_core::Easing::OutQuadratic);
78/// ```
79#[derive(Debug, Clone, Default)]
80pub struct Stagger {
81  /// Delay between each successive animation.
82  each: f32,
83  /// Direction of the stagger.
84  from: StaggerFrom,
85  /// Easing applied to the stagger distribution.
86  ease: Option<Easing>,
87  /// Total duration to distribute staggers across.
88  /// If set, overrides `each` to fit all animations in this duration.
89  total: Option<f32>,
90}
91
92impl Stagger {
93  /// Create a stagger with the given delay between each animation.
94  pub fn each(delay: f32) -> Self {
95    Self {
96      each: delay.max(0.0),
97      from: StaggerFrom::default(),
98      ease: None,
99      total: None,
100    }
101  }
102
103  /// Create a stagger that distributes animations across a total duration.
104  ///
105  /// The delay between each animation is calculated as `total / (count - 1)`.
106  pub fn total(duration: f32) -> Self {
107    Self {
108      each: 0.0,
109      from: StaggerFrom::default(),
110      ease: None,
111      total: Some(duration.max(0.0)),
112    }
113  }
114
115  /// Set the stagger direction.
116  pub fn from(mut self, from: StaggerFrom) -> Self {
117    self.from = from;
118    self
119  }
120
121  /// Apply an easing function to the stagger distribution.
122  ///
123  /// This affects how the delays are distributed, not the animations
124  /// themselves.
125  pub fn ease(mut self, easing: Easing) -> Self {
126    self.ease = Some(easing);
127    self
128  }
129
130  /// Get the delay for a specific index in a collection.
131  ///
132  /// # Parameters
133  ///
134  /// - `index`: The position of this animation (0-based)
135  /// - `total`: Total number of animations
136  ///
137  /// # Returns
138  ///
139  /// The delay in seconds before this animation should start.
140  pub fn delay_for(&self, index: usize, total: usize) -> f32 {
141    if total == 0 {
142      return 0.0;
143    }
144
145    if total == 1 {
146      return 0.0;
147    }
148
149    // Get the base factor (0.0 to 1.0)
150    let factor = self.from.index_factor(index, total);
151
152    // Apply easing if present
153    let eased_factor = match &self.ease {
154      Some(easing) => easing.y(factor),
155      None => factor,
156    };
157
158    // Calculate the delay
159    let each_delay = match self.total {
160      Some(t) => t / (total - 1) as f32,
161      None => self.each,
162    };
163
164    eased_factor * each_delay * (total - 1) as f32
165  }
166
167  /// Get the total stagger duration for a collection.
168  ///
169  /// This is the delay of the last animation to start.
170  pub fn total_stagger_duration(&self, count: usize) -> f32 {
171    if count <= 1 {
172      return 0.0;
173    }
174
175    match self.total {
176      Some(t) => t,
177      None => self.each * (count - 1) as f32,
178    }
179  }
180
181  /// Get the each delay value.
182  pub fn each_delay(&self) -> f32 {
183    self.each
184  }
185
186  /// Get the stagger direction.
187  pub fn direction(&self) -> StaggerFrom {
188    self.from
189  }
190}
191
192/// Calculate stagger delays for a collection of items.
193///
194/// Returns a vector of delays corresponding to each index.
195pub fn calculate_stagger_delays(stagger: &Stagger, count: usize) -> Vec<f32> {
196  (0..count).map(|i| stagger.delay_for(i, count)).collect()
197}
198
199#[cfg(test)]
200mod tests {
201  use super::*;
202
203  #[test]
204  fn test_stagger_each() {
205    let stagger = Stagger::each(0.1);
206
207    assert_eq!(stagger.delay_for(0, 5), 0.0);
208    assert!((stagger.delay_for(1, 5) - 0.1).abs() < 0.001);
209    assert!((stagger.delay_for(2, 5) - 0.2).abs() < 0.001);
210    assert!((stagger.delay_for(4, 5) - 0.4).abs() < 0.001);
211  }
212
213  #[test]
214  fn test_stagger_from_end() {
215    let stagger = Stagger::each(0.1).from(StaggerFrom::End);
216
217    assert!((stagger.delay_for(0, 5) - 0.4).abs() < 0.001);
218    assert!((stagger.delay_for(4, 5) - 0.0).abs() < 0.001);
219  }
220
221  #[test]
222  fn test_stagger_from_center() {
223    let stagger = Stagger::each(0.1).from(StaggerFrom::Center);
224
225    // With 5 items (indices 0,1,2,3,4), center is 2
226    // Delays should be: 2->0, 1&3->0.1, 0&4->0.2
227    assert!((stagger.delay_for(2, 5) - 0.0).abs() < 0.001);
228    assert!((stagger.delay_for(1, 5) - 0.2).abs() < 0.001);
229    assert!((stagger.delay_for(3, 5) - 0.2).abs() < 0.001);
230    assert!((stagger.delay_for(0, 5) - 0.4).abs() < 0.001);
231    assert!((stagger.delay_for(4, 5) - 0.4).abs() < 0.001);
232  }
233
234  #[test]
235  fn test_stagger_total() {
236    let stagger = Stagger::total(1.0);
237
238    // 5 items across 1.0 seconds = 0.25s between each
239    assert_eq!(stagger.delay_for(0, 5), 0.0);
240    assert!((stagger.delay_for(1, 5) - 0.25).abs() < 0.001);
241    assert!((stagger.delay_for(4, 5) - 1.0).abs() < 0.001);
242  }
243
244  #[test]
245  fn test_stagger_single_item() {
246    let stagger = Stagger::each(0.1);
247
248    assert_eq!(stagger.delay_for(0, 1), 0.0);
249  }
250
251  #[test]
252  fn test_stagger_empty() {
253    let stagger = Stagger::each(0.1);
254
255    assert_eq!(stagger.delay_for(0, 0), 0.0);
256  }
257
258  #[test]
259  fn test_total_stagger_duration() {
260    let stagger = Stagger::each(0.1);
261
262    assert_eq!(stagger.total_stagger_duration(5), 0.4);
263    assert_eq!(stagger.total_stagger_duration(1), 0.0);
264  }
265
266  #[test]
267  fn test_calculate_stagger_delays() {
268    let stagger = Stagger::each(0.1);
269    let delays = calculate_stagger_delays(&stagger, 3);
270
271    assert_eq!(delays.len(), 3);
272    assert_eq!(delays[0], 0.0);
273    assert!((delays[1] - 0.1).abs() < 0.001);
274    assert!((delays[2] - 0.2).abs() < 0.001);
275  }
276}