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

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)是用于编码物件动画的核心结构,它内部存储了一系列动画及其展示时间。

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

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 才能应用 writeunwrite 等动画:

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());
    }
}

在这个例子中:

  1. 创建了一个空白时间线
  2. Square 转换为 VItem 后应用 morph_to 动画
  3. Circle 转换为 VItem 后应用 unwrite 动画

类型转换通过 VItem::from().into() 完成,转换后的物件可以使用更多底层动画效果。