Skip to main content

animato_tween/
stagger_pattern.rs

1//! Advanced stagger delay patterns.
2
3#[cfg(any(feature = "std", feature = "alloc"))]
4use alloc::boxed::Box;
5use core::fmt;
6
7/// Origin used by grid-based stagger patterns.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub enum GridOrigin {
11    /// Top-left cell starts first.
12    TopLeft,
13    /// Top-right cell starts first.
14    TopRight,
15    /// Bottom-left cell starts first.
16    BottomLeft,
17    /// Bottom-right cell starts first.
18    BottomRight,
19    /// Center cell or center cells start first.
20    Center,
21    /// Top edge starts first.
22    Top,
23    /// Bottom edge starts first.
24    Bottom,
25    /// Left edge starts first.
26    Left,
27    /// Right edge starts first.
28    Right,
29}
30
31/// Deterministic delay pattern for staggered animation starts.
32pub enum StaggerPattern {
33    /// 2D grid delay based on cell distance from an origin.
34    Grid {
35        /// Number of columns.
36        cols: usize,
37        /// Number of rows.
38        rows: usize,
39        /// Starting origin.
40        origin: GridOrigin,
41        /// Seconds per distance step.
42        step: f32,
43    },
44    /// Deterministic random delay inside `[min_delay, max_delay]`.
45    Random {
46        /// Deterministic seed.
47        seed: u32,
48        /// Minimum seconds delay.
49        min_delay: f32,
50        /// Maximum seconds delay.
51        max_delay: f32,
52    },
53    /// Start from the center index and move outward.
54    CenterOut {
55        /// Number of items.
56        count: usize,
57        /// Seconds per distance step.
58        step: f32,
59    },
60    /// Start from both edges and move inward.
61    EdgesIn {
62        /// Number of items.
63        count: usize,
64        /// Seconds per distance step.
65        step: f32,
66    },
67    /// User-defined delay function.
68    #[cfg(any(feature = "std", feature = "alloc"))]
69    Custom(
70        /// Function receiving `(index, total)` and returning seconds delay.
71        Box<dyn Fn(usize, usize) -> f32 + Send + Sync + 'static>,
72    ),
73}
74
75impl fmt::Debug for StaggerPattern {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        match self {
78            Self::Grid {
79                cols,
80                rows,
81                origin,
82                step,
83            } => f
84                .debug_struct("Grid")
85                .field("cols", cols)
86                .field("rows", rows)
87                .field("origin", origin)
88                .field("step", step)
89                .finish(),
90            Self::Random {
91                seed,
92                min_delay,
93                max_delay,
94            } => f
95                .debug_struct("Random")
96                .field("seed", seed)
97                .field("min_delay", min_delay)
98                .field("max_delay", max_delay)
99                .finish(),
100            Self::CenterOut { count, step } => f
101                .debug_struct("CenterOut")
102                .field("count", count)
103                .field("step", step)
104                .finish(),
105            Self::EdgesIn { count, step } => f
106                .debug_struct("EdgesIn")
107                .field("count", count)
108                .field("step", step)
109                .finish(),
110            #[cfg(any(feature = "std", feature = "alloc"))]
111            Self::Custom(_) => f.debug_tuple("Custom").field(&"<function>").finish(),
112        }
113    }
114}
115
116impl StaggerPattern {
117    /// Return the delay for `index` out of `total` items.
118    pub fn delay(&self, index: usize, _total: usize) -> f32 {
119        match self {
120            Self::Grid {
121                cols,
122                rows,
123                origin,
124                step,
125            } => grid_delay(*cols, *rows, *origin, *step, index),
126            Self::Random {
127                seed,
128                min_delay,
129                max_delay,
130            } => random_delay(*seed, *min_delay, *max_delay, index),
131            Self::CenterOut { count, step } => {
132                center_distance(index.min(count.saturating_sub(1)), *count) as f32
133                    * finite_or(*step, 0.0).max(0.0)
134            }
135            Self::EdgesIn { count, step } => {
136                let count = *count;
137                if count == 0 {
138                    return 0.0;
139                }
140                let index = index.min(count - 1);
141                index.min(count - 1 - index) as f32 * finite_or(*step, 0.0).max(0.0)
142            }
143            #[cfg(any(feature = "std", feature = "alloc"))]
144            Self::Custom(callback) => finite_or(callback(index, _total), 0.0).max(0.0),
145        }
146    }
147}
148
149fn grid_delay(cols: usize, rows: usize, origin: GridOrigin, step: f32, index: usize) -> f32 {
150    let cols = cols.max(1);
151    let rows = rows.max(1);
152    let index = index.min(cols.saturating_mul(rows).saturating_sub(1));
153    let x = index % cols;
154    let y = index / cols;
155    let distance = match origin {
156        GridOrigin::TopLeft => x + y,
157        GridOrigin::TopRight => (cols - 1 - x) + y,
158        GridOrigin::BottomLeft => x + (rows - 1 - y),
159        GridOrigin::BottomRight => (cols - 1 - x) + (rows - 1 - y),
160        GridOrigin::Center => {
161            let left = x.abs_diff((cols - 1) / 2).min(x.abs_diff(cols / 2));
162            let top = y.abs_diff((rows - 1) / 2).min(y.abs_diff(rows / 2));
163            left + top
164        }
165        GridOrigin::Top => y,
166        GridOrigin::Bottom => rows - 1 - y,
167        GridOrigin::Left => x,
168        GridOrigin::Right => cols - 1 - x,
169    };
170    distance as f32 * finite_or(step, 0.0).max(0.0)
171}
172
173fn center_distance(index: usize, count: usize) -> usize {
174    if count <= 1 {
175        return 0;
176    }
177    index
178        .abs_diff((count - 1) / 2)
179        .min(index.abs_diff(count / 2))
180}
181
182fn random_delay(seed: u32, min_delay: f32, max_delay: f32, index: usize) -> f32 {
183    let min = finite_or(min_delay, 0.0).max(0.0);
184    let max = finite_or(max_delay, min).max(min);
185    let mut x = seed ^ (index as u32).wrapping_mul(0x9E37_79B9);
186    x ^= x >> 16;
187    x = x.wrapping_mul(0x7FEB_352D);
188    x ^= x >> 15;
189    x = x.wrapping_mul(0x846C_A68B);
190    x ^= x >> 16;
191    min + (max - min) * (x as f32 / u32::MAX as f32)
192}
193
194fn finite_or(value: f32, fallback: f32) -> f32 {
195    if value.is_finite() { value } else { fallback }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn grid_center_is_symmetric() {
204        let pattern = StaggerPattern::Grid {
205            cols: 3,
206            rows: 3,
207            origin: GridOrigin::Center,
208            step: 0.1,
209        };
210        assert_eq!(pattern.delay(4, 9), 0.0);
211        assert_eq!(pattern.delay(0, 9), pattern.delay(8, 9));
212        assert_eq!(pattern.delay(2, 9), pattern.delay(6, 9));
213    }
214
215    #[test]
216    fn random_is_deterministic_and_bounded() {
217        let pattern = StaggerPattern::Random {
218            seed: 9,
219            min_delay: 0.2,
220            max_delay: 0.5,
221        };
222        let delay = pattern.delay(3, 10);
223        assert_eq!(delay, pattern.delay(3, 10));
224        assert!((0.2..=0.5).contains(&delay));
225    }
226
227    #[test]
228    fn center_out_and_edges_in_order() {
229        let center = StaggerPattern::CenterOut {
230            count: 5,
231            step: 0.1,
232        };
233        assert_eq!(center.delay(2, 5), 0.0);
234        assert_eq!(center.delay(0, 5), 0.2);
235
236        let edges = StaggerPattern::EdgesIn {
237            count: 5,
238            step: 0.1,
239        };
240        assert_eq!(edges.delay(0, 5), 0.0);
241        assert_eq!(edges.delay(4, 5), 0.0);
242        assert_eq!(edges.delay(2, 5), 0.2);
243    }
244}