motion-canvas-rs 0.2.4

A high-performance vector animation engine inspired by Motion Canvas, built on Vello and Typst.
Documentation
use crate::core::animation::base::{Animation, AudioEvent};
use crate::core::animation::AnyAnimation;
use std::time::Duration;

const INFINITE_DURATION_CAP: Duration = Duration::from_secs(3600); // 1 hour cap for infinite loops

/// An animation that repeats another animation multiple times (or infinitely).
///
/// `LoopAnim` takes a factory function that produces an [`Animation`].
/// Every time the current animation finishes, the factory is called to
/// create a fresh instance for the next iteration.
pub struct LoopAnim {
    pub(crate) factory: Box<dyn Fn() -> AnyAnimation + Send + Sync>,
    pub(crate) current: Box<AnyAnimation>,
    pub(crate) repeat_count: Option<usize>,
    pub(crate) finished_count: usize,
}

impl LoopAnim {
    /// Creates a new `LoopAnim` with the provided factory and iteration count.
    ///
    /// If `count` is `None`, the animation loops infinitely (capped at 1 hour
    /// for timeline duration calculations).
    pub fn new(factory: Box<dyn Fn() -> AnyAnimation + Send + Sync>, count: Option<usize>) -> Self {
        let current = Box::new(factory());
        Self {
            factory,
            current,
            repeat_count: count,
            finished_count: 0,
        }
    }
}

impl Animation for LoopAnim {
    /// Updates the current animation instance.
    /// If it finishes and more iterations remain, it instantiates a new
    /// animation using the factory.
    fn update(&mut self, mut dt: Duration) -> (bool, Duration) {
        loop {
            let (finished, leftover) = self.current.update(dt);
            if !finished {
                return (false, Duration::ZERO);
            }

            self.finished_count += 1;

            if let Some(max) = self.repeat_count {
                if self.finished_count >= max {
                    return (true, leftover);
                }
            }

            self.current = Box::new((self.factory)());
            dt = leftover;
            if dt == Duration::ZERO {
                return (false, Duration::ZERO);
            }
        }
    }

    /// The total duration is (inner duration * count).
    /// Returns 1 hour for infinite loops.
    fn duration(&self) -> Duration {
        match self.repeat_count {
            Some(count) => self.current.duration() * count as u32,
            None => INFINITE_DURATION_CAP,
        }
    }

    /// Collects audio events only from the currently active child animation with loop offset.
    fn collect_audio_events(&mut self, current_time: Duration, events: &mut Vec<AudioEvent>) {
        let loop_offset = self.current.duration() * self.finished_count as u32;
        self.current
            .collect_audio_events(current_time + loop_offset, events);
    }

    /// Resets the loop counter and recreates the initial animation.
    fn reset(&mut self) {
        self.current = Box::new((self.factory)());
        self.finished_count = 0;
    }
}

/// Creates an animation that repeats an animation generated by a factory.
///
/// Generally used via the [`loop_anim!`](crate::loop_anim) macro.
///
/// ### Example
/// ```rust
/// # use motion_canvas_rs::prelude::*;
/// # use std::time::Duration;
/// # let node = Rect::default()
/// #    .with_size(Vec2::new(100.0, 100.0))
/// #    .with_fill(Color::RED);
/// # let target = Vec2::new(100.0, 100.0);
/// # let dur = Duration::from_secs(1);
/// // Repeat 5 times
/// loop_anim!(node.position.to(target.clone(), dur), Some(5));
///
/// # let node_2 = Rect::default()
/// #    .with_size(Vec2::new(100.0, 100.0))
/// #    .with_fill(Color::RED);
/// // Repeat infinitely
/// loop_anim!(node_2.position.to(target, dur), None);
/// ```
pub fn loop_anim<F>(factory: F, count: Option<usize>) -> AnyAnimation
where
    F: Fn() -> AnyAnimation + Send + Sync + 'static,
{
    AnyAnimation::Loop(LoopAnim::new(Box::new(factory), count))
}