Getting Started
Caution
本章内容已过时,请结合 Example 和源码来了解
Warning
注意:
当前本章内容非常不完善,结构不清晰、内容不完整,目前建议结合 Example 和源码来了解。
在 Ranim 中,定义并渲染/预览一段动画的代码基本长成下面这个样子:
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 main() {
// Scene and output defination
let scene = Scene {
name: "scene_name".to_string(),
constructor: scene_name,
config: SceneConfig {
clear_color: "#000000" // Or any css color
},
outputs: vec![
Output {
/// The width of the output texture in pixels.
width: 1280,
height: 720,
fps: 30,
format: OutputFormat::Mp4,
..Default::default()
}
],
};
// Render a scene, the seccond argument is the number of frame buffers used during rendering.
ranim::cmd::render_scene(&scene, 2);
// Needs "preview" feature
ranim::cmd::preview_scene(&scene);
}
形如 fn(&mut RanimScene) 的函数被称为场景函数,在其中可以通过 RanimScene 这个核心结构来对时间线进行操作以编码动画。
此外,ranim 提供了一些过程宏来简化 Scene 结构的定义:
#[scene(clear_color = "#000000")]
#[output(width = 1280, height = 720, fps = 30, format = "mp4")]
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 main() {
// Render a scene, use 2 frame buffers
ranim::cmd::render_scene!(scene_name);
// Needs "preview" feature
ranim::cmd::preview_scene!(scene_name);
}
会在一个同名模块内定义一个同名函数返回对应的 Scene,宏的内部是这样的:
mod scene;
mod utils;
use proc_macro::TokenStream;
use proc_macro_crate::{FoundCrate, crate_name};
use proc_macro2::Span;
use quote::quote;
use syn::{Data, DeriveInput, Fields, Ident, ItemFn, parse_macro_input};
use crate::scene::parse_scene_attrs;
const RANIM_CRATE_NAME: &str = “ranim”;
fn ranim_path() -> proc_macro2::TokenStream {
match (
crate_name(RANIM_CRATE_NAME),
std::env::var(“CARGO_CRATE_NAME”).as_deref(),
) {
(Ok(FoundCrate::Itself), Ok(RANIM_CRATE_NAME)) => quote!(crate),
(Ok(FoundCrate::Name(name)), _) => {
let ident = Ident::new(&name, Span::call_site());
quote!(::#ident)
}
_ => quote!(::ranim),
}
}
fn ranim_core_path() -> proc_macro2::TokenStream {
if let Ok(res) = crate_name(“ranim-core”) {
match (res, std::env::var(“CARGO_CRATE_NAME”).as_deref()) {
(FoundCrate::Itself, Ok(“ranim-core”) | Ok(“ranim_core”)) => return quote!(crate),
(FoundCrate::Name(name), _) => {
let ident = Ident::new(&name, Span::call_site());
return quote!(::#ident);
}
_ => (),
}
} else if let Ok(res) = crate_name(“ranim”) {
match (res, std::env::var(“CARGO_CRATE_NAME”).as_deref()) {
(FoundCrate::Itself, Ok(“ranim”)) => return quote!(crate::core),
(FoundCrate::Name(name), _) => {
let ident = Ident::new(&name, Span::call_site());
return quote!(::#ident::core);
}
_ => (),
}
}
ranim_path()
}
/// 解析单个属性(#[scene(…)] / / #[output(…)])
#[derive(Default)]
struct SceneAttrs {
name: Option, // #[scene(name = “…”)]
clear_color: Option, // #[scene(clear_color = “#000000”)]
wasm_demo_doc: bool, // #[wasm_demo_doc]
outputs: Vec, // #[output(…)]
}
/// 一个 #[output(…)] 里的字段
#[derive(Default)]
struct OutputDef {
width: u32,
height: u32,
fps: u32,
save_frames: bool,
name: Option,
dir: String,
format: Option,
}
// MARK: scene
#[proc_macro_attribute]
pub fn scene(args: TokenStream, input: TokenStream) -> TokenStream {
let ranim = ranim_path();
let input_fn = parse_macro_input!(input as ItemFn);
let attrs = parse_scene_attrs(args, input_fn.attrs.as_slice()).unwrap();
let fn_name = &input_fn.sig.ident;
let vis = &input_fn.vis;
let fn_body = &input_fn.block;
let doc_attrs: Vec<_> = input_fn
.attrs
.iter()
.filter(|attr| attr.path().is_ident(“doc”))
.collect();
// 场景名称
let scene_name = attrs.name.unwrap_or_else(|| fn_name.to_string());
// StaticSceneConfig
let clear_color = attrs.clear_color.unwrap_or(“#333333ff”.to_string());
let scene_config = quote! {
#ranim::StaticSceneConfig {
clear_color: #clear_color,
}
};
// StaticOutput 列表
let mut outputs = Vec::new();
for OutputDef {
width,
height,
fps,
save_frames,
name,
dir,
format,
} in attrs.outputs
{
let name_token = match name.as_deref() {
Some(n) if !n.is_empty() => quote! { Some(#n) },
_ => quote! { None },
};
let format_token = match format.as_deref() {
Some(“mp4”) | None => quote! { #ranim::OutputFormat::Mp4 },
Some(“webm”) => quote! { #ranim::OutputFormat::Webm },
Some(“mov”) => quote! { #ranim::OutputFormat::Mov },
Some(“gif”) => quote! { #ranim::OutputFormat::Gif },
Some(other) => panic!(“unknown output format: {other:?}”),
};
outputs.push(quote! {
#ranim::StaticOutput {
width: #width,
height: #height,
fps: #fps,
save_frames: #save_frames,
name: #name_token,
dir: #dir,
format: #format_token,
}
});
}
if outputs.is_empty() {
outputs.push(quote! {
#ranim::StaticOutput::DEFAULT
});
}
let doc = if attrs.wasm_demo_doc {
quote! {
#[doc = concat!(“<canvas id="ranim-app-”, stringify!(#fn_name), “" width="1280" height="720" style="width: 100%;">”)]
#[doc = concat!(“<script type="module">”)]
#[doc = concat!(“ const { find_scene, preview_scene } = await ranim_examples;“)]
#[doc = concat!(“ preview_scene(find_scene("“, stringify!(#fn_name), “"));”)]
#[doc = “”]
}
} else {
quote!
};
let static_output_name = syn::Ident::new(“__OUTPUTS”, fn_name.span());
let static_scene_name = syn::Ident::new(“__SCENE”, fn_name.span());
let output_cnt = outputs.len();
let scene = quote! {
#ranim::StaticScene {
name: #scene_name,
constructor: super::#fn_name,
config: #scene_config,
outputs: &#static_output_name,
}
};
let expanded = quote! {
#doc
#(#doc_attrs)*
#vis fn #fn_name(r: &mut #ranim::RanimScene) #fn_body
#[doc(hidden)]
#vis mod #fn_name {
/// The static outputs.
pub static #static_output_name: [#ranim::StaticOutput; #output_cnt] = [#(#outputs),*];
/// The static scene descriptor.
pub static #static_scene_name: #ranim::StaticScene = #scene;
#ranim::inventory::submit!{
#scene
}
pub fn scene() -> #ranim::Scene {
#ranim::Scene::from(&#static_scene_name)
}
}
};
TokenStream::from(expanded)
}
/// Define a video output.
///
/// Default: 1920x1080 60fps, save_frames = false
///
/// Available attributes:
/// - width: output width in pixels
/// - height: output height in pixels
/// - fps: frames per second
/// - save_frames: save frames to disk
/// - dir: directory for output
#[proc_macro_attribute]
pub fn output(_: TokenStream, _: TokenStream) -> TokenStream {
TokenStream::new()
}
// #[proc_macro_attribute]
// pub fn preview(_: TokenStream, _: TokenStream) -> TokenStream {
// TokenStream::new()
// }
#[proc_macro_attribute]
pub fn wasm_demo_doc(_attr: TokenStream, _: TokenStream) -> TokenStream {
TokenStream::new()
}
// MARK: derive Traits
#[proc_macro_derive(Fill)]
pub fn derive_fill(input: TokenStream) -> TokenStream {
let core = ranim_core_path();
impl_derive(input, quote! {#core::traits::Fill}, |field_positions| {
quote! {
fn set_fill_opacity(&mut self, opacity: f32) -> &mut Self {
#(
self.#field_positions.set_fill_opacity(opacity);
)*
self
}
fn fill_color(&self) -> #core::color::AlphaColor<#core::color::Srgb> {
[#(self.#field_positions.fill_color(), )*].first().cloned().unwrap()
}
fn set_fill_color(&mut self, color: #core::color::AlphaColor<#core::color::Srgb>) -> &mut Self {
#(
self.#field_positions.set_fill_color(color);
)*
self
}
}
})
}
#[proc_macro_derive(Stroke)]
pub fn derive_stroke(input: TokenStream) -> TokenStream {
let core = ranim_core_path();
impl_derive(input, quote! {#core::traits::Stroke}, |field_positions| {
quote! {
fn stroke_color(&self) -> #core::color::AlphaColor<#core::color::Srgb> {
[#(self.#field_positions.stroke_color(), )*].first().cloned().unwrap()
}
fn apply_stroke_func(&mut self, f: impl for<’a> Fn(&’a mut [#core::components::width::Width])) -> &mut Self {
#(
self.#field_positions.apply_stroke_func(&f);
)*
self
}
fn set_stroke_color(&mut self, color: #core::color::AlphaColor<#core::color::Srgb>) -> &mut Self {
#(
self.#field_positions.set_stroke_color(color);
)*
self
}
fn set_stroke_opacity(&mut self, opacity: f32) -> &mut Self {
#(
self.#field_positions.set_stroke_opacity(opacity);
)*
self
}
}
})
}
#[proc_macro_derive(Partial)]
pub fn derive_partial(input: TokenStream) -> TokenStream {
let core = ranim_core_path();
impl_derive(input, quote! {#core::traits::Partial}, |field_positions| {
quote! {
fn get_partial(&self, range: std::ops::Range) -> Self {
Self {
#(
#field_positions: self.#field_positions.get_partial(range.clone()),
)*
}
}
fn get_partial_closed(&self, range: std::ops::Range) -> Self {
Self {
#(
#field_positions: self.#field_positions.get_partial(range.clone()),
)*
}
}
}
})
}
#[proc_macro_derive(Empty)]
pub fn derive_empty(input: TokenStream) -> TokenStream {
let core = ranim_core_path();
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let generics = &input.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let fields = match &input.data {
Data::Struct(data) => &data.fields,
_ => panic!(“Empty can only be derived for structs”),
};
let field_impls = match fields {
Fields::Named(fields) => {
let (field_names, field_types): (Vec<>, Vec<>) =
fields.named.iter().map(|f| (&f.ident, &f.ty)).unzip();
quote! {
Self {
#(
#field_names: #field_types::empty(),
)*
}
}
}
Fields::Unnamed(fields) => {
let field_types = fields.unnamed.iter().map(|f| &f.ty);
quote! {
Self (
#(
#field_types::empty(),
)*
)
}
}
Fields::Unit => quote! {},
};
let expanded = quote! {
impl #impl_generics #core::traits::Empty for #name #ty_generics #where_clause {
fn empty() -> Self {
#field_impls
}
}
};
TokenStream::from(expanded)
}
#[proc_macro_derive(Opacity)]
pub fn derive_opacity(input: TokenStream) -> TokenStream {
let core = ranim_core_path();
impl_derive(input, quote! {#core::traits::Opacity}, |field_positions| {
quote! {
fn set_opacity(&mut self, opacity: f32) -> &mut Self {
#(
self.#field_positions.set_opacity(opacity);
)*
self
}
}
})
}
#[proc_macro_derive(Alignable)]
pub fn derive_alignable(input: TokenStream) -> TokenStream {
let core = ranim_core_path();
impl_derive(
input,
quote! {#core::traits::Alignable},
|field_positions| {
quote! {
fn is_aligned(&self, other: &Self) -> bool {
#(
self.#field_positions.is_aligned(&other.#field_positions) &&
)* true
}
fn align_with(&mut self, other: &mut Self) {
#(
self.#field_positions.align_with(&mut other.#field_positions);
)*
}
}
},
)
}
#[proc_macro_derive(Interpolatable)]
pub fn derive_interpolatable(input: TokenStream) -> TokenStream {
let core = ranim_core_path();
impl_derive(
input,
quote! {#core::traits::Interpolatable},
|field_positions| {
quote! {
fn lerp(&self, other: &Self, t: f64) -> Self {
Self {
#(
#field_positions: #core::traits::Interpolatable::lerp(&self.#field_positions, &other.#field_positions, t),
)*
}
}
}
},
)
}
#[proc_macro_derive(ShiftTransform)]
pub fn derive_shift_impl(input: TokenStream) -> TokenStream {
let core = ranim_core_path();
impl_derive(
input,
quote! {#core::traits::ShiftTransform},
|field_positions| {
quote! {
fn shift(&mut self, shift: #core::glam::DVec3) -> &mut Self {
#(self.#field_positions.shift(shift);)*
self
}
}
},
)
}
#[proc_macro_derive(RotateTransform)]
pub fn derive_rotate_impl(input: TokenStream) -> TokenStream {
let core = ranim_core_path();
impl_derive(
input,
quote! {#core::traits::RotateTransform},
|field_positions| {
quote! {
fn rotate_on_axis(&mut self, axis: #core::glam::DVec3, angle: f64) -> &mut Self {
#(self.#field_positions.rotate_on_axis(axis, angle);)*
self
}
}
},
)
}
#[proc_macro_derive(ScaleTransform)]
pub fn derive_scale_impl(input: TokenStream) -> TokenStream {
let core = ranim_core_path();
impl_derive(
input,
quote! {#core::traits::ScaleTransform},
|field_positions| {
quote! {
fn scale(&mut self, scale: #core::glam::DVec3) -> &mut Self {
#(self.#field_positions.scale(scale);)*
self
}
}
},
)
}
#[proc_macro_derive(PointsFunc)]
pub fn derive_point_func(input: TokenStream) -> TokenStream {
let core = ranim_core_path();
impl_derive(
input,
quote! {#core::traits::PointsFunc},
|field_positions| {
quote! {
fn apply_points_func(&mut self, f: impl for<’a> Fn(&’a mut [DVec3])) -> &mut Self {
#(self.#field_positions.apply_points_func(f);)*
self
}
}
},
)
}
fn impl_derive(
input: TokenStream,
trait_path: proc_macro2::TokenStream,
impl_token: impl Fn(Vec<proc_macro2::TokenStream>) -> proc_macro2::TokenStream,
) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let generics = &input.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let fields = match &input.data {
Data::Struct(data) => &data.fields,
_ => panic!(“Can only be derived for structs”),
};
let field_positions = get_field_positions(fields)
.ok_or(“cannot get field from unit struct”)
.unwrap();
let impl_token = impl_token(field_positions);
let expanded = quote! {
impl #impl_generics #trait_path for #name #ty_generics #where_clause {
#impl_token
}
};
TokenStream::from(expanded)
}
fn get_field_positions(fields: &Fields) -> Option<Vec<proc_macro2::TokenStream>> {
match fields {
Fields::Named(fields) => Some(
fields
.named
.iter()
.map(|f| {
let pos = &f.ident;
quote!
})
.collect::<Vec<_>>(),
),
Fields::Unnamed(fields) => Some(
(0..fields.unnamed.len())
.map(syn::Index::from)
.map(|i| {
quote!
})
.collect::<Vec<_>>(),
),
Fields::Unit => None,
}
}
支持的属性:
#[scene]:为场景函数生成一个对应的static <scene_name>_scene: &'static Scene,包含了场景名称、函数指针、场景设置、输出设置(通过#[output]来设置)等信息。 可以配置一些属性:#[scene(name = "...")]:为场景指定一个名称,默认与函数名相同。#[scene(clear_color = "#ffffffff")]:为场景指定一个清除颜色,默认值为#333333ff。#[output]:为场景添加一个输出: 输出的文件名<output_name>会被命名为<scene_name>_<width>x<height>_<frame_rate>。#[output(dir = "...")]:设置相对于.的输出目录,也可以是绝对路径,默认是./output#[output(width = 1920)]:设置输出宽度#[output(height = 1080)]:设置输出高度#[output(fps = 60)]:设置输出帧率#[output(save_frames = true)]:设置是否保存每一帧(保存在<dir>/<output_name>-frames/下)#[output(format = "mp4")]:设置输出格式mp4,webm,mov,gif
使用 ranim-cli 可以方便的对场景进行预览、渲染:
Important
注意:
如果想要使用 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 示例中的全部场景的所有输出
1. 场景的构造
场景函数是一个签名为 fn(&mut RanimScene) 的函数,通过 #[scene] 宏标注后会自动生成对应的场景配置。
整个构造过程围绕着 &mut RanimScene,它是 ranim 中编码动画 API 的主入口。
2. 时间线
每一条被插入到场景中的时间线都有一个唯一的 TimelineId。
时间线(Timeline)是用于编码物件动画的核心结构,它内部存储了一系列动画及其展示时间。
编码动画的过程本质上是在向时间线中插入动态或静态的动画:

2.1 创建时间线
有两种方式创建时间线:
1. 创建空白时间线
let tid: TimelineId = r.insert_empty();
2. 插入物件并创建时间线
通过 r.insert(item) 可以插入一个物件,自动为其创建时间线并播放 show 动画:
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_square = r.insert(square);
let r_circle = r.insert(circle);
2.2 访问时间线
时间线创建后,通过 r.timeline() 或 r.timeline_mut() 来访问:
// 获取不可变引用
let timeline_ref: &Timeline = r.timeline(r_square);
// 获取可变引用
let timeline_mut: &mut Timeline = r.timeline_mut(r_square);
也可以同时访问多条时间线:
// 访问多条时间线的可变引用
let [t1, t2] = r.timeline_mut([r_square1, r_square2]);
访问全部时间线:
// 类型为 &[Timeline]
let timelines = r.timelines();
// 类型为 &mut [Timeline]
let timelines = r.timelines_mut();
2.3 操作时间线
Timeline 提供了以下方法用于编码动画:
| 方法 | 描述 |
|---|---|
show() | 显示时间线中的物体 |
hide() | 隐藏时间线中的物体 |
forward(secs) | 推进时间线指定秒数 |
forward_to(target_sec) | 推进时间线到指定时间点 |
play(anim) | 向时间线中插入动画 |
所有方法都返回 &mut Self,支持链式调用。
下面的例子使用一个 Square 物件创建了一个时间线,然后编码了淡入 1 秒、显示 1 秒、隐藏、显示 1 秒、淡出 1 秒的动画:
use ranim::{
anims::fading::FadingAnim, color::palettes::manim, items::vitem::geometry::Square, prelude::*,
};
#[scene]
#[output(dir = "./output/getting_started0")]
fn getting_started0(r: &mut RanimScene) {
// Equivalent to creating a new timeline then playing `CameraFrame::default().show()` on it
let _r_cam = r.insert(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_empty();
{
let timeline = r.timeline_mut(r_square);
timeline
.play(square.clone().fade_in()) // Can be written as `square.fade_in_ref()`
.forward(1.0)
.hide()
.forward(1.0)
.show()
.forward(1.0)
.play(square.clone().fade_out()); // Can be written as `square.fade_out_ref()`
}
// In the end, ranim will automatically sync all timelines and forward to the end.
// Equivalent to `r.timelines_mut().sync();`
}
2.4 物件类型转换
在对物件进行动画编码时,有时需要进行类型转换。例如,Square 需要转换为更低级的 VItem 才能应用 write 和 unwrite 等动画:
use ranim::{
anims::{creation::WritingAnim, morph::MorphAnim},
color::palettes::manim,
items::vitem::{
VItem,
geometry::{Circle, Square},
},
prelude::*,
};
#[scene]
#[output(dir = "./output/getting_started1")]
fn getting_started1(r: &mut RanimScene) {
let _r_cam = r.insert(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);
});
let r_vitem = r.insert_empty();
{
let timeline = r.timeline_mut(r_vitem);
// In order to do more low-level opeerations,
// sometimes we need to convert the item to a low-level item.
timeline.play(VItem::from(square).morph_to(VItem::from(circle.clone())));
timeline.play(VItem::from(circle).unwrite());
}
}
在这个例子中:
- 创建了一个空白时间线
- 将
Square转换为VItem后应用morph_to动画 - 将
Circle转换为VItem后应用unwrite动画
类型转换通过 VItem::from() 或 .into() 完成,转换后的物件可以使用更多底层动画效果。
Packages
.
├── src/ # ranim - 顶层 facade crate
├── packages/
│ ├── ranim-core/ # 核心动画引擎(时间线、组件、动画 trait)
│ ├── ranim-macros/ # proc-macro(#[scene]、#[output] 等)
│ ├── ranim-items/ # 内置可视元素(VItem、几何图形、SVG、文本)
│ ├── ranim-anims/ # 内置动画(淡入淡出、变形、书写等)
│ ├── ranim-render/ # GPU 渲染层(wgpu)
│ └── ranim-cli/ # CLI 工具(渲染、预览、热加载)
├── example-packages/app/ # 示例应用
├── benches/ # 性能基准测试
└── xtasks/xtask-examples/ # 示例构建自动化
graph BT
macros[ranim-macros]
core[ranim-core] --> macros
items[ranim-items] --> core
anims[ranim-anims] --> core
render[ranim-render] --> core
ranim[ranim] --> core
ranim --> items
ranim --> anims
ranim --> render
cli[ranim-cli] --> ranim
核心概念
动画
本节将对 Ranim 中 动画 的实现思路进行讲解。
Eval<T> Trait
一个标准化的动画其实本质上就是一个函数 ,它的输入是一个进度值 ,输出是该动画在对应进度处的结果 :
这个函数 不仅定义了动画的 求值,同时其内部也包含了求值所需要的 信息。对应到计算机世界,其实也就是 算法 和 数据,而对应到编程语言上也就是 方法 和 数据类型。
在由 Rust 实现的 Ranim 中也就是 Eval<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 Eval<T> {
/// Evaluates at the given progress value `alpha` in range [0, 1].
fn eval_alpha(&self, alpha: f64) -> T;
// ...
}
它接受自身的不可变引用和一个进度值作为输入,经过计算,输出一个自身类型的结果。
例 | Static 动画
ranim_core::animation::Static 动画是最基础的,也是唯一一个内置进 ranim-core 的动画:
/// A static animation.
pub struct Static<T>(pub T);
impl<T: Clone> Eval<T> for Static<T> {
fn eval_alpha(&self, _alpha: f64) -> T {
self.0.clone()
}
}
非常简单,其内置的 信息 就是物件本身,其 求值 就是简单的返回相同的物件。
例 | Transform 动画
以 ranim_anims::transform::Transform 动画为例,其内部包含了物件初始状态和目标状态,以及用于插值的对齐后的初始和目标状态,在 EvalDynamic<T> 的实现中使用内部的数据进行计算求值得到结果:
{{#include ../../../../packages/ranim-anims/src/transform.rs:Transform}}
{{#include ../../../../packages/ranim-anims/src/transform.rs:Transform-Eval}}
AnimationCell
一个动画还会有很多额外的信息:
- 开始时间
- 持续时间
- 速率函数
- …
在 Ranim 中,这些被信息被表示为一个 AnimationInfo 结构:
/// Info of an animation.
///
/// When [`AnimationInfo::enabled`] is `false`, the animation will not be evaluated.
#[derive(Debug, Clone)]
pub struct AnimationInfo {
/// The rate function used for evaluating, default value: [`linear`]
pub rate_func: fn(f64) -> f64,
/// Start sec, default value: 0.0
pub start_sec: f64,
/// The duration seconds, default value: 1.0
pub duration_secs: f64,
/// Is enabled, default value: true
pub enabled: bool,
}
impl Default for AnimationInfo {
fn default() -> Self {
Self {
rate_func: linear,
start_sec: 0.0,
duration_secs: 1.0,
enabled: true,
}
}
}
通过这些信息,我们可以将全局的秒映射到局部的 ,并将局部的 映射到内部标准化的 :
impl AnimationInfo {
/// Map the global sec to outer alpha
///
/// note that this uses a range_inclusive
pub fn map_sec_to_alpha(&self, sec: f64) -> Option<f64> {
if self.range_inclusive().contains(&sec) {
let alpha = (sec - self.start_sec) / self.duration_secs;
let alpha = if alpha.is_nan() { 1.0 } else { alpha };
Some(alpha)
} else {
None
}
}
/// Map the outer alpha to inner alpha
pub fn map_alpha(&self, alpha: f64) -> f64 {
(self.rate_func)(alpha)
}
// ...
}
如此,将以 为输入标准化的动画函数与这些信息结合,也就构造除了一个以秒 为输入的动画函数 :
在 Ranim 中,这对应着 AnimationCell 结构:
/// A cell of an animation
pub struct AnimationCell<T> {
inner: Box<dyn Eval<T>>,
/// The animation info
pub info: AnimationInfo,
// ...
}
impl<T> Eval<T> for AnimationCell<T> {
fn eval_alpha(&self, alpha: f64) -> T {
self.inner.eval_alpha(self.info.map_alpha(alpha))
}
}
Requirement Trait 模式
相信你注意到了,在实际的动画编写中,我们并没有手动构造任何一个动画结构,而是直接在物件身上调用一个方法来构造 AnimationCell:
let vitem_a = // ...;
let vitem_b = // ...;
// let anim = Transform::new(vitem_a, vitem_b).to_animation_cell();
// r.timeline_mut(t_id).play(anim);
r.timeline_mut(t_id).play(vitem_a.clone().transform_to(vitem_b));
let mut vitem_a = // ...;
let vitem_b = // ...;
// let anim = Transform::new(vitem_a, vitem_b).to_animation_cell();
// vitem_a = anim.eval_alpha(1.0);
// r.timeline_mut(t_id).play(anim);
r.timeline_mut(t_id).play(vitem_a.morph_to(vitem_b));
这是 Ranim 动画的一种编程模式,每一个动画都有一个对应的 Requirement Trait:
/// The requirement of [`Morph`]
pub trait MorphRequirement: Alignable + Interpolatable + Clone {}
impl<T: Alignable + Interpolatable + Clone> MorphRequirement for T {}
同时还有一个对应的 Animation Trait,包含了一系列的 Helper 函数,以及为 T: <Requirement Trait> 的实现:
/// The methods to create animations for `T` that satisfies [`MorphRequirement`]
pub trait MorphAnim: MorphRequirement + Sized + 'static {
/// Create a [`Morph`] anim with a func.
fn morph<F: Fn(&mut Self)>(&mut self, f: F) -> AnimationCell<Self>;
/// Create a [`Morph`] anim from src.
fn morph_from(&mut self, src: Self) -> AnimationCell<Self>;
/// Create a [`Morph`] anim to dst.
fn morph_to(&mut self, dst: Self) -> AnimationCell<Self>;
}
impl<T: MorphRequirement + 'static> MorphAnim for T {
fn morph<F: Fn(&mut T)>(&mut self, f: F) -> AnimationCell<T> {
let mut dst = self.clone();
(f)(&mut dst);
Morph::new(self.clone(), dst)
.into_animation_cell()
.with_rate_func(smooth)
.apply_to(self)
}
fn morph_from(&mut self, s: T) -> AnimationCell<T> {
Morph::new(s, self.clone())
.into_animation_cell()
.with_rate_func(smooth)
.apply_to(self)
}
fn morph_to(&mut self, d: T) -> AnimationCell<T> {
Morph::new(self.clone(), d)
.into_animation_cell()
.with_rate_func(smooth)
.apply_to(self)
}
}
通过这种模式可以便捷地构造动画,并将动画的效果应用到物件状态上。
时间线
简单来说,时间线的本质是类型擦除后的 AnimationCell<T> 的容器,若干条时间线组合在一起即表示了整个场景的完整动画。
因为 AnimationCell<T> 带有范性参数,所以也就涉及类型擦除,满足于 T: AnyExtractCoreItem 的 AnimationCell<T> 会被擦除为 Box<dyn CoreItemAnimation>:
impl<T: AnyExtractCoreItem> CoreItemAnimation for AnimationCell<T> {
fn eval_alpha_dyn(&self, alpha: f64) -> DynItem {
DynItem(Box::new(self.eval_alpha(alpha)))
}
fn eval_alpha_core_item(&self, alpha: f64) -> Vec<CoreItem> {
self.eval_alpha(alpha).extract()
}
// ...
}
/// A timeline for a animations.
#[derive(Default)]
pub struct Timeline {
anims: Vec<Box<dyn CoreItemAnimation>>,
// Followings are states use while constructing
cur_sec: f64,
/// The start time of the planning static anim.
/// When it is some, it means that it is showing and has a planning static anim.
planning_static_start_sec: Option<f64>,
}
在编写动画时的一系列操作(如 forward、play 等)最后都会转变为对 Timeline 内部属性的操作,最终达成的结果就是在其 anims 属性中完成此条时间线所有动 AnimationCell 的编码(即“把动画在时间上放到正确的位置”)。