Skip to main content

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}