Skip to main content

guise/anim/
stagger.rs

1//! Choreography across a list: the same motion, offset per element.
2//!
3//! One element is one clip, so staggering is not a timeline feature here —
4//! it is a function from an index to a delay, which you fold into each
5//! element's own [`Motion`](super::Motion). That keeps the N elements
6//! independent (a list can grow or reorder mid-flight without restarting
7//! anything) and makes the whole thing a pure calculation you can unit-test.
8//!
9//! ```ignore
10//! let rise = Stagger::new(40.0).from(StaggerFrom::Center);
11//! for (i, row) in rows.iter().enumerate() {
12//!     Animated::new(("row", i))
13//!         .motion(Motion::enter(TransitionKind::SlideUp).delay(rise.at(i, rows.len())))
14//!         .child(row)
15//! }
16//! ```
17
18use super::Easing;
19
20/// Which element goes first.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum StaggerFrom {
23  #[default]
24  First,
25  Last,
26  Center,
27  /// A specific index leads and the rest ripple out from it.
28  Index(usize),
29}
30
31/// Restrict a grid stagger to one axis.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum StaggerAxis {
34  X,
35  Y,
36}
37
38/// Index-to-delay (or index-to-value) mapping for a list or a grid.
39#[derive(Debug, Clone, Copy, PartialEq)]
40pub struct Stagger {
41  /// Milliseconds between neighbours.
42  pub step: f32,
43  /// Milliseconds added to everyone.
44  pub start: f32,
45  pub from: StaggerFrom,
46  /// Columns and rows, when the elements are laid out as a grid.
47  pub grid: Option<(usize, usize)>,
48  /// With a grid, measure distance along one axis only.
49  pub axis: Option<StaggerAxis>,
50  /// Reshape the spacing — `Easing::In(Curve::Quad)` bunches the early
51  /// elements together and spreads the late ones out.
52  pub ease: Option<Easing>,
53  pub reversed: bool,
54}
55
56impl Stagger {
57  /// `step` milliseconds between neighbours.
58  pub fn new(step: f32) -> Self {
59    Stagger {
60      step,
61      start: 0.0,
62      from: StaggerFrom::First,
63      grid: None,
64      axis: None,
65      ease: None,
66      reversed: false,
67    }
68  }
69
70  pub fn start(mut self, ms: f32) -> Self {
71    self.start = ms;
72    self
73  }
74
75  pub fn from(mut self, from: StaggerFrom) -> Self {
76    self.from = from;
77    self
78  }
79
80  /// Treat the indices as a `columns × rows` grid in row-major order.
81  pub fn grid(mut self, columns: usize, rows: usize) -> Self {
82    self.grid = Some((columns.max(1), rows.max(1)));
83    self
84  }
85
86  pub fn axis(mut self, axis: StaggerAxis) -> Self {
87    self.axis = Some(axis);
88    self
89  }
90
91  pub fn ease(mut self, easing: Easing) -> Self {
92    self.ease = Some(easing);
93    self
94  }
95
96  pub fn reversed(mut self, reversed: bool) -> Self {
97    self.reversed = reversed;
98    self
99  }
100
101  /// The delay for element `index` of `total`, in milliseconds.
102  pub fn at(&self, index: usize, total: usize) -> f32 {
103    self.start + self.weight(index, total) * self.furthest(total) * self.step
104  }
105
106  /// Spread a value instead of a delay: element 0 gets `from`, the
107  /// furthest gets `to`, everyone else lands in between. anime.js's
108  /// `stagger([a, b])`.
109  pub fn value(&self, index: usize, total: usize, from: f32, to: f32) -> f32 {
110    from + (to - from) * self.weight(index, total)
111  }
112
113  /// How long until the last element has started.
114  pub fn span(&self, total: usize) -> f32 {
115    self.start + self.furthest(total) * self.step
116  }
117
118  /// 0..=1: how far this index is from the leading one.
119  fn weight(&self, index: usize, total: usize) -> f32 {
120    let furthest = self.furthest(total);
121    if furthest <= 0.0 {
122      return 0.0;
123    }
124    let mut t = (self.distance(index, total) / furthest).clamp(0.0, 1.0);
125    if self.reversed {
126      t = 1.0 - t;
127    }
128    match self.ease {
129      Some(easing) => easing.apply(t),
130      None => t,
131    }
132  }
133
134  /// Distance from the leading element, in element-widths.
135  fn distance(&self, index: usize, total: usize) -> f32 {
136    match self.grid {
137      Some((columns, _)) => {
138        let (x, y) = ((index % columns) as f32, (index / columns) as f32);
139        let (ox, oy) = self.grid_origin(total);
140        match self.axis {
141          Some(StaggerAxis::X) => (x - ox).abs(),
142          Some(StaggerAxis::Y) => (y - oy).abs(),
143          None => ((x - ox).powi(2) + (y - oy).powi(2)).sqrt(),
144        }
145      }
146      None => {
147        let last = total.saturating_sub(1) as f32;
148        let origin = match self.from {
149          StaggerFrom::First => 0.0,
150          StaggerFrom::Last => last,
151          StaggerFrom::Center => last / 2.0,
152          StaggerFrom::Index(i) => i as f32,
153        };
154        (index as f32 - origin).abs()
155      }
156    }
157  }
158
159  fn grid_origin(&self, total: usize) -> (f32, f32) {
160    let (columns, rows) = self.grid.unwrap_or((1, 1));
161    let rows = rows.max(total.div_ceil(columns));
162    let (last_x, last_y) = ((columns - 1) as f32, (rows.saturating_sub(1)) as f32);
163    match self.from {
164      StaggerFrom::First => (0.0, 0.0),
165      StaggerFrom::Last => (last_x, last_y),
166      StaggerFrom::Center => (last_x / 2.0, last_y / 2.0),
167      StaggerFrom::Index(i) => ((i % columns) as f32, (i / columns) as f32),
168    }
169  }
170
171  /// The largest distance any index reaches — what normalizes the weight
172  /// so `ease` and `value` have a fixed range to work in.
173  fn furthest(&self, total: usize) -> f32 {
174    (0..total)
175      .map(|i| self.distance(i, total))
176      .fold(0.0_f32, f32::max)
177  }
178}
179
180#[cfg(test)]
181mod tests {
182  use super::*;
183  use crate::anim::ease::Curve;
184
185  #[test]
186  fn a_plain_stagger_steps_one_by_one() {
187    let stagger = Stagger::new(50.0);
188    assert_eq!(stagger.at(0, 4), 0.0);
189    assert_eq!(stagger.at(1, 4), 50.0);
190    assert_eq!(stagger.at(3, 4), 150.0);
191    assert_eq!(stagger.span(4), 150.0);
192  }
193
194  #[test]
195  fn start_shifts_everyone() {
196    let stagger = Stagger::new(50.0).start(100.0);
197    assert_eq!(stagger.at(0, 4), 100.0);
198    assert_eq!(stagger.at(2, 4), 200.0);
199  }
200
201  #[test]
202  fn from_last_reverses_the_order() {
203    let stagger = Stagger::new(50.0).from(StaggerFrom::Last);
204    assert_eq!(stagger.at(3, 4), 0.0);
205    assert_eq!(stagger.at(0, 4), 150.0);
206  }
207
208  #[test]
209  fn from_center_ripples_outward() {
210    let stagger = Stagger::new(50.0).from(StaggerFrom::Center);
211    // Five elements: the middle leads, the ends arrive together.
212    assert_eq!(stagger.at(2, 5), 0.0);
213    assert_eq!(stagger.at(0, 5), stagger.at(4, 5));
214    assert!(stagger.at(1, 5) < stagger.at(0, 5));
215  }
216
217  #[test]
218  fn a_named_index_leads() {
219    let stagger = Stagger::new(10.0).from(StaggerFrom::Index(2));
220    assert_eq!(stagger.at(2, 5), 0.0);
221    assert_eq!(stagger.at(4, 5), 20.0);
222  }
223
224  #[test]
225  fn a_grid_measures_in_two_dimensions() {
226    let stagger = Stagger::new(100.0).grid(3, 2);
227    // Row-major 3x2. Corner-to-corner is sqrt(2^2 + 1^2).
228    assert_eq!(stagger.at(0, 6), 0.0);
229    let far = stagger.at(5, 6);
230    assert!((far - 100.0 * 5.0_f32.sqrt()).abs() < 1e-3, "{far}");
231    // Same column, next row: distance 1.
232    assert!((stagger.at(3, 6) - 100.0).abs() < 1e-3);
233  }
234
235  #[test]
236  fn an_axis_flattens_the_grid_to_one_direction() {
237    let stagger = Stagger::new(100.0).grid(3, 2).axis(StaggerAxis::Y);
238    assert_eq!(stagger.at(0, 6), stagger.at(2, 6), "same row, same delay");
239    assert!(stagger.at(3, 6) > stagger.at(0, 6));
240  }
241
242  #[test]
243  fn reversed_flips_the_weights() {
244    let plain = Stagger::new(50.0);
245    let flipped = Stagger::new(50.0).reversed(true);
246    assert_eq!(flipped.at(0, 4), plain.at(3, 4));
247    assert_eq!(flipped.at(3, 4), plain.at(0, 4));
248  }
249
250  #[test]
251  fn easing_reshapes_the_spacing_without_moving_the_ends() {
252    let eased = Stagger::new(50.0).ease(Easing::In(Curve::Quad));
253    assert_eq!(eased.at(0, 5), 0.0);
254    assert_eq!(eased.at(4, 5), 200.0);
255    // Quadratic in: the early elements bunch up.
256    assert!(eased.at(1, 5) < 50.0);
257  }
258
259  #[test]
260  fn values_spread_across_a_range() {
261    let stagger = Stagger::new(0.0);
262    assert_eq!(stagger.value(0, 5, -100.0, 100.0), -100.0);
263    assert_eq!(stagger.value(4, 5, -100.0, 100.0), 100.0);
264    assert_eq!(stagger.value(2, 5, -100.0, 100.0), 0.0);
265  }
266
267  #[test]
268  fn a_single_element_never_waits() {
269    let stagger = Stagger::new(50.0).from(StaggerFrom::Center);
270    assert_eq!(stagger.at(0, 1), 0.0);
271    assert_eq!(stagger.span(1), 0.0);
272    assert_eq!(stagger.at(0, 0), 0.0);
273  }
274}