dinamika_core/timeline/mod.rs
1//! Timeline: composition of animations over time and scene sampling.
2//!
3//! The timeline is created first, then shapes are registered on it via
4//! [`Shape::on`](crate::Shape::on). Animations are appended directly to the
5//! timeline with combinator methods:
6//!
7//! - a pause in seconds — [`Timeline::pause`];
8//! - simultaneous execution — [`Timeline::parallel`];
9//! - sequential execution (optionally with a cascade pause between) —
10//! [`Timeline::sequence`] / [`Timeline::cascade`].
11//!
12//! The elements of these blocks are [`Action`]s, which are most conveniently
13//! obtained directly from shape properties: `shape.x(200.0).over(1.0,
14//! Easing::CubicInOut)` animates the X coordinate (the same setter method, but
15//! with `.over(...)` — see [`Shape`](crate::Shape)). Since both shapes and
16//! signals are shared by `Rc`, the timeline holds only references and can draw a
17//! frame at any moment in time.
18//!
19//! The timeline uses interior mutability, so it does not need to be declared as
20//! `mut`.
21//!
22//! The submodules split the responsibility:
23//! - [`tween`] — an animation leaf over a single signal ([`TweenObj`],
24//! `new_tween`);
25//! - [`action`] — the composition unit [`Action`], its combinators and the
26//! flattening of the tree into a flat list of tweens.
27//!
28//! ```
29//! use dinamika_core::*;
30//!
31//! let tl = Timeline::new(320, 160, Color::from_rgba8(20, 20, 24, 255), 30.0);
32//!
33//! let box_ = Shape::rect()
34//! .at(0.0, 80.0)
35//! .size(40.0, 40.0)
36//! .background(Color::WHITE)
37//! .on(&tl);
38//!
39//! tl.parallel(vec![
40//! box_.x(200.0).over(1.0, Easing::CubicInOut),
41//! box_.background(Color::from_rgba8(229, 192, 123, 255)).over(1.0, Easing::Linear),
42//! ]);
43//! tl.pause(0.5);
44//! tl.sequence(vec![
45//! box_.y(10.0).over(0.5, Easing::QuadOut),
46//! box_.rotation(45.0).over(0.5, Easing::QuadInOut),
47//! ]);
48//!
49//! assert!((tl.duration() - 2.5).abs() < 1e-6);
50//! let _frame = tl.frame(0.5);
51//! ```
52
53use std::cell::RefCell;
54use std::cmp::Ordering;
55use std::rc::{Rc, Weak};
56
57use dinamika_cpu::{Color, Pixmap};
58
59use crate::render;
60use crate::shape::Shape;
61
62mod action;
63mod tween;
64
65pub use action::{cascade, delay, parallel, pause, sequence, Action};
66
67pub(crate) use tween::{new_tween, TweenObj};
68
69use action::{duration_of, flatten};
70
71/// A sampling-ready plan: tweens with their absolute start time set, sorted by
72/// start. The action tree is immutable during rendering, so the plan is assembled
73/// once and reused between frames.
74type Plan = Rc<Vec<Rc<dyn TweenObj>>>;
75
76/// The timeline's shared state.
77///
78/// Moved into an `Rc` so that shapes (via [`Shape::on`]) and the [`Action`]s
79/// built from them can hold a back `Weak` reference and add **themselves** to the
80/// timeline on drop (see `Drop for Action` in [`action`]). This is why a single
81/// animation can simply be written as a statement expression, without wrapping it
82/// in [`sequence`]/[`parallel`].
83pub(crate) struct TimelineState {
84 actions: RefCell<Vec<Action>>,
85 /// A cache of the flattened and sorted sampling plan. Assembled lazily on the
86 /// first sampling and cleared on any change to `actions`.
87 plan: RefCell<Option<Plan>>,
88 shapes: RefCell<Vec<Shape>>,
89 width: u32,
90 height: u32,
91 background: Color,
92 fps: f64,
93}
94
95impl TimelineState {
96 /// Appends a top-level action to the end of the timeline, marking it as
97 /// "accounted for" (so its own `Drop` does not register it again), and clears
98 /// the plan cache. The single point of addition: both the explicit
99 /// `pause`/`parallel`/`sequence`/`cascade` and auto-registration on drop go
100 /// through it.
101 pub(super) fn append(&self, action: Action) {
102 action.mark_registered();
103 self.actions.borrow_mut().push(action);
104 self.invalidate();
105 }
106
107 /// Returns the cached sampling plan, assembling it on first access. The action
108 /// tree is immutable during rendering, so the tweens are flattened (with their
109 /// start time set) and sorted by start once, not on each frame.
110 fn plan(&self) -> Plan {
111 if self.plan.borrow().is_none() {
112 let mut tweens = Vec::new();
113 let mut offset = 0.0;
114 for a in self.actions.borrow().iter() {
115 offset += flatten(a, offset, &mut tweens);
116 }
117 tweens.sort_by(|a, b| a.start().partial_cmp(&b.start()).unwrap_or(Ordering::Equal));
118 *self.plan.borrow_mut() = Some(Rc::new(tweens));
119 }
120 self.plan.borrow().clone().unwrap()
121 }
122
123 /// Clears the plan cache. Called on any change to the action tree.
124 fn invalidate(&self) {
125 *self.plan.borrow_mut() = None;
126 }
127
128 /// The full duration of the timeline in seconds. A pure computation over the
129 /// action tree — it does not touch the tweens' state (unlike
130 /// [`plan`](Self::plan)).
131 fn duration(&self) -> f64 {
132 self.actions.borrow().iter().map(duration_of).sum()
133 }
134
135 /// See [`Timeline::seek`].
136 fn seek(&self, t: f64) {
137 let plan = self.plan();
138 // First reset all signals to their baseline and only then apply the active
139 // tweens: otherwise resetting a tween that hasn't started yet would
140 // overwrite the result of an already-applied tween over the same signal.
141 //
142 // The reset goes in reverse start order (`rev`), so for a value or a
143 // shared cell (e.g. a text stage) touched by several tweens, the "final
144 // say" on reset belongs to the earliest of them — that is, the cell ends
145 // up with the state BEFORE the first animation over it. Otherwise, while
146 // none of the tweens has started yet, the cell would keep the baseline of
147 // the latest tween: for text that is the committed text with edits from
148 // previous blocks already applied, and those would "leak" onto the screen
149 // before their own animation starts.
150 for e in plan.iter().rev() {
151 e.reset();
152 }
153 for e in plan.iter() {
154 if e.start() > t {
155 break;
156 }
157 e.capture_from();
158 e.apply(t);
159 }
160 }
161
162 /// See [`Timeline::frame`].
163 fn frame(&self, t: f64) -> Pixmap {
164 self.seek(t);
165 let shapes = self.shapes.borrow();
166 render::render_scene(self.width, self.height, self.background, &shapes)
167 }
168
169 /// Renders the whole animation into a sequence of frames at the frame rate
170 /// set in [`Timeline::new`].
171 fn frames(&self) -> Vec<Pixmap> {
172 let fps = self.fps.max(1.0);
173 let duration = self.duration();
174 let frame_count = (duration * fps).ceil() as u64 + 1;
175 (0..frame_count).map(|i| self.frame(i as f64 / fps)).collect()
176 }
177}
178
179/// An animation timeline: a top-level sequence of actions plus references to the
180/// shapes to draw.
181///
182/// This is a cheap shared handle (`Rc`) to [`TimelineState`]: interior mutability
183/// lets you add actions and shapes via a shared reference — the timeline does not
184/// need to be held as `mut`. This is consistent with the library's philosophy:
185/// shapes and signals are also shared and mutated via `Rc<RefCell<…>>`.
186///
187/// A shape registered via [`Shape::on`] remembers its timeline, so an animation
188/// built from it can be added by simply writing it as an expression:
189/// `title.content("Hi").smooth(0.5, Easing::CubicInOut);` — it appends itself to
190/// the end of the timeline. Wrapping a single animation in [`sequence`] is no
191/// longer needed; [`parallel`]/[`sequence`]/[`cascade`] are left for **grouping**
192/// several actions.
193pub struct Timeline {
194 inner: Rc<TimelineState>,
195}
196
197impl Timeline {
198 /// An empty timeline with render parameters: frame size, background and frame
199 /// rate. These parameters are set once here and then used by the
200 /// [`frame`](Self::frame) and [`render`](Self::render) methods — there is no
201 /// need to pass them to the render itself anymore.
202 pub fn new(width: u32, height: u32, background: Color, fps: f64) -> Self {
203 Timeline {
204 inner: Rc::new(TimelineState {
205 actions: RefCell::new(Vec::new()),
206 plan: RefCell::new(None),
207 shapes: RefCell::new(Vec::new()),
208 width,
209 height,
210 background,
211 fps,
212 }),
213 }
214 }
215
216 /// A `Weak` reference to the timeline state — shapes receive it in
217 /// [`Shape::on`] so the actions built from them can auto-register.
218 pub(crate) fn weak(&self) -> Weak<TimelineState> {
219 Rc::downgrade(&self.inner)
220 }
221
222 /// Appends a pause of `seconds` seconds to the end of the timeline.
223 pub fn pause(&self, seconds: f64) -> &Self {
224 self.inner.append(pause(seconds));
225 self
226 }
227
228 /// Appends a block of simultaneous actions (the duration is the maximum of the
229 /// nested ones).
230 pub fn parallel(&self, items: impl IntoIterator<Item = Action>) -> &Self {
231 self.inner.append(parallel(items));
232 self
233 }
234
235 /// Appends a block of sequential actions (with no gaps).
236 pub fn sequence(&self, items: impl IntoIterator<Item = Action>) -> &Self {
237 self.inner.append(sequence(items));
238 self
239 }
240
241 /// Appends a cascade of animations: a sequence with a `gap`-second pause
242 /// between neighbors. Handy for a "wave" effect — add here the animations that
243 /// should start one after another at an equal interval.
244 pub fn cascade(&self, items: impl IntoIterator<Item = Action>, gap: f64) -> &Self {
245 self.inner.append(cascade(items, gap));
246 self
247 }
248
249 /// Registers a shape for drawing. Called from [`Shape::on`]; root shapes are
250 /// drawn in the order of addition.
251 pub(crate) fn register_shape(&self, shape: Shape) {
252 self.inner.shapes.borrow_mut().push(shape);
253 }
254
255 /// The registered root shapes (clones of the `Rc` handles).
256 pub fn shapes(&self) -> Vec<Shape> {
257 self.inner.shapes.borrow().clone()
258 }
259
260 /// The full duration of the timeline in seconds. A pure computation over the
261 /// action tree — it does not touch the tweens' state (unlike sampling).
262 pub fn duration(&self) -> f64 {
263 self.inner.duration()
264 }
265
266 /// Brings all participating signals to their state at the moment in time `t`.
267 ///
268 /// Sampling is deterministic: first all values are reset to their baseline,
269 /// then tweens are applied in start order — so tweens over one signal
270 /// correctly "pick up" the previous value.
271 pub fn seek(&self, t: f64) {
272 self.inner.seek(t);
273 }
274
275 /// Renders a single frame at the moment in time `t` with the size and
276 /// background set in [`Timeline::new`].
277 pub fn frame(&self, t: f64) -> Pixmap {
278 self.inner.frame(t)
279 }
280
281 /// Renders the whole animation into a sequence of frames at the frame rate set
282 /// in [`Timeline::new`]. Used by [`render`](Self::render).
283 pub(crate) fn frames(&self) -> Vec<Pixmap> {
284 self.inner.frames()
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use crate::easing::Easing;
292 use crate::signal::Signal;
293
294 #[test]
295 fn sequence_chains_durations() {
296 let s = Signal::new(0.0_f32);
297 let tl = Timeline::new(64, 64, Color::BLACK, 30.0);
298 tl.sequence(vec![
299 s.tween_to(10.0, 1.0, Easing::Linear),
300 s.tween_to(20.0, 1.0, Easing::Linear),
301 ]);
302
303 assert!((tl.duration() - 2.0).abs() < 1e-6);
304
305 tl.seek(0.0);
306 assert!((s.get() - 0.0).abs() < 1e-3);
307 tl.seek(0.5);
308 assert!((s.get() - 5.0).abs() < 1e-3);
309 tl.seek(1.0);
310 assert!((s.get() - 10.0).abs() < 1e-3);
311 tl.seek(1.5);
312 assert!((s.get() - 15.0).abs() < 1e-3);
313 tl.seek(2.0);
314 assert!((s.get() - 20.0).abs() < 1e-3);
315 }
316
317 #[test]
318 fn parallel_runs_simultaneously() {
319 let a = Signal::new(0.0_f32);
320 let b = Signal::new(0.0_f32);
321 let tl = Timeline::new(64, 64, Color::BLACK, 30.0);
322 tl.parallel(vec![
323 a.tween_to(100.0, 1.0, Easing::Linear),
324 b.tween_to(50.0, 2.0, Easing::Linear),
325 ]);
326
327 assert!((tl.duration() - 2.0).abs() < 1e-6);
328 tl.seek(1.0);
329 assert!((a.get() - 100.0).abs() < 1e-3, "a={}", a.get());
330 assert!((b.get() - 25.0).abs() < 1e-3, "b={}", b.get());
331 }
332
333 #[test]
334 fn pause_offsets_following_actions() {
335 let s = Signal::new(0.0_f32);
336 let tl = Timeline::new(64, 64, Color::BLACK, 30.0);
337 tl.pause(1.0);
338 tl.sequence(vec![s.tween_to(10.0, 1.0, Easing::Linear)]);
339
340 assert!((tl.duration() - 2.0).abs() < 1e-6);
341 tl.seek(1.0);
342 assert!((s.get() - 0.0).abs() < 1e-3);
343 tl.seek(1.5);
344 assert!((s.get() - 5.0).abs() < 1e-3);
345 }
346
347 #[test]
348 fn seek_is_reversible() {
349 let s = Signal::new(0.0_f32);
350 let tl = Timeline::new(64, 64, Color::BLACK, 30.0);
351 tl.sequence(vec![s.tween_to(10.0, 1.0, Easing::Linear)]);
352
353 tl.seek(1.0);
354 assert!((s.get() - 10.0).abs() < 1e-3);
355 // Seeking back should return an intermediate value, not stay at 10.
356 tl.seek(0.25);
357 assert!((s.get() - 2.5).abs() < 1e-3, "got {}", s.get());
358 }
359
360 #[test]
361 fn cascade_inserts_pause() {
362 let s = Signal::new(0.0_f32);
363 let tl = Timeline::new(64, 64, Color::BLACK, 30.0);
364 tl.cascade(
365 vec![
366 s.tween_to(10.0, 1.0, Easing::Linear),
367 s.tween_to(20.0, 1.0, Easing::Linear),
368 ],
369 0.5,
370 );
371 // 1.0 + 0.5 (pause) + 1.0
372 assert!((tl.duration() - 2.5).abs() < 1e-6);
373 tl.seek(1.25); // within the pause — hold 10
374 assert!((s.get() - 10.0).abs() < 1e-3, "got {}", s.get());
375 }
376}