Ranim 是一个使用 Rust 编写的程序化动画引擎, 受 3b1b/manimjkjkil4/JAnim 启发

  • 矢量图形基于二阶贝塞尔曲线表示,使用 SDF 渲染
  • 使用 wgpu,兼容多种后端图形 API

注:目前 ranim 的接口仍并不稳定,而本页面的说明文字可能更新的不是特别勤(但是带视频的部分即 example 是会同步更新的)

Getting Started

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

use ranim::prelude::*;

#[scene]
struct HelloWorldScene;

impl TimelineConstructor for HelloWorldScene {
    fn construct<'t: 'r, 'r>(
        self,
        timeline: &'t RanimTimeline,
        camera: &'r mut Rabject<'t, CameraFrame>,
    ) {
        // ...
    }
}

fn main() {
    render_scene(HelloWorldScene, &AppOptions::default());
}

HelloWorldScene 是一个 Scene,即下面两个 Trait 的组合:

  • SceneMetaTrait 实现了 fn meta(&self) -> SceneMeta 方法。

    使用 #[scene] 会以结构体的 snake_case 命名(去掉 Scene 后缀)作为 SceneMetaname 字段自动实现这个 Trait。

    也可以通过 #[scene(name = "<NAME>")] 来手动命名。

  • SceneConstructor 则是定义了动画的构造过程。

使用 render_scene 可以用一个 Scene 来构造一个 RanimTimeline 并对其进行渲染,渲染结果将被输出到 <output_dir>/<scene_name>/ 目录下。

construct 方法有两个关键的参数:

  • timeline: &'t RanimTimeline:Ranim API 的主要入口,几乎全部对动画的编码操作都发生在这个结构上
  • camera: &'r Rabject<'t, CameraFrame>:默认的相机 Rabject,也是 RanimTimeline 中被插入的第一个 Rabject

RanimTimelineRabject 这两个类型非常重要,将贯穿整个 Ranim 动画的编码。

1. RanimTimeline 和 Rabject

Ranim 使用一个 RanimTimeline 结构来编码动画,首先介绍两个最基本的操作:

  • 使用 timeline.forward(duration_secs) 来使时间线推进一段时间
  • 使用 timeline.insert(item) 来将一个 item: T 插入时间线,返回一个 Rabject<T>

Rabject<T> 的结构很简单,如下:

pub struct Rabject<'a, T> {
    pub timeline: &'a RanimTimeline,
    pub id: usize,
    pub data: T,
}

当某个物件 T 被插入 RanimTimeline 中时,会被赋予一个 Id,以 Rabject<T> 的形式返回,同时在 RanimTimeline 内部会以 T 的值为初始状态创建一条 RabjectTimeline

使用 timeline.show(&rabject)timeline.hide(&rabject) 可以控制接下来 forward 时的表现。

当一个 Rabjectdrop 时,它会被 hide 掉:

impl<T> Drop for Rabject<'_, T> {
    fn drop(&mut self) {
        self.timeline.hide(self);
    }
}

下面的例子使用一个 VItem 物件和 timeline.insert 在时间线中创建了一个 Rabject<VItem> 并展示了 showhide 以及 drop 对其影响:

1use ranim::{color::palettes::manim, items::vitem::Square, prelude::*};
2
3#[scene]
4struct GettingStarted0Scene;
5
6impl TimelineConstructor for GettingStarted0Scene {
7 fn construct<'t: 'r, 'r>(
8 self,
9 timeline: &'t RanimTimeline,
10 _camera: &'r mut Rabject<'t, CameraFrame>,
11 ) {
12 let mut square = Square(2.0).build(); // An VItem of a square
13 square.set_color(manim::BLUE_C);
14
15 timeline.forward(0.5);
16 let square = timeline.insert(square); // Create a "Rabject" in the timeline
17 timeline.forward(0.5); // By default the rabject timeline is at "show" state
18 timeline.hide(&square);
19 timeline.forward(0.5); // After called "hide", the forward will encode blank into timeline
20
21 timeline.show(&square);
22 timeline.forward(0.5);
23
24 drop(square); // The drop is equal to `timeline.hide(&rabject)`
25 timeline.forward(0.5);
26 }
27}
28
29fn main() {
30 render_scene(GettingStarted0Scene, &AppOptions::default());
31}
32

2. 播放动画

Ranim 中的每一个动画都会为实现了对应 Trait 的物件添加对应的创建方法。

比如对于 FadingAnim,凡是实现了 Opacity + Interpolatable Trait 的物件都会拥有 fade_infade_out 方法。

对一个 Rabject<T> 调用创建动画的方法会返回一个 AnimSchedule<T>,将它传入 timeline.play(anim_schedule) 即可将这段动画编码在 RanimTimeline 中。

let mut square = timeline.insert(square);
timeline.play(square.fade_in());
timeline.play(square.fade_out());

上面的动画也可以这样写:

let mut square = timeline.insert(square);
timeline.play(square.fade_in().chain(|data| data.fade_out()));

AnimSchedule<T>chain 方法,接受一个 impl FnOnce(T) -> Animation<T>,会将两个动画拼接在一起。

T&'r mut Rabject<'t, T> 相同,也有创建动画的方法,不过返回的是 Animation<T>

1use ranim::{
2 AppOptions, animation::fading::FadingAnimSchedule, color::palettes::manim,
3 items::vitem::Square, prelude::*, render_scene,
4};
5
6#[scene]
7struct GettingStarted1Scene;
8
9impl TimelineConstructor for GettingStarted1Scene {
10 fn construct<'t: 'r, 'r>(
11 self,
12 timeline: &'t RanimTimeline,
13 _camera: &'r mut Rabject<'t, CameraFrame>,
14 ) {
15 let mut square = Square(2.0).build();
16 square.set_color(manim::BLUE_C);
17
18 let mut square = timeline.insert(square);
19 #[allow(deprecated)]
20 timeline.play(square.fade_in());
21 timeline.play(square.fade_out());
22 }
23}
24
25fn main() {
26 render_scene(GettingStarted1Scene, &AppOptions::default());
27}
28

3. 动画参数

AnimSchedule<T>Animation<T> 都具有一些控制动画属性的参数,可以通过链式调用的方式来设置:

  • with_duration(duration_secs):设置动画持续时间
  • with_rate_func(rate_func):设置动画速率函数

此外在这个例子中你会发现,在播放了 transform_to(circle) 之后,再播放 fade_out 时,播放的并不是圆形的淡出,而是方形。

这并不是一个 Bug,而是一种刻意的设计,请继续向下阅读 4. 向 Rabject 应用动画变更,了解更多。

1use ranim::{
2 animation::{fading::FadingAnimSchedule, transform::TransformAnimSchedule},
3 color::palettes::manim,
4 items::vitem::{Circle, Square},
5 prelude::*,
6 utils::rate_functions::linear,
7};
8
9#[scene]
10struct GettingStarted2Scene;
11
12impl TimelineConstructor for GettingStarted2Scene {
13 fn construct<'t: 'r, 'r>(
14 self,
15 timeline: &'t RanimTimeline,
16 _camera: &'r mut Rabject<'t, CameraFrame>,
17 ) {
18 let mut square = Square(2.0).build();
19 square.set_color(manim::BLUE_C);
20
21 let mut square = timeline.insert(square);
22 let mut circle = Circle(2.0).build();
23 circle.set_color(manim::RED_C);
24
25 timeline.play(
26 square
27 .transform_to(circle)
28 .with_duration(2.0)
29 .with_rate_func(linear),
30 ); // Anim Schedule won't change the data in Rabject
31 timeline.forward(1.0);
32 timeline.play(square.fade_out()); // Anim is created based on the data in Rabject
33 }
34}
35
36fn main() {
37 render_scene(GettingStarted2Scene, &AppOptions::default());
38}
39

4. 向 Rabject 应用动画变更(AnimSchedule 与 apply)

使用 Rabject 创建动画时是基于 Rabject 当前的内部数据来创建的,创建与播放动画并不会修改其内部数据。 如果想要一个动画的效果实际应用到 Rabject 中,那么需要对 AnimSchedule 使用 apply 方法。

这样的好处是对于一些对数据有 损坏性变更 的动画(比如 unwrite 等),我们不需要提前对数据进行备份。

1use ranim::{
2 animation::{
3 creation::WritingAnimSchedule, fading::FadingAnimSchedule, transform::TransformAnimSchedule,
4 },
5 color::palettes::manim,
6 items::vitem::{Circle, Square},
7 prelude::*,
8};
9
10#[scene]
11struct HelloRanimScene;
12
13impl TimelineConstructor for HelloRanimScene {
14 fn construct<'t: 'r, 'r>(
15 self,
16 timeline: &'t RanimTimeline,
17 _camera: &'r mut Rabject<'t, CameraFrame>,
18 ) {
19 let mut square = Square(2.0).build();
20 square.set_color(manim::BLUE_C);
21
22 let mut square = timeline.insert(square);
23 let mut circle = Circle(2.0).build();
24 circle.set_color(manim::RED_C);
25
26 timeline.play(square.transform_to(circle).apply()); // Use `apply` on an anim schedule to update rabject data
27 timeline.play(square.unwrite()); // Do not use `apply` to keep the data in Rabject not changed
28 timeline.play(square.write());
29 timeline.play(square.fade_out());
30 }
31}
32
33fn main() {
34 #[cfg(feature = "app")]
35 run_scene_app(HelloRanimScene);
36 #[cfg(not(feature = "app"))]
37 {
38 render_scene(HelloRanimScene, &AppOptions::default());
39 render_scene_at_sec(HelloRanimScene, 0.0, "preview.png", &AppOptions::default());
40 }
41}
42

不过 chain 是会以第一个动画的结束状态为基础创建下一个动画的,但是要注意此时的 AnimSchedule 是整个被拼接后的动画,如果不调用 apply 是不会更新 Rabject 内部的数据的,而调用 apply 会应用整个被拼接后的动画的变更:

// <-- Rabject's data is a square
timeline.play(
    square
        .transform_to(circle)
        .chain(|data| data.unwrite())
);
// <-- Rabject's data is still a square
timeline.play(square.write()); // This plays a square's unwrite, but not circle's
// <-- Rabject's data is a square
timeline.play(
    square
        .transform_to(circle)
        .chain(|data| data.unwrite())
        .apply(), // <-- Rabject's data is an unwrote circle now
);
timeline.play(square.write()); // This plays nothing, because after the apply, the data is empty(unwrote circle)

简单来说 AnimSchedule 的作用就是将具有紧密关系的动画组合在一起,通过 apply 会应用整个动画(类似 Transaction 的感觉)。