Skip to main content

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}