Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Getting Started

注意:

当前本章内容非常不完善,结构不清晰、内容不完整,目前建议结合 Example 和源码来了解。

在 Ranim 中,定义并渲染一段动画的代码基本长成下面这个样子:

use ranim::prelude::*;

#[output(dir = "hello_ranim")]
fn hello_ranim(r: &mut RanimScene) {
        // ...
    }
}

fn main() {
    render_scene_output(
        hello_ranim,
        "hello_ranim".to_string(),
        &SceneConfig::default(),
        &Output::default()
    );
}

render_scene 函数接收一个 impl SceneConstructor 并使用它对动画进行构造、求值、渲染,并将渲染结果按照传入的 Output 定义输出为视频。

默认的输出路径为 ./output/<scene_name>_<width>x<height>_<fps>.mp4

此外,当启用了 app feature 时,可以使用 run_scene_app 来启动一个能够可视化时间线并拖动进度条预览画面的应用:

run_scene_app(hello_ranim, "hello_ranim".to_string());

1. 场景的构造

任何实现了 SceneConstructor Trait 的类型都可以被用于构造场景:

/// A scene constructor
///
/// It can be a simple fn pointer of `fn(&mut RanimScene)`,
/// or any type implements `Fn(&mut RanimScene) + Send + Sync`.
pub trait SceneConstructor: Send + Sync {
    /// The construct logic
    fn construct(&self, r: &mut RanimScene);

    /// Use the constructor to build a [`SealedRanimScene`]
    fn build_scene(&self) -> SealedRanimScene {
        let mut scene = RanimScene::new();
        self.construct(&mut scene);
        scene.seal()
    }
}

ranim 自动为 F: Fn(&mut RanimScene) + Send + Sync 实现了该 Trait。

也就是说,对于要求 impl SceneConstructor 的参数:

  • 既可以传入函数指针 fn(&mut RanimScene)
  • 也可以传入一个闭包 |r: &mut RanimScene| { /*...*/ }

整个构造过程围绕着 &mut RanimScene,它是 ranim 中编码动画 api 的主入口。

2. 时间线

每一个被插入时间线的物件都有一个唯一的 ItemId,同时也有一条对应的时间线。

时间线是一种用于编码物件动画的结构,它的内部有一个存储了动画以及展示时间的列表,以及用于编码静态动画的物件状态。

编码动画的过程本质上是在向时间线中插入动态或静态的动画:

Timeline

2.1 插入物件(创建时间线)

通过 r.insert(state) 可以插入一个物件并为其创建一条时间线:

let square = Square::new(2.0).with(|x| {
    x.set_color(manim::BLUE_C);
});
let circle = Circle::new(1.0).with(|x| {
    x.set_color(manim::RED_C);
});

let r_square1 = r.insert(square.clone()); // 类型为 `ItemId<Square>`
let r_square2 = r.insert(square); // 类型为 `ItemId<Square>`
let r_circle = r.insert(circle); // 类型为 `ItemId<Circle>`

2.1 访问时间线

时间线在被创建之后,需要通过 r.timeline(&index)r.timeline_mut(&index) 来访问:

{
    // 类型为 `&ItemTimeline<Square>`
    let square_timeline_ref = r.timeline(&r_square1);
}
{
    // 类型为 `&ItemTimeline<Circle>`
    let circle_timeline_ref = r.timeline(&r_circle);
}

除了通过单一的 &ItemId 来访问单一的时间线,也可以通过 &[&ItemId<T>; N] 来访问多条时间线:

// 类型为 `[&mut ItemTimeline<Square>]`
let [sq1_timeline_ref, sq2_timeline_ref] = r.timeline_ref(&[&r_square1, &r_square2]);

同时也可以访问全部时间线的切片的不可变/可变引用,不过元素是类型擦除后的 ItemDynTimelines

// 类型为 &[ItemDynTimelines]
let timelines = r.timelines();
// 类型为 &mut [ItemDynTimelines]
let timelines = r.timelines_mut();

2.2 操作时间线

ItemTimeline<T>ItemDynTimelines 都具有一些用于编码动画的操作方法:

方法ItemTimeline<T>ItemDynTimelines描述
show / hide显示/隐藏时间线中的物体
forward / forward_to推进时间线
play / play_with向时间线中插入动画
update / update_with更新时间线中物体状态
state获取时间线中物体状态

有关方法的具体详细定义可以参考 API 文档。

下面的例子使用一个 Square 物件创建了一个时间线,然后编码了淡入 1 秒、显示 0.5 秒、消失 0.5 秒、显示 0.5 秒、淡出 1 秒的动画:

use log::LevelFilter;
use ranim::{
    animation::fading::FadingAnim, color::palettes::manim, items::vitem::geometry::Square,
    prelude::*,
};

#[scene]
#[preview]
#[output(dir = "getting_started0")]
fn getting_started0(r: &mut RanimScene) {
    let _r_cam = r.insert_and_show(CameraFrame::default());
    // A Square with size 2.0 and color blue
    let square = Square::new(2.0).with(|square| {
        square.set_color(manim::BLUE_C);
    });

    let r_square = r.insert(square);
    {
        let timeline = r.timeline_mut(&r_square);
        timeline
            .play_with(|square| square.fade_in())
            .forward(1.0)
            .hide()
            .forward(1.0)
            .show()
            .forward(1.0)
            .play_with(|square| square.fade_out());
    }
}

fn main() {
    #[cfg(not(target_arch = "wasm32"))]
    {
        #[cfg(debug_assertions)]
        pretty_env_logger::formatted_timed_builder()
            .filter(Some("ranim"), LevelFilter::Trace)
            .init();
        #[cfg(not(debug_assertions))]
        pretty_env_logger::formatted_timed_builder()
            .filter(Some("ranim"), LevelFilter::Info)
            .init();
    }

    #[cfg(feature = "app")]
    preview(getting_started0_scene);
    #[cfg(not(feature = "app"))]
    render_scene(getting_started0_scene);
}

2.3 转换时间线类型

在对一个物件进行动画编码的过程中有时会涉及物件类型的转换,比如一个 Square 物件需要被转换为更低级的 VItem 才能够被应用 Write 和 UnWrite 动画, 此时就需要对时间线类型进行转换:

use log::LevelFilter;
use ranim::{
    animation::{creation::WritingAnim, transform::TransformAnim},
    color::palettes::manim,
    items::vitem::{
        VItem,
        geometry::{Circle, Square},
    },
    prelude::*,
};

#[scene]
#[preview]
#[output(dir = "getting_started1")]
fn getting_started1(r: &mut RanimScene) {
    let _r_cam = r.insert_and_show(CameraFrame::default());
    // A Square with size 2.0 and color blue
    let square = Square::new(2.0).with(|square| {
        square.set_color(manim::BLUE_C);
    });

    let circle = Circle::new(2.0).with(|circle| {
        circle.set_color(manim::RED_C);
    });

    // In order to do more low-level opeerations,
    // sometimes we need to convert the item to a low-level item.
    let r_vitem = r.insert(VItem::from(square));
    {
        let timeline = r.timeline_mut(&r_vitem);
        timeline.play_with(|vitem| vitem.transform_to(VItem::from(circle.clone())));
        timeline.play_with(|vitem| vitem.unwrite());
    }
}

fn main() {
    #[cfg(not(target_arch = "wasm32"))]
    {
        #[cfg(debug_assertions)]
        pretty_env_logger::formatted_timed_builder()
            .filter(Some("ranim"), LevelFilter::Trace)
            .init();
        #[cfg(not(debug_assertions))]
        pretty_env_logger::formatted_timed_builder()
            .filter(Some("ranim"), LevelFilter::Info)
            .init();
    }

    #[cfg(feature = "app")]
    preview(getting_started1_scene);
    #[cfg(not(feature = "app"))]
    render_scene(getting_started1_scene);
}

Ranim Cli

使用 Ranim Cli 可以更方便地进行场景地预览、输出属性等的定义:

#[scene]
#[preview]
#[output]
pub fn scene_constructor1(r: &mut RanimScene) {
    // ...
}

#[scene(frame_height = 8.0, name = "custom")]
#[preview]
#[output(width = 1920, height = 1080, frame_rate = 60, save_frames = false, dir = "output")]
pub fn scene_constructor2(r: &mut RanimScene) {
    // ...
}

同时,不必再编写 main.rs 来手动调用渲染或预览 api,直接通过 cli 命令即可完成场景的预览或渲染(而且预览支持热重载):

  • ranim preview:调用 Cargo 构建指定的 lib,然后启动一个预览应用加载编译出的 dylib,并监听改动进行重载。
  • ranim render:调用 Cargo 构建指定的 lib,然后加载它并渲染动画。

但是,要注意为你的 lib 添加 crate-type = ["dylib"] 来使得它能被编译为动态库。

核心概念

动画

本节将对 Ranim 中 动画 的实现思路进行讲解。

EvalDynamic<T> Trait

一个标准化的动画其实本质上就是一个函数 ,它的输入是一个进度值 ,输出是该动画在对应进度处的结果

这个函数 不仅定义了动画的 求值,同时其内部也包含了求值所需要的 信息。对应到计算机世界,其实也就是 算法数据,而对应到编程语言上也就是 方法数据类型

在由 Rust 实现的 Ranim 中也就是 EvalDynamic<T> Trait 和实现了它的类型 T

/// This is the core of any animation, an animation is basically a function on time.
///
/// This represents a normalized animation function for type `T`, which accepts
/// a progress value `alpha` in range [0, 1] and returns the evaluation result in type `T`.
pub trait EvalDynamic<T> {
    /// Evaluates at the given progress value `alpha` in range [0, 1].
    fn eval_alpha(&self, alpha: f64) -> T;
}

它接受自身的不可变引用和一个进度值作为输入,经过计算,输出一个自身类型的结果。

Transform 动画为例,其内部包含了物件初始状态和目标状态,以及用于插值的对齐后的初始和目标状态,在 EvalDynamic<T> 的实现中使用内部的数据进行计算求值得到结果:

/// Transform Anim
pub struct Transform<T: TransformRequirement> {
    src: T,
    dst: T,
    aligned_src: T,
    aligned_dst: T,
}

impl<T: TransformRequirement> EvalDynamic<T> for Transform<T> {
    fn eval_alpha(&self, alpha: f64) -> T {
        if alpha == 0.0 {
            self.src.clone()
        } else if 0.0 < alpha && alpha < 1.0 {
            self.aligned_src.lerp(&self.aligned_dst, alpha)
        } else if alpha == 1.0 {
            self.dst.clone()
        } else {
            unreachable!()
        }
    }
}

AnimationSpan

有了以进度 为输入标准化的动画函数后,加上持续秒数 、速率函数 ,就可以构造一个以秒 为输入的动画函数

在 Ranim 中,这对应着 AnimationSpan 结构:

/// An [`AnimationSpan<T>`] consist of an [`Evaluator<T>`] and some metadata,
/// such as `rate_func` and `duration_secs`, to control the evaluation process.
pub struct AnimationSpan<T> {
    pub(crate) evaluator: Evaluator<T>,
    /// The rate function used for evaluating
    pub rate_func: fn(f64) -> f64,
    /// The duration seconds
    pub duration_secs: f64,
}

impl<T> AnimationSpan<T> {
    /// Evaluate at the given progress value `alpha` in [0, 1]
    pub fn eval_alpha(&self, alpha: f64) -> EvalResult<T> {
        self.eval_sec(alpha * self.duration_secs)
    }
    /// Evaluate at the given second `sec`
    pub fn eval_sec(&self, sec: f64) -> EvalResult<T> {
        self.evaluator
            .eval_alpha((self.rate_func)((sec / self.duration_secs).clamp(0.0, 1.0)))
    }
}

其中的 evaluator: Evaluator<T> 其实就是对 Box<dyn EvalDynamic<T>> 的封装。

时间线

简单来说,时间线的本质是动画的容器,将若干动画以及其起止时间信息打包在一起也就得到了一条时间线。 一条时间线对应一个物件的全部动画,若干条时间线组合在一起即表示了整个场景的完整动画。

不过因为涉及泛型以及类型擦除,Ranim 的时间线封装并非简单的一层,而是很多层:

classDiagram
    class ItemDynTimelines {
        +timelines: Vec~ItemDynTimelines~
    }

    ItemDynTimelines --* DynTimeline

    class DynTimeline {
        +CameraFrame(Box~dyn AnyTimelineFunc~)
        +VisualItem(Box~dyn AnyVisualItemTimelineTrait~)
    }
    <<Enumeration>> DynTimeline

    DynTimeline ..* VisualItemTimelineTrait
    DynTimeline ..* TimelineFunc

    class ItemTimeline~T~

    ItemTimeline --|> TimelineFunc
    ItemTimeline ..|> VisualItemTimelineTrait : Where T is VisualItem

    class TimelineFunc 
    <<Trait>> TimelineFunc

    VisualItemTimelineTrait --|> TimelineFunc

    class VisualItemTimelineTrait
    <<Trait>> VisualItemTimelineTrait

ItemTimeline<T>

ItemTimeline 是第一层,它的本质就是一个动画的容器:

/// `ItemTimeline<T>` is used to encode animations for a single type `T`,
/// it contains a list of [`AnimationSpan<T>`] and the corresponding metadata for each span.
pub struct ItemTimeline<T> {
    type_name: String,
    anims: Vec<(AnimationSpan<T>, std::ops::Range<f64>)>,

    // Followings are states use while constructing
    cur_sec: f64,
    /// The state used for static anim.
    state: T,
    /// The start time of the planning static anim.
    /// When it is true, it means that it is showing.
    planning_static_start_sec: Option<f64>,
}

在编写动画时的一系列操作(如 forwardplay 等)最后都会转变为对 ItemTimeline 内部属性的操作, 最终达成的结果就是在其 anims 属性中完成此条时间线所有动画以及其起止时间的编码(即“把动画在时间上放到正确的位置”)。

DynTimeline

DynTimeline 是第二层,用于对 ItemTimeline 进行类型擦除:

/// A type erased [`ItemTimeline<T>`]
///
/// Currently There are two types of Timeline:
/// - [`DynTimeline::CameraFrame`]: Can be created from [`CameraFrame`], has a boxed [`AnyTimelineFunc`] in it.
/// - [`DynTimeline::VisualItem`]: Can be created from [`VisualItem`], has a boxed [`AnyVisualItemTimelineTrait`] in it.
pub enum DynTimeline {
    /// A type erased timeline for [`CameraFrame`], its inner is a boxed [`AnyTimelineFunc`].
    CameraFrame(Box<dyn AnyTimelineFunc>),
    /// A type erased timeline for [`VisualItem`], its inner is a boxed [`AnyVisualItemTimelineTrait`].
    VisualItem(Box<dyn AnyVisualItemTimelineTrait>),
}

在场景中,我们会有多个物件,每个物件都有自己的时间线,为了能够遍历时间线进行求值等操作,必须要对时间线进行类型擦除,从而将不同物件的时间线放到一个容器中。

AnyTimelineFunc 就是 Any + TimelineFunc,带有基础的时间线操作,而 AnyVisualItemTimelineTrait 就是 Any + VisualItemTimelineTrait,在 TimelineFunc 的基础上额外多了 eval_sec 方法:

/// A visual item timeline, which can eval to `EvalResult<Box<dyn VisualItem>>`.
///
/// This is auto implemented for `ItemTimeline<T>` where `T: Clone + VisualItem + 'static`
pub trait VisualItemTimelineTrait: TimelineFunc {
    /// Evaluate the timeline at `target_sec`
    fn eval_sec(&self, target_sec: f64) -> Option<(EvalResult<Box<dyn VisualItem>>, usize)>;
}

这样,通过 TimelineId<T> 可以获取对应的 Timeline 并还原类型,而在求值、渲染时没有类型信息,直接使用 Trait 提供的方法进行求值。

ItemDynTimelines

其实到 DynTimeline 已经足够了,但是为了支持“变更同一条 Timeline 的类型”,在 DynTimeline 的基础上又包了第三层:

/// A item timeline which contains multiple [`DynTimeline`], so
/// that it can contains multiple [`ItemTimeline<T>`] in different type of `T`.
pub struct ItemDynTimelines {
    id: usize,
    timelines: Vec<DynTimeline>,
}

简单来说,在实际操作的时候,永远操作的是最后一个 DynTimeline,这使得其表现得像是一个 DynTimeline,不过额外有一个 apply_map 方法,可以使用一个 map_fn: impl FnOnce(T) -> E 来使用最后一个 DynTimeline 的内部状态进行转换,然后再插入一个新的 DynTimeline