dinamika_core/signal/mod.rs
1//! Reactive signals in the spirit of [Motion Canvas].
2//!
3//! [`Signal`] is a shared value container (`Rc<RefCell<T>>`). A signal can be
4//! read ([`Signal::get`]), written ([`Signal::set`]) and, most importantly,
5//! animated — [`Signal::tween_to`] returns an [`Action`] for the timeline.
6//!
7//! All shape properties are signals, so any characteristic can be animated
8//! uniformly:
9//!
10//! ```
11//! use dinamika_core::*;
12//!
13//! let s = Signal::new(0.0_f32);
14//! assert_eq!(s.get(), 0.0);
15//! s.set(10.0);
16//! assert_eq!(s.get(), 10.0);
17//! let _action = s.tween_to(100.0, 1.0, Easing::CubicInOut);
18//! ```
19//!
20//! The submodules split the responsibility:
21//! - [`tweenable`] — the [`Tweenable`] trait and its implementations for built-in
22//! types;
23//! - [`computed`] — the read-only derived signal [`Computed`].
24//!
25//! [Motion Canvas]: https://motioncanvas.io/
26
27use std::cell::RefCell;
28use std::rc::Rc;
29
30use crate::easing::Easing;
31use crate::timeline::{new_tween, Action};
32
33mod computed;
34mod tweenable;
35
36pub use computed::Computed;
37pub use tweenable::Tweenable;
38
39/// A reactive value, shared by reference.
40///
41/// Cloning is cheap (shared `Rc`): a copy points to the same value, so a signal
42/// passed to the timeline and a signal inside a shape are one object.
43pub struct Signal<T> {
44 cell: Rc<RefCell<T>>,
45}
46
47impl<T> Clone for Signal<T> {
48 fn clone(&self) -> Self {
49 Signal { cell: Rc::clone(&self.cell) }
50 }
51}
52
53impl<T: Tweenable> Signal<T> {
54 /// Creates a signal with an initial value.
55 pub fn new(value: T) -> Self {
56 Signal { cell: Rc::new(RefCell::new(value)) }
57 }
58
59 /// The current value (a clone).
60 pub fn get(&self) -> T {
61 self.cell.borrow().clone()
62 }
63
64 /// Writes the value immediately.
65 pub fn set(&self, value: T) {
66 *self.cell.borrow_mut() = value;
67 }
68
69 /// Creates an animation from the current value to `to` over `duration`
70 /// seconds.
71 ///
72 /// The start value is captured at the moment the animation runs on the
73 /// timeline, so consecutive tweens neatly "pick up" from each other.
74 pub fn tween_to(&self, to: T, duration: f64, easing: Easing) -> Action {
75 new_tween(self.cell.clone(), self.get(), to, duration, easing)
76 }
77
78 /// An animation from an explicit `from` to `to` over `duration` seconds.
79 ///
80 /// Unlike [`tween_to`](Signal::tween_to), the start is taken not from the
81 /// current value but from the passed `from`. This is needed by the shape
82 /// setter methods: they set the value immediately, but on animation should
83 /// start from the previous one.
84 pub(crate) fn tween_from(&self, from: T, to: T, duration: f64, easing: Easing) -> Action {
85 new_tween(self.cell.clone(), from, to, duration, easing)
86 }
87
88 /// An instant value set as a timeline element (a zero-length tween).
89 pub fn step_to(&self, to: T) -> Action {
90 new_tween(self.cell.clone(), self.get(), to, 0.0, Easing::Linear)
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn signal_shares_value() {
100 // The clone shares the same value.
101 let a = Signal::new(1.0_f32);
102 let b = a.clone();
103 a.set(42.0);
104 assert_eq!(b.get(), 42.0);
105 }
106}