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

Ranim 的场景由一个 fn(&mut RanimScene) 函数构造。场景函数只负责定义动画;预览、渲染和输出配置由 #[scene]#[output] 与 ranim CLI 处理。

准备项目

使用 CLI 热加载 lib target 时,crate 需要生成动态库:

[lib]
crate-type = ["rlib", "cdylib"]

动画代码通常从 prelude、item 类型和对应的动画扩展 trait 中导入 API:

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

第一个场景

下面的场景让一个蓝色正方形淡入、保持一秒,再淡出。相机作为独立 Sequence 与内容并行播放:

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

#[scene(clear_color = "#000000")]
#[output(width = 1280, height = 720, fps = 30, format = "mp4")]
fn hello(r: &mut RanimScene) {
    let square = Square::new(2.0).with(|square| {
        square.set_color(manim::BLUE_C);
    });

    let mut content = AnimSequence::new();
    content
        .push(square.clone().fade_in())
        .hold(1.0)
        .push(square.fade_out());

    let mut camera = AnimSequence::new();
    camera
        .push(CameraFrame::default().show())
        .hold_to(content.cursor_sec());

    r.play(camera);
    r.play(content);
}

#[scene] 会保留场景函数,并生成、注册对应的静态 Scene 描述。通常不需要手工创建 Scene 或编写 main

Scene 的根是并行 Stack

RanimScene::play 等价于向根 AnimStack 执行 push

r.play(camera);
r.play(content);

这两个动画共享局部 0 秒并行播放,互不覆盖。play 不维护全局 cursor,也不会根据 item 值查找并修改之前加入的动画。

需要顺序播放时,先使用 AnimSequence 组织一条完整状态序列,再将 Sequence 加入 Scene。

使用 AnimSequence

AnimSequence 维护自己的 cursor:

方法行为
push(animation)在当前 cursor 加入动画,并按其 duration 推进 cursor
forward(secs)只推进 cursor,空白区间没有输出
forward_to(sec)将 cursor 推进到指定绝对时间
hold(secs)保持 cursor 处的 Sequence 状态并推进 cursor
hold_to(sec)将当前状态保持到指定绝对时间
cursor_sec()返回当前 Sequence 时长/cursor

完整的 show/hide 示例可以直接查看:

use ranim::{
    anims::fading::FadingAnim, color::palettes::manim, items::vitem::geometry::Square, prelude::*,
    utils::rate_functions::smooth,
};

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

    let mut content = seq![square.clone().fade_in().with_rate_func(smooth)];
    content
        .hold(1.0)
        .push(square.hide())
        .forward(1.0)
        .push(square.show())
        .hold(1.0)
        .push(square.clone().fade_out().with_rate_func(smooth));

    r.play(
        CameraFrame::default()
            .show()
            .with_duration(content.cursor_sec()),
    );
    r.play(content);
}

holdforward

forward 表示明确的空白时间;hold 表示把当前状态延长一段时间。新模型不会隐式认为一个动画结束后物件仍然存在。

sequence
    .push(square.fade_in())
    .hold(1.0)    // 保持淡入后的状态
    .forward(0.5) // 接下来 0.5 秒没有该 Sequence 的输出
    .push(circle.fade_in());

showhide

show()hide() 是用于 Sequence 状态切换的零时长事件。cursor 上出现状态事件时,后续 hold 使用这些事件组成新的完整状态快照,不再继承左侧状态。

sequence
    .push(square.show())
    .hold(1.0)
    .push(square.hide())
    .hold(1.0);

hide 不会跨 Sequence 查找同一个 item。需要独立显示/隐藏的内容应放在独立 Sequence 中,再通过根 Stack 并行组合。

并行组合

固定数量的并行动画可以使用 stack!

let scene = stack![
    background.show().with_duration(total_secs),
    content,
    camera.show().with_duration(total_secs),
];
r.play(scene);

运行时动态生成的动画使用 AnimStack

let mut layers = AnimStack::new();
for animation in animations {
    layers.push(animation);
}
r.play(layers);

AnimStack 的 duration 是最长子动画的 duration。较短子动画结束后不会自动保持到 Stack 结束。

类型转换与动画扩展 trait

动画方法由 requirement/extension trait 提供,使用前需要导入对应 trait。例如 fade_in 来自 FadingAnimmorph_to 来自 MorphAnimwrite/unwrite 来自 WritingAnim

有些动画只对更底层的 VItem 实现。几何 item 可以通过 VItem::from.into() 转换:

use ranim::{
    anims::{creation::WritingAnim, morph::MorphAnim},
    color::palettes::manim,
    items::vitem::{
        VItem,
        geometry::{Circle, Square},
    },
    prelude::*,
    utils::rate_functions::smooth,
};

#[scene]
#[output(dir = "./output/getting_started1")]
fn getting_started1(r: &mut RanimScene) {
    // 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 content = seq![
        VItem::from(square)
            .morph_to(VItem::from(circle.clone()))
            .with_rate_func(smooth),
        VItem::from(circle).unwrite().with_rate_func(smooth),
    ];

    r.play(
        CameraFrame::default()
            .show()
            .with_duration(content.cursor_sec()),
    );
    r.play(content);
}

多个独立 Sequence 的组合示例:

use ranim::{
    anims::{
        creation::{CreationAnim, WritingAnim},
        morph::MorphAnim,
    },
    color::palettes::manim,
    items::vitem::{
        VItem,
        geometry::{Circle, Rectangle, Square},
    },
    prelude::*,
    utils::rate_functions::{linear, smooth},
};

#[scene]
#[output(dir = "./output/getting_started2")]
fn getting_started2(r: &mut RanimScene) {
    let rect = Rectangle::new(4.0, 9.0 / 4.0).with(|rect| {
        rect.set_stroke_color(manim::GREEN_C);
    });

    let square: VItem = Square::new(2.0)
        .with(|square| {
            square.set_color(manim::BLUE_C);
        })
        .into();
    let circle: VItem = Circle::new(2.0)
        .with(|circle| {
            circle.set_color(manim::RED_C);
        })
        .into();
    let mut rect_sequence = seq![rect.clone().show()];
    rect_sequence
        .hold(1.0)
        .push(VItem::from(rect).uncreate().with_rate_func(smooth));

    let mut item_sequence = seq![square.clone().show()];
    item_sequence
        .hold(1.0)
        .push(square.clone().create().with_rate_func(smooth))
        .push(
            square
                .clone()
                .morph_to(circle.clone())
                .with_rate_func(linear),
        )
        .push(circle.clone().unwrite().with_rate_func(smooth));

    let total_secs = item_sequence.cursor_sec().max(rect_sequence.cursor_sec());
    r.play(CameraFrame::default().show().with_duration(total_secs));
    r.play(stack![rect_sequence, item_sequence]);
}

Scene 与 Output 属性

#[scene] 支持:

  • name = "...":设置注册的场景名称,默认使用函数名。
  • clear_color = "...":设置 CSS 格式的清屏颜色,默认 #333333ff

每个 #[output] 定义一个输出;一个 Scene 可以声明多个 output:

  • widthheight:输出像素尺寸,默认 1920x1080。
  • fps:帧率,默认 60。
  • formatmp4webmmovgif
  • dir:输出目录,默认 ./output
  • name:输出文件名前缀;未设置时使用 Scene 名称。
  • save_frames:是否保存逐帧图片,默认 false

没有写 #[output] 时会使用默认输出配置。

预览与渲染

安装 CLI:

cargo install ranim-cli

预览或渲染当前 package 的 lib target:

ranim preview
ranim render
ranim render hello

指定 workspace package 或 example target:

ranim preview -p package_name --example example_name
ranim render -p package_name --example example_name

preview 可以接收一个可选 Scene 名称;render 可以接收零个或多个 Scene 名称。额外的 Cargo 构建参数放在 -- 后,例如:

ranim render hello -- --release

在本仓库中可以直接运行 CLI package:

cargo run -p ranim-cli --release -- preview --example getting_started0
cargo run -p ranim-cli --release -- render --example getting_started0