Skip to main content

motion_canvas_rs/core/animation/
tween.rs

1use crate::core::animation::base::Animation;
2use crate::elements::shapes::{PathData, PathNode};
3use glam::Vec2;
4use kurbo::Affine;
5use peniko::Color;
6use std::sync::{Arc, Mutex};
7use std::time::Duration;
8
9const DEFAULT_EASING: fn(f32) -> f32 = crate::core::easings::cubic_in_out;
10
11fn lerp(a: f32, b: f32, t: f32) -> f32 {
12    a + (b - a) * t
13}
14
15/// A trait for types that can be interpolated over time.
16pub trait Tweenable: Clone + Send + Sync + std::fmt::Debug + 'static {
17    /// Interpolates between `a` and `b` by factor `t` (0.0 to 1.0).
18    fn interpolate(a: &Self, b: &Self, t: f32) -> Self;
19    /// Returns a hash of the current value for change detection.
20    fn state_hash(&self) -> u64;
21}
22
23impl Tweenable for f32 {
24    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
25        lerp(*a, *b, t)
26    }
27    fn state_hash(&self) -> u64 {
28        crate::assets::hash::hash_f32(*self)
29    }
30}
31
32impl Tweenable for Vec2 {
33    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
34        Vec2::new(lerp(a.x, b.x, t), lerp(a.y, b.y, t))
35    }
36    fn state_hash(&self) -> u64 {
37        crate::assets::hash::combine_hashes(
38            crate::assets::hash::hash_f32(self.x),
39            crate::assets::hash::hash_f32(self.y),
40        )
41    }
42}
43
44impl Tweenable for Vec<Vec2> {
45    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
46        if a.len() == b.len() {
47            a.iter()
48                .zip(b.iter())
49                .map(|(v1, v2)| Vec2::interpolate(v1, v2, t))
50                .collect()
51        } else if t >= 1.0 {
52            b.clone()
53        } else {
54            a.clone()
55        }
56    }
57    fn state_hash(&self) -> u64 {
58        let mut h = crate::assets::hash::Hasher::new();
59        for v in self {
60            h.update_u64(v.state_hash());
61        }
62        h.finish()
63    }
64}
65
66impl Tweenable for bool {
67    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
68        if t >= 1.0 {
69            *b
70        } else {
71            *a
72        }
73    }
74    fn state_hash(&self) -> u64 {
75        if *self {
76            1
77        } else {
78            0
79        }
80    }
81}
82
83impl Tweenable for String {
84    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
85        if t >= 1.0 {
86            b.clone()
87        } else {
88            a.clone()
89        }
90    }
91    fn state_hash(&self) -> u64 {
92        crate::assets::hash::hash_str(self)
93    }
94}
95
96impl Tweenable for Color {
97    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
98        let t = t.clamp(0.0, 1.0);
99        Color::rgba8(
100            lerp(a.r as f32, b.r as f32, t) as u8,
101            lerp(a.g as f32, b.g as f32, t) as u8,
102            lerp(a.b as f32, b.b as f32, t) as u8,
103            lerp(a.a as f32, b.a as f32, t) as u8,
104        )
105    }
106    fn state_hash(&self) -> u64 {
107        let mut h = crate::assets::hash::Hasher::new();
108        h.update_bytes(&[self.r, self.g, self.b, self.a]);
109        h.finish()
110    }
111}
112
113impl Tweenable for Affine {
114    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
115        let t = t as f64;
116        let c1 = a.as_coeffs();
117        let c2 = b.as_coeffs();
118        Affine::new([
119            c1[0] + (c2[0] - c1[0]) * t,
120            c1[1] + (c2[1] - c1[1]) * t,
121            c1[2] + (c2[2] - c1[2]) * t,
122            c1[3] + (c2[3] - c1[3]) * t,
123            c1[4] + (c2[4] - c1[4]) * t,
124            c1[5] + (c2[5] - c1[5]) * t,
125        ])
126    }
127    fn state_hash(&self) -> u64 {
128        let c = self.as_coeffs();
129        let mut h = crate::assets::hash::Hasher::new();
130        for val in c {
131            h.update_u64(val.to_bits() as u64);
132        }
133        h.finish()
134    }
135}
136
137impl<T: Tweenable> Tweenable for Option<T> {
138    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
139        match (a, b) {
140            (Some(va), Some(vb)) => Some(T::interpolate(va, vb, t)),
141            (Some(va), None) => {
142                if t < 0.5 {
143                    Some(va.clone())
144                } else {
145                    None
146                }
147            }
148            (None, Some(vb)) => {
149                if t >= 0.5 {
150                    Some(vb.clone())
151                } else {
152                    None
153                }
154            }
155            (None, None) => None,
156        }
157    }
158    fn state_hash(&self) -> u64 {
159        match self {
160            Some(v) => {
161                let mut h = crate::assets::hash::Hasher::new();
162                h.update_u64(1);
163                h.update_u64(v.state_hash());
164                h.finish()
165            }
166            None => 0,
167        }
168    }
169}
170
171/// Internal data for a signal.
172pub struct SignalData<T> {
173    /// The current value of the signal.
174    pub value: T,
175    /// The initial value the signal was created with.
176    pub initial: T,
177}
178
179/// A reactive wrapper for a value that can be tweened over time.
180///
181/// `Signal` is the primary way to define animatable properties for `Node` elements.
182/// It wraps a `Tweenable` value and provides methods to create animations that
183/// change this value over time.
184///
185/// ### Example
186/// ```rust
187/// # use motion_canvas_rs::prelude::*;
188/// # use glam::Vec2;
189/// # use std::time::Duration;
190/// let pos = Signal::new(Vec2::ZERO);
191/// let tween = pos.to(Vec2::new(100.0, 100.0), Duration::from_secs(1));
192/// ```
193#[derive(Clone)]
194pub struct Signal<T> {
195    pub data: Arc<Mutex<SignalData<T>>>,
196}
197
198/// A trait for types that can be constructed from a `Vec2`.
199/// Used for mapping path samples to signal types (e.g. `Vec2` or `Affine`).
200pub trait FromVec2: Send + Sync + 'static {
201    fn from_vec2(v: Vec2) -> Self;
202}
203
204impl FromVec2 for Vec2 {
205    fn from_vec2(v: Vec2) -> Self {
206        v
207    }
208}
209
210impl FromVec2 for Affine {
211    fn from_vec2(v: Vec2) -> Self {
212        Affine::translate((v.x as f64, v.y as f64))
213    }
214}
215
216/// The target of a tween animation.
217pub enum Target<T> {
218    /// A specific fixed value.
219    Fixed(T),
220    /// A value calculated lazily at the start of the animation based on the signal's state at that time.
221    Lazy(Arc<dyn Fn(&T) -> T + Send + Sync>),
222}
223
224impl<T: Clone> Clone for Target<T> {
225    fn clone(&self) -> Self {
226        match self {
227            Self::Fixed(v) => Self::Fixed(v.clone()),
228            Self::Lazy(f) => Self::Lazy(f.clone()),
229        }
230    }
231}
232impl<T: Tweenable + PartialEq> Signal<T> {
233    /// Creates a new signal with the given initial value.
234    pub fn new(value: T) -> Self {
235        Self {
236            data: Arc::new(Mutex::new(SignalData {
237                value: value.clone(),
238                initial: value,
239            })),
240        }
241    }
242
243    /// Retrieves a clone of the current value.
244    pub fn get(&self) -> T {
245        self.data.lock().unwrap().value.clone()
246    }
247
248    /// Manually overrides the current value.
249    pub fn set(&self, value: T) {
250        let mut data = self.data.lock().unwrap();
251        if data.value != value {
252            data.value = value;
253        }
254    }
255
256    /// Returns a hash of the current value.
257    pub fn state_hash(&self) -> u64 {
258        self.data.lock().unwrap().value.state_hash()
259    }
260
261    /// Resets the signal to its initial value.
262    pub fn reset(&self) {
263        let mut data = self.data.lock().unwrap();
264        data.value = data.initial.clone();
265    }
266
267    /// Returns a tween animation that changes the signal to a fixed target value.
268    pub fn to(&self, target: T, duration: Duration) -> SignalTween<T> {
269        SignalTween {
270            data: self.data.clone(),
271            start_value: None,
272            target: Target::Fixed(target),
273            target_value: None,
274            duration,
275            elapsed: Duration::ZERO,
276            easing: DEFAULT_EASING,
277        }
278    }
279
280    /// Returns a tween animation where the target is calculated lazily when the animation starts.
281    ///
282    /// Useful for relative offsets (e.g., `pos.to_lazy(|v| *v + offset, duration)`).
283    pub fn to_lazy<F>(&self, factory: F, duration: Duration) -> SignalTween<T>
284    where
285        F: Fn(&T) -> T + Send + Sync + 'static,
286    {
287        SignalTween {
288            data: self.data.clone(),
289            start_value: None,
290            target: Target::Lazy(Arc::new(factory)),
291            target_value: None,
292            duration,
293            elapsed: Duration::ZERO,
294            easing: DEFAULT_EASING,
295        }
296    }
297
298    /// Returns an animation that makes the signal's value follow a `PathNode`.
299    pub fn follow(&self, path: &PathNode, duration: Duration) -> FollowPath<T>
300    where
301        T: FromVec2,
302    {
303        FollowPath {
304            data: self.data.clone(),
305            path_data: path.data.clone(),
306            duration,
307            elapsed: Duration::ZERO,
308            easing: DEFAULT_EASING,
309        }
310    }
311
312    /// Binds this signal to another signal, transforming its value via a mapper function.
313    pub fn bind<S: Tweenable + PartialEq, F>(
314        &self,
315        source: Signal<S>,
316        mapper: F,
317    ) -> crate::core::animation::binding::BindingNode<T, S>
318    where
319        F: Fn(S) -> T + Send + Sync + 'static,
320    {
321        crate::core::animation::binding::BindingNode::new(source, self.clone(), mapper)
322    }
323}
324
325/// An animation that changes a `Signal`'s value over time.
326pub struct SignalTween<T> {
327    data: Arc<Mutex<SignalData<T>>>,
328    start_value: Option<T>,
329    target: Target<T>,
330    target_value: Option<T>,
331    duration: Duration,
332    elapsed: Duration,
333    easing: fn(f32) -> f32,
334}
335
336impl<T: Tweenable> SignalTween<T> {
337    /// Applies a custom easing function to this tween.
338    pub fn ease(mut self, easing: fn(f32) -> f32) -> Self {
339        self.easing = easing;
340        self
341    }
342}
343
344impl<T: Tweenable> Animation for SignalTween<T> {
345    fn update(&mut self, dt: Duration) -> (bool, Duration) {
346        // Capture values on first update
347        if self.start_value.is_none() {
348            let current = self.data.lock().unwrap().value.clone();
349            self.start_value = Some(current.clone());
350
351            // Evaluate target if needed
352            if self.target_value.is_none() {
353                match &self.target {
354                    Target::Fixed(v) => self.target_value = Some(v.clone()),
355                    Target::Lazy(f) => self.target_value = Some(f(&current)),
356                }
357            }
358        }
359
360        let target = self.target_value.as_ref().unwrap();
361
362        if self.duration == Duration::ZERO {
363            let mut data = self.data.lock().unwrap();
364            data.value = target.clone();
365            return (true, dt);
366        }
367
368        self.elapsed += dt;
369        let finished = self.elapsed >= self.duration;
370        let leftover = if finished {
371            self.elapsed - self.duration
372        } else {
373            Duration::ZERO
374        };
375
376        let t_linear = (self.elapsed.as_secs_f32() / self.duration.as_secs_f32()).min(1.0);
377        let t_eased = (self.easing)(t_linear);
378
379        let start = self.start_value.as_ref().unwrap();
380        let mut data = self.data.lock().unwrap();
381        data.value = T::interpolate(start, target, t_eased);
382
383        (finished, leftover)
384    }
385
386    fn duration(&self) -> Duration {
387        self.duration
388    }
389
390    fn set_easing(&mut self, easing: fn(f32) -> f32) {
391        self.easing = easing;
392    }
393
394    fn reset(&mut self) {
395        self.start_value = None;
396        self.target_value = None;
397        self.elapsed = Duration::ZERO;
398
399        let mut data = self.data.lock().unwrap();
400        data.value = data.initial.clone();
401    }
402}
403
404/// An animation that samples a `PathNode`'s geometry to update a `Signal`.
405pub struct FollowPath<T> {
406    data: Arc<Mutex<SignalData<T>>>,
407    path_data: Arc<PathData>,
408    duration: Duration,
409    elapsed: Duration,
410    easing: fn(f32) -> f32,
411}
412
413impl<T: Send + Sync + 'static> FollowPath<T> {
414    /// Applies a custom easing function to this animation.
415    pub fn ease(mut self, easing: fn(f32) -> f32) -> Self {
416        self.easing = easing;
417        self
418    }
419}
420
421impl<T: Tweenable + FromVec2> Animation for FollowPath<T> {
422    fn update(&mut self, dt: Duration) -> (bool, Duration) {
423        if self.duration == Duration::ZERO {
424            let mut data = self.data.lock().unwrap();
425            data.value = T::from_vec2(self.path_data.sample(1.0));
426            return (true, dt);
427        }
428
429        self.elapsed += dt;
430        let finished = self.elapsed >= self.duration;
431        let leftover = if finished {
432            self.elapsed - self.duration
433        } else {
434            Duration::ZERO
435        };
436
437        let t_linear = (self.elapsed.as_secs_f32() / self.duration.as_secs_f32()).min(1.0);
438        let t_eased = (self.easing)(t_linear);
439
440        let mut data = self.data.lock().unwrap();
441        data.value = T::from_vec2(self.path_data.sample(t_eased));
442
443        (finished, leftover)
444    }
445
446    fn duration(&self) -> Duration {
447        self.duration
448    }
449
450    fn set_easing(&mut self, easing: fn(f32) -> f32) {
451        self.easing = easing;
452    }
453
454    fn reset(&mut self) {
455        self.elapsed = Duration::ZERO;
456        let mut data = self.data.lock().unwrap();
457        data.value = data.initial.clone();
458    }
459}
460
461impl<T: Tweenable> From<SignalTween<T>> for Box<dyn Animation> {
462    fn from(tween: SignalTween<T>) -> Self {
463        Box::new(tween)
464    }
465}
466
467impl<T: Tweenable + FromVec2> From<FollowPath<T>> for Box<dyn Animation> {
468    fn from(anim: FollowPath<T>) -> Self {
469        Box::new(anim)
470    }
471}