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}