Getting Started
注意:
当前本章内容非常不完善,结构不清晰、内容不完整,目前建议结合 Example 和源码来了解。
在 Ranim 中,定义并渲染一段动画的代码基本长成下面这个样子:
#![allow(unused)] fn main() { #[scene] #[output] fn scene_name(r: &mut RanimScene) { let _r_cam = r.insert_and_show(CameraFrame::default()); let r_square = r.insert(square); r.timeline_mut(&r_square) .play_with(|square| square.fade_in()) .forward(0.5); // ... } }
形如 fn(&mut RanimScene)
的函数被称为场景函数,在其中可以通过 RanimScene
这个核心结构来对时间线进行操作以编码动画。
上面的例子中涉及了一些宏:
#[scene]
:为场景函数生成一个对应的static <scene_name>_scene: &'static Scene
,包含了场景名称、函数指针、场景设置、输出设置(通过#[output]
来设置)等信息。 可以配置一些属性:#[scene(name = "...")]
:为场景指定一个名称,默认与函数名相同。#[scene(frame_height = 8.0)]
:为场景指定一个默认的高度,默认值为 8.0。#[scene(clear_color = "#ffffffff")]
:为场景指定一个清除颜色,默认值为#333333ff
。#[output]
:为场景添加一个输出: 输出的文件名<output_name>
会被命名为<scene_name>_<width>x<height>_<frame_rate>
。#[output(dir = "...")]
:设置相对于./output
的输出目录,也可以是绝对路径#[output(pixel_size = (1920, 1080))]
:设置输出像素大小#[output(frame_rate = 60)]
:设置输出帧率#[output(save_frames = true)]
:设置是否保存每一帧(保存在<dir>/<output_name>-frames/
下)
#[wasm_demo_doc]
:为场景指定一个文档字符串,默认值为空字符串。
使用 ranim-cli 可以方便的对场景进行预览、渲染:
如果想要使用 ranim-cli,需要为 crate-type
添加 dylib
。
-
ranim preview
:调用 Cargo 构建指定的 target,然后启动一个预览应用加载编译出的 dylib,并监听改动进行重载。ranim preview # 预览根 package 的 lib target ranim preview -p package_name # 预览 package_name 包的 lib target ranim preview -p package_name --example example_name # 预览 package_name 包的 example_name 示例
-
ranim render
:调用 Cargo 构建指定的 target,然后启动一个渲染应用加载编译出的 dylib,并渲染动画。ranim render # 渲染根 package 的全部场景的所有输出 ranim render scene_name # 渲染根 package 中名称为 scene_name 的场景的所有输出 ranim render -p package_name # 渲染 package_name 包的全部场景的所有输出 ranim render -p package_name --example example_name # 渲染 package_name 包的 example_name 示例中的全部场景的所有输出
此外,ranim
还提供了一些 api 来直接渲染或预览场景。
render_scene(hello_ranim_scene);
preview_scene(hello_ranim_scene); // 需要 `app` feature
1. 场景的构造
任何实现了 SceneConstructor
Trait 的类型都可以被用于构造场景:
ranim 自动为 F: Fn(&mut RanimScene) + Send + Sync
实现了该 Trait。
也就是说,对于要求 impl SceneConstructor
的参数:
- 既可以传入函数指针
fn(&mut RanimScene)
- 也可以传入一个闭包
|r: &mut RanimScene| { /*...*/ }
。
整个构造过程围绕着 &mut RanimScene
,它是 ranim 中编码动画 api 的主入口。
2. 时间线
每一个被插入时间线的物件都有一个唯一的 ItemId
,同时也有一条对应的时间线。
时间线是一种用于编码物件动画的结构,它的内部有一个存储了动画以及展示时间的列表,以及用于编码静态动画的物件状态。
编码动画的过程本质上是在向时间线中插入动态或静态的动画:
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 ranim::{
anims::fading::FadingAnim, color::palettes::manim, items::vitem::geometry::Square, prelude::*,
};
#[scene]
#[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());
}
}
2.3 转换时间线类型
在对一个物件进行动画编码的过程中有时会涉及物件类型的转换,比如一个 Square
物件需要被转换为更低级的 VItem
才能够被应用 Write 和 UnWrite 动画,
此时就需要对时间线类型进行转换:
use ranim::{
anims::{creation::WritingAnim, transform::TransformAnim},
color::palettes::manim,
items::vitem::{
VItem,
geometry::{Circle, Square},
},
prelude::*,
};
#[scene]
#[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());
}
}
Ranim Cli
使用 Ranim Cli 可以更方便地进行场景地预览、输出属性等的定义:
#[scene]
#[output]
pub fn scene_constructor1(r: &mut RanimScene) {
// ...
}
#[scene(frame_height = 8.0, name = "custom")]
#[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
:
{{#include ../../../../src/animation.rs:EvalDynamic}}
它接受自身的不可变引用和一个进度值作为输入,经过计算,输出一个自身类型的结果。
以 Transform
动画为例,其内部包含了物件初始状态和目标状态,以及用于插值的对齐后的初始和目标状态,在 EvalDynamic<T>
的实现中使用内部的数据进行计算求值得到结果:
{{#include ../../../../src/animation/transform.rs:Transform}}
{{#include ../../../../src/animation/transform.rs:Transform-EvalDynamic}}
AnimationSpan
有了以进度 为输入标准化的动画函数后,加上持续秒数 、速率函数 ,就可以构造一个以秒 为输入的动画函数 :
在 Ranim 中,这对应着 AnimationSpan
结构:
{{#include ../../../../src/animation.rs:AnimationSpan}}
{{#include ../../../../src/animation.rs:AnimationSpan-eval}}
其中的 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
是第一层,它的本质就是一个动画的容器:
{{#include ../../../../src/timeline.rs:ItemTimeline}}
在编写动画时的一系列操作(如 forward
、play
等)最后都会转变为对 ItemTimeline
内部属性的操作,
最终达成的结果就是在其 anims
属性中完成此条时间线所有动画以及其起止时间的编码(即“把动画在时间上放到正确的位置”)。
DynTimeline
DynTimeline
是第二层,用于对 ItemTimeline
进行类型擦除:
{{#include ../../../../src/timeline.rs:DynTimeline}}
在场景中,我们会有多个物件,每个物件都有自己的时间线,为了能够遍历时间线进行求值等操作,必须要对时间线进行类型擦除,从而将不同物件的时间线放到一个容器中。
AnyTimelineFunc
就是 Any + TimelineFunc
,带有基础的时间线操作,而 AnyVisualItemTimelineTrait
就是 Any + VisualItemTimelineTrait
,在 TimelineFunc
的基础上额外多了 eval_sec
方法:
{{#include ../../../../src/timeline.rs:VisualItemTimelineTrait}}
这样,通过 TimelineId<T>
可以获取对应的 Timeline 并还原类型,而在求值、渲染时没有类型信息,直接使用 Trait 提供的方法进行求值。
ItemDynTimelines
其实到 DynTimeline
已经足够了,但是为了支持“变更同一条 Timeline 的类型”,在 DynTimeline
的基础上又包了第三层:
{{#include ../../../../src/timeline.rs:ItemDynTimelines}}
简单来说,在实际操作的时候,永远操作的是最后一个 DynTimeline
,这使得其表现得像是一个 DynTimeline
,不过额外有一个 apply_map
方法,可以使用一个 map_fn: impl FnOnce(T) -> E
来使用最后一个 DynTimeline
的内部状态进行转换,然后再插入一个新的 DynTimeline
。