motion-canvas-rs 0.1.7

A high-performance vector animation engine inspired by Motion Canvas, built on Vello and Typst.
Documentation
use crate::engine::animation::base::Animation;
use crate::engine::nodes::{PathData, PathNode};
use glam::Vec2;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use vello::kurbo::Affine;
use vello::peniko::Color;

const DEFAULT_EASING: fn(f32) -> f32 = crate::engine::easings::cubic_in_out;

fn lerp(a: f32, b: f32, t: f32) -> f32 {
    a + (b - a) * t
}

pub trait Tweenable: Clone + Send + Sync + std::fmt::Debug + 'static {
    fn interpolate(a: &Self, b: &Self, t: f32) -> Self;
    fn state_hash(&self) -> u64;
}

impl Tweenable for f32 {
    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
        lerp(*a, *b, t)
    }
    fn state_hash(&self) -> u64 {
        crate::engine::util::hash::hash_f32(*self)
    }
}

impl Tweenable for Vec2 {
    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
        Vec2::new(lerp(a.x, b.x, t), lerp(a.y, b.y, t))
    }
    fn state_hash(&self) -> u64 {
        crate::engine::util::hash::combine_hashes(
            crate::engine::util::hash::hash_f32(self.x),
            crate::engine::util::hash::hash_f32(self.y),
        )
    }
}

impl Tweenable for Vec<Vec2> {
    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
        if a.len() == b.len() {
            a.iter()
                .zip(b.iter())
                .map(|(v1, v2)| Vec2::interpolate(v1, v2, t))
                .collect()
        } else if t >= 1.0 {
            b.clone()
        } else {
            a.clone()
        }
    }
    fn state_hash(&self) -> u64 {
        let mut h = crate::engine::util::hash::Hasher::new();
        for v in self {
            h.update_u64(v.state_hash());
        }
        h.finish()
    }
}

impl Tweenable for String {
    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
        if t >= 1.0 {
            b.clone()
        } else {
            a.clone()
        }
    }
    fn state_hash(&self) -> u64 {
        crate::engine::util::hash::hash_str(self)
    }
}

impl Tweenable for Color {
    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
        let t = t.clamp(0.0, 1.0);
        Color::rgba8(
            lerp(a.r as f32, b.r as f32, t) as u8,
            lerp(a.g as f32, b.g as f32, t) as u8,
            lerp(a.b as f32, b.b as f32, t) as u8,
            lerp(a.a as f32, b.a as f32, t) as u8,
        )
    }
    fn state_hash(&self) -> u64 {
        let mut h = crate::engine::util::hash::Hasher::new();
        h.update_bytes(&[self.r, self.g, self.b, self.a]);
        h.finish()
    }
}

impl Tweenable for Affine {
    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
        let t = t as f64;
        let c1 = a.as_coeffs();
        let c2 = b.as_coeffs();
        Affine::new([
            c1[0] + (c2[0] - c1[0]) * t,
            c1[1] + (c2[1] - c1[1]) * t,
            c1[2] + (c2[2] - c1[2]) * t,
            c1[3] + (c2[3] - c1[3]) * t,
            c1[4] + (c2[4] - c1[4]) * t,
            c1[5] + (c2[5] - c1[5]) * t,
        ])
    }
    fn state_hash(&self) -> u64 {
        let c = self.as_coeffs();
        let mut h = crate::engine::util::hash::Hasher::new();
        for val in c {
            h.update_u64(val.to_bits() as u64);
        }
        h.finish()
    }
}

/// SignalData now only stores the current value, allowing multiple animations
/// to control it sequentially or in parallel without state interference.
pub struct SignalData<T> {
    pub value: T,
    pub initial: T,
}

#[derive(Clone)]
pub struct Signal<T> {
    pub data: Arc<Mutex<SignalData<T>>>,
}

pub trait FromVec2: Send + Sync + 'static {
    fn from_vec2(v: Vec2) -> Self;
}

impl FromVec2 for Vec2 {
    fn from_vec2(v: Vec2) -> Self {
        v
    }
}

impl FromVec2 for Affine {
    fn from_vec2(v: Vec2) -> Self {
        Affine::translate((v.x as f64, v.y as f64))
    }
}

pub enum Target<T> {
    Fixed(T),
    Lazy(Arc<dyn Fn(&T) -> T + Send + Sync>),
}

impl<T: Clone> Clone for Target<T> {
    fn clone(&self) -> Self {
        match self {
            Self::Fixed(v) => Self::Fixed(v.clone()),
            Self::Lazy(f) => Self::Lazy(f.clone()),
        }
    }
}

impl<T: Tweenable + PartialEq> Signal<T> {
    pub fn new(value: T) -> Self {
        Self {
            data: Arc::new(Mutex::new(SignalData {
                value: value.clone(),
                initial: value,
            })),
        }
    }

    pub fn get(&self) -> T {
        self.data.lock().unwrap().value.clone()
    }

    pub fn set(&self, value: T) {
        let mut data = self.data.lock().unwrap();
        if data.value != value {
            data.value = value;
        }
    }

    pub fn state_hash(&self) -> u64 {
        self.data.lock().unwrap().value.state_hash()
    }

    pub fn reset(&self) {
        let mut data = self.data.lock().unwrap();
        data.value = data.initial.clone();
    }

    pub fn to(&self, target: T, duration: Duration) -> SignalTween<T> {
        SignalTween {
            data: self.data.clone(),
            start_value: None,
            target: Target::Fixed(target),
            target_value: None,
            duration,
            elapsed: Duration::ZERO,
            easing: DEFAULT_EASING,
        }
    }

    pub fn to_lazy<F>(&self, factory: F, duration: Duration) -> SignalTween<T>
    where
        F: Fn(&T) -> T + Send + Sync + 'static,
    {
        SignalTween {
            data: self.data.clone(),
            start_value: None,
            target: Target::Lazy(Arc::new(factory)),
            target_value: None,
            duration,
            elapsed: Duration::ZERO,
            easing: DEFAULT_EASING,
        }
    }

    pub fn follow(&self, path: &PathNode, duration: Duration) -> FollowPath<T>
    where
        T: FromVec2,
    {
        FollowPath {
            data: self.data.clone(),
            path_data: path.data.clone(),
            duration,
            elapsed: Duration::ZERO,
            easing: DEFAULT_EASING,
        }
    }

    pub fn bind<S: Tweenable + PartialEq, F>(
        &self,
        source: Signal<S>,
        mapper: F,
    ) -> crate::engine::nodes::video::BindingNode<T, S>
    where
        F: Fn(S) -> T + Send + Sync + 'static,
    {
        crate::engine::nodes::video::BindingNode::new(source, self.clone(), mapper)
    }
}

/// SignalTween now tracks its own elapsed time and start/target values.
pub struct SignalTween<T> {
    data: Arc<Mutex<SignalData<T>>>,
    start_value: Option<T>,
    target: Target<T>,
    target_value: Option<T>,
    duration: Duration,
    elapsed: Duration,
    easing: fn(f32) -> f32,
}

impl<T: Tweenable> SignalTween<T> {
    pub fn ease(mut self, easing: fn(f32) -> f32) -> Self {
        self.easing = easing;
        self
    }
}

impl<T: Tweenable> Animation for SignalTween<T> {
    fn update(&mut self, dt: Duration) -> (bool, Duration) {
        // Capture values on first update
        if self.start_value.is_none() {
            let current = self.data.lock().unwrap().value.clone();
            self.start_value = Some(current.clone());

            // Evaluate target if needed
            if self.target_value.is_none() {
                match &self.target {
                    Target::Fixed(v) => self.target_value = Some(v.clone()),
                    Target::Lazy(f) => self.target_value = Some(f(&current)),
                }
            }
        }

        let target = self.target_value.as_ref().unwrap();

        if self.duration == Duration::ZERO {
            let mut data = self.data.lock().unwrap();
            data.value = target.clone();
            return (true, dt);
        }

        self.elapsed += dt;
        let finished = self.elapsed >= self.duration;
        let leftover = if finished {
            self.elapsed - self.duration
        } else {
            Duration::ZERO
        };

        let t_linear = (self.elapsed.as_secs_f32() / self.duration.as_secs_f32()).min(1.0);
        let t_eased = (self.easing)(t_linear);

        let start = self.start_value.as_ref().unwrap();
        let mut data = self.data.lock().unwrap();
        data.value = T::interpolate(start, target, t_eased);

        (finished, leftover)
    }

    fn duration(&self) -> Duration {
        self.duration
    }

    fn set_easing(&mut self, easing: fn(f32) -> f32) {
        self.easing = easing;
    }

    fn reset(&mut self) {
        self.start_value = None;
        self.target_value = None;
        self.elapsed = Duration::ZERO;

        let mut data = self.data.lock().unwrap();
        data.value = data.initial.clone();
    }
}

pub struct FollowPath<T> {
    data: Arc<Mutex<SignalData<T>>>,
    path_data: Arc<PathData>,
    duration: Duration,
    elapsed: Duration,
    easing: fn(f32) -> f32,
}

impl<T: Send + Sync + 'static> FollowPath<T> {
    pub fn ease(mut self, easing: fn(f32) -> f32) -> Self {
        self.easing = easing;
        self
    }
}

impl<T: Tweenable + FromVec2> Animation for FollowPath<T> {
    fn update(&mut self, dt: Duration) -> (bool, Duration) {
        if self.duration == Duration::ZERO {
            let mut data = self.data.lock().unwrap();
            data.value = T::from_vec2(self.path_data.sample(1.0));
            return (true, dt);
        }

        self.elapsed += dt;
        let finished = self.elapsed >= self.duration;
        let leftover = if finished {
            self.elapsed - self.duration
        } else {
            Duration::ZERO
        };

        let t_linear = (self.elapsed.as_secs_f32() / self.duration.as_secs_f32()).min(1.0);
        let t_eased = (self.easing)(t_linear);

        let mut data = self.data.lock().unwrap();
        data.value = T::from_vec2(self.path_data.sample(t_eased));

        (finished, leftover)
    }

    fn duration(&self) -> Duration {
        self.duration
    }

    fn set_easing(&mut self, easing: fn(f32) -> f32) {
        self.easing = easing;
    }

    fn reset(&mut self) {
        self.elapsed = Duration::ZERO;
        let mut data = self.data.lock().unwrap();
        data.value = data.initial.clone();
    }
}

impl<T: Tweenable> From<SignalTween<T>> for Box<dyn Animation> {
    fn from(tween: SignalTween<T>) -> Self {
        Box::new(tween)
    }
}

impl<T: Tweenable + FromVec2> From<FollowPath<T>> for Box<dyn Animation> {
    fn from(anim: FollowPath<T>) -> Self {
        Box::new(anim)
    }
}