Skip to main content

ranim_core/animation/compose/
lagged.rs

1//! Lagged (staggered, end-filled) authoring combinator.
2//!
3//! [`AnimLagged`] is build-time only: it staggers children by placing them
4//! on the content axis and lowers to a plain stack of per-item sequence
5//! tracks in [`IntoAnimNode::into_anim_node`] — no runtime kind of its own.
6
7use std::any::type_name;
8
9use crate::animation::build::{IntoAnimNode, Unplaced};
10use crate::animation::compose::sequence::AnimSequence;
11use crate::animation::node::{AnimNode, NodeContent, static_cell};
12
13/// How an [`AnimLagged`] fills the time outside a child's window.
14///
15/// Fills are materialized as real static cells at `build` time (sampled from
16/// the child's window edges), so the preview timeline shows exactly what is
17/// rendered — there is no hidden clamping rule.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum LaggedFill {
20    /// Render nothing outside the window.
21    Empty,
22    /// Keep showing the window-edge state with a static animation
23    /// (the initial state before, the final state after).
24    Hold,
25}
26
27/// Dynamic lagged (staggered, end-filled) animation container.
28///
29/// Children are pushed un-placed ([`Unplaced`]); the container computes the
30/// placement itself: child `i` starts at `start_{i-1} + lag_ratio · d_{i-1}`.
31/// `lag_ratio` interpolates between the other two containers:
32///
33/// - `0.0` — all children start together (like [`AnimStack`](super::stack::AnimStack));
34/// - `1.0` — each child starts when the previous ends (like [`AnimSequence`]);
35/// - in between — overlapping succession.
36///
37/// By default the time outside a child's window is **filled with real static
38/// cells** ([`LaggedFill::Hold`] on both ends): each item is materialized at
39/// build time as a per-item sequence track `[leading fill][anim][trailing
40/// fill]` spanning the whole extent — before its start the item shows its
41/// initial state, after its end it keeps showing its final state, and the
42/// preview timeline shows exactly what is rendered. Configure with
43/// [`with_leading`](Self::with_leading) /
44/// [`with_trailing`](Self::with_trailing) — e.g. `Empty` leading makes items
45/// appear only at their window; to hide an item after its window instead of
46/// holding it, give it an animation that ends hidden, e.g.
47/// `seq![item.fade_in(), item.hide()]`.
48///
49/// Fills are sampled at build time: empty fills are skipped, and a
50/// zero-duration child gets no leading fill (it appears at its point, which is
51/// what "show from that point on" means). Because fills are build-time
52/// samples, children are expected to be pure (closed-form) animations — a
53/// stateful child's trailing fill would be its initial state, not its true
54/// final state.
55pub struct AnimLagged {
56    animations: Vec<AnimNode>,
57    lag_ratio: f64,
58    /// Start offset for the next pushed child.
59    cursor_sec: f64,
60    duration_secs: f64,
61    leading: LaggedFill,
62    trailing: LaggedFill,
63}
64
65impl AnimLagged {
66    /// Create an empty lagged container with the given stagger ratio.
67    pub fn new(lag_ratio: f64) -> Self {
68        assert!(
69            lag_ratio.is_finite() && lag_ratio >= 0.0,
70            "lag ratio must be finite and non-negative"
71        );
72        Self {
73            animations: Vec::new(),
74            lag_ratio,
75            cursor_sec: 0.0,
76            duration_secs: 0.0,
77            leading: LaggedFill::Hold,
78            trailing: LaggedFill::Hold,
79        }
80    }
81
82    /// The stagger ratio between successive children.
83    pub fn lag_ratio(&self) -> f64 {
84        self.lag_ratio
85    }
86
87    /// Configure the fill before each child's window (default
88    /// [`LaggedFill::Hold`]).
89    pub fn with_leading(mut self, behavior: LaggedFill) -> Self {
90        self.leading = behavior;
91        self
92    }
93
94    /// Configure the fill after each child's window (default
95    /// [`LaggedFill::Hold`]).
96    pub fn with_trailing(mut self, behavior: LaggedFill) -> Self {
97        self.trailing = behavior;
98        self
99    }
100
101    /// Add an animation, placed by the container's stagger rule.
102    pub fn push<A: Unplaced + 'static>(&mut self, animation: A) -> &mut Self {
103        let mut animation = animation.into_anim_node();
104        let duration_secs = animation.duration_secs();
105        animation.shift_by(self.cursor_sec);
106        self.duration_secs = self.duration_secs.max(animation.time_range.end);
107        self.animations.push(animation);
108        self.cursor_sec += self.lag_ratio * duration_secs;
109        self
110    }
111
112    /// Materialize each child as a per-item sequence track:
113    /// `[leading fill][anim][trailing fill]` (empty fills skipped).
114    ///
115    /// The lagged container is thus a stack of per-item sequences — each
116    /// item's track spans the whole extent, with its window-edge states held
117    /// by real static cells.
118    fn materialize_fills(&mut self) {
119        let total = self.duration_secs;
120        let children = std::mem::take(&mut self.animations);
121        let mut animations = Vec::with_capacity(children.len());
122        for child in children {
123            let start = child.time_range.start;
124            let end = child.time_range.end;
125            let mut track = AnimSequence::new();
126            if self.leading == LaggedFill::Hold && start > 0.0 && child.duration_secs() > 0.0 {
127                let mut state = Vec::new();
128                child.eval_at(start, &mut state);
129                if !state.is_empty() {
130                    track.animations.push(static_cell(state, 0.0..start));
131                }
132            }
133            track.animations.push(child);
134            if self.trailing == LaggedFill::Hold && end < total {
135                let child = track.animations.last().unwrap();
136                let mut state = Vec::new();
137                child.eval_at(end, &mut state);
138                if !state.is_empty() {
139                    track.animations.push(static_cell(state, end..total));
140                }
141            }
142            track.cursor_sec = total;
143            animations.push(track.into_anim_node());
144        }
145        self.animations = animations;
146    }
147
148    /// Current total extent (the last child's end).
149    pub fn duration_secs(&self) -> f64 {
150        self.duration_secs
151    }
152
153    /// Borrow the children as placed by the stagger rule — the
154    /// pre-materialization view, before `build` turns them into filled
155    /// tracks.
156    pub fn built_animations(&self) -> &[AnimNode] {
157        &self.animations
158    }
159}
160
161impl Unplaced for AnimLagged {}
162impl IntoAnimNode for AnimLagged {
163    /// Desugar: materialize each item into a full-extent sequence track,
164    /// then lower the whole container to a plain runtime stack node — the
165    /// stagger is ordinary window placement, so lagged needs no
166    /// runtime kind of its own. `anim_name` keeps the authoring identity.
167    fn into_anim_node(mut self) -> AnimNode {
168        self.materialize_fills();
169        let duration_secs = self.duration_secs;
170        AnimNode {
171            content: NodeContent::Stack(self.animations),
172            internal_time_secs: duration_secs,
173            rate_func: None,
174            time_range: 0.0..duration_secs,
175            enabled: true,
176            anim_name: type_name::<Self>(),
177        }
178    }
179}
180
181/// Construct an [`AnimLagged`] with a stagger ratio, pushing each animation in order.
182#[macro_export]
183macro_rules! lagged {
184    ($lag_ratio:expr; $($animation:expr),* $(,)?) => {
185        {
186            #[allow(unused_mut)]
187            let mut lagged = $crate::animation::compose::lagged::AnimLagged::new($lag_ratio);
188            $(lagged.push($animation);)*
189            lagged
190        }
191    };
192}