use super::Easing;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StaggerFrom {
#[default]
First,
Last,
Center,
Index(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StaggerAxis {
X,
Y,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Stagger {
pub step: f32,
pub start: f32,
pub from: StaggerFrom,
pub grid: Option<(usize, usize)>,
pub axis: Option<StaggerAxis>,
pub ease: Option<Easing>,
pub reversed: bool,
}
impl Stagger {
pub fn new(step: f32) -> Self {
Stagger {
step,
start: 0.0,
from: StaggerFrom::First,
grid: None,
axis: None,
ease: None,
reversed: false,
}
}
pub fn start(mut self, ms: f32) -> Self {
self.start = ms;
self
}
pub fn from(mut self, from: StaggerFrom) -> Self {
self.from = from;
self
}
pub fn grid(mut self, columns: usize, rows: usize) -> Self {
self.grid = Some((columns.max(1), rows.max(1)));
self
}
pub fn axis(mut self, axis: StaggerAxis) -> Self {
self.axis = Some(axis);
self
}
pub fn ease(mut self, easing: Easing) -> Self {
self.ease = Some(easing);
self
}
pub fn reversed(mut self, reversed: bool) -> Self {
self.reversed = reversed;
self
}
pub fn at(&self, index: usize, total: usize) -> f32 {
self.start + self.weight(index, total) * self.furthest(total) * self.step
}
pub fn value(&self, index: usize, total: usize, from: f32, to: f32) -> f32 {
from + (to - from) * self.weight(index, total)
}
pub fn span(&self, total: usize) -> f32 {
self.start + self.furthest(total) * self.step
}
fn weight(&self, index: usize, total: usize) -> f32 {
let furthest = self.furthest(total);
if furthest <= 0.0 {
return 0.0;
}
let mut t = (self.distance(index, total) / furthest).clamp(0.0, 1.0);
if self.reversed {
t = 1.0 - t;
}
match self.ease {
Some(easing) => easing.apply(t),
None => t,
}
}
fn distance(&self, index: usize, total: usize) -> f32 {
match self.grid {
Some((columns, _)) => {
let (x, y) = ((index % columns) as f32, (index / columns) as f32);
let (ox, oy) = self.grid_origin(total);
match self.axis {
Some(StaggerAxis::X) => (x - ox).abs(),
Some(StaggerAxis::Y) => (y - oy).abs(),
None => ((x - ox).powi(2) + (y - oy).powi(2)).sqrt(),
}
}
None => {
let last = total.saturating_sub(1) as f32;
let origin = match self.from {
StaggerFrom::First => 0.0,
StaggerFrom::Last => last,
StaggerFrom::Center => last / 2.0,
StaggerFrom::Index(i) => i as f32,
};
(index as f32 - origin).abs()
}
}
}
fn grid_origin(&self, total: usize) -> (f32, f32) {
let (columns, rows) = self.grid.unwrap_or((1, 1));
let rows = rows.max(total.div_ceil(columns));
let (last_x, last_y) = ((columns - 1) as f32, (rows.saturating_sub(1)) as f32);
match self.from {
StaggerFrom::First => (0.0, 0.0),
StaggerFrom::Last => (last_x, last_y),
StaggerFrom::Center => (last_x / 2.0, last_y / 2.0),
StaggerFrom::Index(i) => ((i % columns) as f32, (i / columns) as f32),
}
}
fn furthest(&self, total: usize) -> f32 {
(0..total)
.map(|i| self.distance(i, total))
.fold(0.0_f32, f32::max)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::anim::ease::Curve;
#[test]
fn a_plain_stagger_steps_one_by_one() {
let stagger = Stagger::new(50.0);
assert_eq!(stagger.at(0, 4), 0.0);
assert_eq!(stagger.at(1, 4), 50.0);
assert_eq!(stagger.at(3, 4), 150.0);
assert_eq!(stagger.span(4), 150.0);
}
#[test]
fn start_shifts_everyone() {
let stagger = Stagger::new(50.0).start(100.0);
assert_eq!(stagger.at(0, 4), 100.0);
assert_eq!(stagger.at(2, 4), 200.0);
}
#[test]
fn from_last_reverses_the_order() {
let stagger = Stagger::new(50.0).from(StaggerFrom::Last);
assert_eq!(stagger.at(3, 4), 0.0);
assert_eq!(stagger.at(0, 4), 150.0);
}
#[test]
fn from_center_ripples_outward() {
let stagger = Stagger::new(50.0).from(StaggerFrom::Center);
assert_eq!(stagger.at(2, 5), 0.0);
assert_eq!(stagger.at(0, 5), stagger.at(4, 5));
assert!(stagger.at(1, 5) < stagger.at(0, 5));
}
#[test]
fn a_named_index_leads() {
let stagger = Stagger::new(10.0).from(StaggerFrom::Index(2));
assert_eq!(stagger.at(2, 5), 0.0);
assert_eq!(stagger.at(4, 5), 20.0);
}
#[test]
fn a_grid_measures_in_two_dimensions() {
let stagger = Stagger::new(100.0).grid(3, 2);
assert_eq!(stagger.at(0, 6), 0.0);
let far = stagger.at(5, 6);
assert!((far - 100.0 * 5.0_f32.sqrt()).abs() < 1e-3, "{far}");
assert!((stagger.at(3, 6) - 100.0).abs() < 1e-3);
}
#[test]
fn an_axis_flattens_the_grid_to_one_direction() {
let stagger = Stagger::new(100.0).grid(3, 2).axis(StaggerAxis::Y);
assert_eq!(stagger.at(0, 6), stagger.at(2, 6), "same row, same delay");
assert!(stagger.at(3, 6) > stagger.at(0, 6));
}
#[test]
fn reversed_flips_the_weights() {
let plain = Stagger::new(50.0);
let flipped = Stagger::new(50.0).reversed(true);
assert_eq!(flipped.at(0, 4), plain.at(3, 4));
assert_eq!(flipped.at(3, 4), plain.at(0, 4));
}
#[test]
fn easing_reshapes_the_spacing_without_moving_the_ends() {
let eased = Stagger::new(50.0).ease(Easing::In(Curve::Quad));
assert_eq!(eased.at(0, 5), 0.0);
assert_eq!(eased.at(4, 5), 200.0);
assert!(eased.at(1, 5) < 50.0);
}
#[test]
fn values_spread_across_a_range() {
let stagger = Stagger::new(0.0);
assert_eq!(stagger.value(0, 5, -100.0, 100.0), -100.0);
assert_eq!(stagger.value(4, 5, -100.0, 100.0), 100.0);
assert_eq!(stagger.value(2, 5, -100.0, 100.0), 0.0);
}
#[test]
fn a_single_element_never_waits() {
let stagger = Stagger::new(50.0).from(StaggerFrom::Center);
assert_eq!(stagger.at(0, 1), 0.0);
assert_eq!(stagger.span(1), 0.0);
assert_eq!(stagger.at(0, 0), 0.0);
}
}