Getting Started
[!caution]本书绝大多数内容目前均由 ai 生成,可能叙事逻辑和表述并不是很好,仅供参考。
Ranim 的场景仍由一个 fn(&mut RanimScene) 函数构造,只负责定义动画,可以写在 src/lib.rs、普通模块或独立的库 crate 中;不要求把场景搬进二进制。变化在运行入口:宿主二进制用 App 显式注册场景与项目配置,app.build() 定稿后,native 下交给 run_cli,WASM 下包装为 WasmApp,通过导出的 app() 供 JavaScript 使用。Plugin 只是复用注册和配置的可选工具,并不替代场景函数。
本章介绍场景的编写。完整的 lib / bin 布局、两种平台入口和构建命令见项目入口:场景库与跨平台宿主。
准备项目
Cargo.toml 只需要 ranim 依赖:
[]
= "..."
默认启用 anims、items、cmd-preview;cmd-preview → cmd-render → render。只需原生导出时,设置 default-features = false 并开启 anims,cmd-render。render 本身只提供渲染器和 WASM 宿主,不包含内置物件或原生命令。例如包名为 my-animation,场景定义在 src/lib.rs 时,native 的src/main.rs 从库中导入并注册场景,build() 定稿后交给 run_cli:
use *;
use hello;
app.build()(RanimAppExt,prelude 已导出)是「构建中的 App → 可开会话的项目」的显式转换:断言插件全部就绪(Ready),执行一次 bevy 的finish() / cleanup(),返回 ranim::RanimApp。RanimApp 是项目的会话期形态——Deref / DerefMut 到 bevy App,update、渲染 SubApp安装、资源访问与场景选择照常工作,但不再是插件构建目标;run_cli、preview_scene 等会话入口都以它为输入。
run_cli 安装 tracing、解析进程参数,随即提供preview / output / render / inspect 子命令,详见命令行入口。inspect 是纯 CPU 查询,不执行渲染装配(RenderSetup)的 GPU 回调。
动画代码通常从 prelude、item 类型和对应的动画扩展 trait 中导入 API:
use ;
Prelude 覆盖什么
ranim::prelude::* 覆盖日常作者 API:Eval / IntoAnimNode / Unplaced/ PlaybackExt 等动画协议,AnimSequence / AnimStack / AnimLagged 与seq! / stack! / lagged!,Pure / Iterative / Static,Sound /AudioClip,以及 RanimScene / TimeMark / App / Plugin。
以下类型有意不放进 prelude,需要时显式导入:
use AudioError;
use ;
use ;
use EvaluatedFrame;
EvaluatedFrame:读取求值结果或直接驱动渲染时使用;AnimNode/AnimationInfo:运行时 introspection API;At/Paramed:需要给返回值命名的 timing wrapper;AudioError:音频解码/文件错误处理。
第一个场景
下面的场景让一个蓝色正方形淡入、保持一秒,再淡出。相机作为独立 Sequence 与内容并行播放:
// src/lib.rs
use ;
#[scene] 保留场景函数,并生成同名模块中的 hello::scene() -> Scene。每次调用都创建一个 owned 描述符,其中包含名称、构造函数、配置和输出列表。这里的 pub 同时使场景函数和生成的同名模块对二进制可见;在 lib 中定义场景不会自动注册它,也不会启动渲染。把描述符注册进 App(add_scene / add_scenes 由 AppExt 提供),build() 定稿后交给 run_cli:
// src/main.rs(native)
use hello;
use *;
只有加入 App 的场景才会出现在命令行和 Web 入口中。场景目录保留显式注册顺序,场景名称必须唯一。
渲染上面的场景不需要任何装配:场景输出裸 Square 和 CameraFrame,启用 items 的项目未注册RenderSetup 时渲染与预览回退到默认配置,内置相机与矢量/网格渲染开箱即用(见下文「自定义渲染配置」)。
用 Plugin 组织项目(可选)
ranim::Plugin 就是 bevy 的 Plugin。插件持有自己的配置,在 build 中安装项目能力——build 的签名是 fn build(&self, app: &mut bevy_app::App);ranim::App 与 bevy_app::App 是同一类型,写成 &mut App 完全等价。场景注册等方法由 AppExt trait(prelude 已导出)在 App 上提供。场景变多、需要跨二进制复用,或要携带自定义渲染配置时,把它们收进插件:
// 追加到 src/lib.rs
;
二进制入口安装库导出的插件,场景定义仍留在 lib 中:
// src/main.rs(native)
use DemoScenesPlugin;
use *;
宿主既可以直接安装插件类型,也可以安装另一个 crate 的普通 Rust 工厂函数:
let mut app = new;
app.add_plugins;
// 在另一个应用中使用这个 crate:
let mut app = new;
app.add_plugins;
插件在添加时执行构建,也可以通过 app.add_plugins(...) 继续安装依赖插件。多个插件可写成元组,例如 app.add_plugins((GeometryPlugin, TextPlugin))。带配置的插件直接构造,例如 DemoPlugin { quality: 4 },不要求实现 Default。项目资源就是 bevy world 资源:app.insert_resource(...) 存入(类型需#[derive(Resource)],ranim::Resource 已再导出),经 app.world() 读取;多个项目互不共享全局注册表。
自定义渲染配置(可选)
渲染配置依赖 ranim 的 render feature(默认的 cmd-preview 会间接启用它)。启用 items 的项目未注册RenderSetup 时,渲染与预览使用的默认配置等价于:
new
ranim_items::RanimItemsPlugin 捆绑三个内置物件插件(CameraFramePlugin、VItemPlugin、MeshItemPlugin),覆盖裸内置类型与 Vec<VItem>。它与三个单件插件互斥:Bevy 插件在 App 内唯一,重复安装会 panic。
关闭 items 时默认配置为空,通用 render 不会引入内置物件;自定义渲染项目通过 RenderSetup 安装自己的插件。
若完整输出是默认配置未覆盖的类型——如 Vec<Circle>、Transformed<T, G> 或自定义类型——在插件中注册自己的 RenderSetup,为实际完整类型加入ExtractPlugin<T>(只注册 T 不会读取这些组合):
use ;
;
RenderSetup 是 bevy Resource,保存在项目 App 中,preview / output 从所属App 取得并在每次安装渲染 SubApp 时重放,分辨率变化和后台导出也使用同一项目的配置。保存 setup 不会初始化 GPU,纯 CPU 求值与 inspect 也不执行该回调。详见完整求值帧与渲染提取。
Scene 的根是并行 Stack
RanimScene::play 等价于向根 AnimStack 执行 push:
r.play;
r.play;
这两个动画共享局部 0 秒并行播放,互不覆盖。play 不维护全局 cursor,也不会根据 item 值查找并修改之前加入的动画。
需要顺序播放时,先使用 AnimSequence 组织一条完整状态序列,再将 Sequence 加入 Scene。
使用 AnimSequence
AnimSequence 维护自己的 cursor:
方法 | 行为 |
|---|---|
| 在当前 cursor 加入动画,并按其 duration 推进 cursor |
| 只推进 cursor,空白区间没有输出 |
| 将 cursor 推进到指定绝对时间 |
| 保持 cursor 处的 Sequence 状态并推进 cursor |
| 将当前状态保持到指定绝对时间 |
| 返回当前 Sequence 时长/cursor |
完整的 show/hide 示例可以直接查看:
hold 与 forward
forward 表示明确的空白时间;hold 表示把当前状态延长一段时间。新模型不会隐式认为一个动画结束后物件仍然存在。
sequence
.push
.hold // 保持淡入后的状态
.forward // 接下来 0.5 秒没有该 Sequence 的输出
.push;
show 与 hide
show() 和 hide() 是用于 Sequence 状态切换的零时长事件。cursor 上出现状态事件时,后续 hold 使用这些事件组成新的完整状态快照,不再继承左侧状态。
sequence
.push
.hold
.push
.hold;
hide 不会跨 Sequence 查找同一个 item。需要独立显示/隐藏的内容应放在独立 Sequence 中,再通过根 Stack 并行组合。
并行组合
固定数量的并行动画可以使用 stack!:
let scene = stack!;
r.play;
运行时动态生成的动画使用 AnimStack:
let mut layers = new;
for animation in animations
r.play;
AnimStack 的 duration 是最长子动画的 duration。较短子动画结束后不会自动保持到 Stack 结束。
类型转换与动画扩展 trait
动画方法由 requirement/extension trait 提供,使用前需要导入对应 trait。例如 fade_in 来自 FadingAnim,morph_to 来自 MorphAnim,write/unwrite 来自 WritingAnim。
有些动画只对更底层的 VItem 实现。几何 item 可以通过 VItem::from 或 .into() 转换:
多个独立 Sequence 的组合示例:
Scene 与 Output 属性
#[scene] 支持:
name = "...":设置注册的场景名称,默认使用函数名。clear_color = "...":设置 CSS 格式的清屏颜色,默认#333333ff。
每个 #[output] 定义一个输出;一个 Scene 可以声明多个 output:
width、height:输出像素尺寸,默认 1920x1080。fps:帧率,默认 60。format:mp4、webm、mov或gif。dir:输出目录,默认./output。name:输出文件名前缀;未设置时使用 Scene 名称。save_frames:是否保存逐帧图片,默认false。
没有写 #[output] 时会使用默认输出配置。
预览与渲染
run_cli 已把常用流程做成子命令;需要在 Rust 代码里直接驱动时,创建 App、注册场景,再选择场景(app.scene(name) 返回克隆的描述符):
let mut app = new;
app.add_scenes; // 或安装项目插件:app.add_plugins(my_project::plugin())
let scene = app.scene.expect;
// 项目的渲染装配:读取 RenderSetup 资源,未注册时回退到默认配置
let setup = app
.world
.
.cloned
.unwrap_or_else;
// 需要 cmd-render feature,渲染场景声明的所有输出;2 是 readback 缓冲数量。
render_scene;
// 需要 cmd-preview feature,预览 app 接管定稿后的项目(RanimApp),项目资源全程可用。
preview_scene;
WASM 预览宿主
WASM 的入口同样来自宿主装配,不限制场景源码的位置:场景可以继续放在 lib 中,入口从库中取得描述符或 Plugin,再导出 app() -> WasmApp。本节展示仓库 example 的写法;普通项目的 src/lib.rs + src/main.rs 完整示例见项目入口。
命令行入口 run_cli 只面向原生目标(需要 cmd-render feature)。仓库的 example把平台入口与共享装配放在两个文件中:
examples/arc/
├── main.rs # 唯一 bin target:native CLI / WASM 空 main 和 app() 导出
└── scenes.rs # 共享场景、ScenesPlugin 与 RenderSetup
main.rs 声明 mod scenes。原生 main() 创建 App、安装 scenes::ScenesPlugin后调用 run_cli(app.build());WASM 下则提供空 main(),并在同一文件中导出 app(),使用同一套装配,把定稿后的项目交给 JavaScript:
在根 Cargo.toml 中只声明一个 example,两种平台都使用它:
[[]]
= "arc"
= "examples/arc/main.rs"
= ["anims", "render"]
required-features 声明共享能力,场景所需的 typst、gltf 等 feature 也在其中。原生 CLI 还需 cmd-render,egui 预览由 cmd-preview 提供;两者默认均启用。关闭 default 的原生导出使用 anims,cmd-render,WASM 使用 anims,render。原生未开启 cmd-render 时,入口给出编译错误,提示启用该 feature。
main.rs 用函数级 cfg 切换入口;app() 导出也只在 WASM 下编译:
WASM bin 和 cdylib 在该平台都生成 .wasm 模块,且都支持 wasm-bindgen 导出。bin 需要 main(),wasm-bindgen 将它接入模块初始化;这里的空函数立即返回,模块与导出函数继续存在。加载模块不会自动装配项目或启动预览,JavaScript在 await init() 后显式调用 app() 创建项目,再调用 createPreview 申请 GPU。cdylib 无需 main(),但绑定运行时初始化和显式的 #[wasm_bindgen(start)]仍会在加载时执行。本项目没有声明自动启动应用的函数。独立项目仍可选择 [lib] crate-type = ["cdylib"],开启 ranim/render 并直接依赖wasm-bindgen;库入口直接声明 mod scenes 并导出 app() 即可。
页面初始化模块后,显式选择场景并启动预览:
sceneNames() 返回以换行分隔的注册场景名。createPreview 的 Promise 等待GPU 与场景初始化;返回的会话提供 duration、renderAt(seconds)、resize(width, height)(物理像素)与 dispose()。播放时钟由 Web 端管理,WASM 不自行启动事件循环。每个 WasmApp 创建一次预览;需要新的预览时重新调用 app()。
仓库的 cargo examples build [EXAMPLES...] 会完成编译、wasm-bindgen和 wasm-opt。视口与控制条源码分别位于 website/static/ranim-viewport.js和 website/static/ranim-controls.js,由 Notist SSG 直接提供;为 rustdoc 等其他输出目录构建时,会把这两个组件及其音频和 timeline 模块复制到输出根目录。<ranim-viewport> 负责 canvas 和播放时钟,<ranim-controls> 通过 for绑定同一 DOM 根中的视口 ID,可放在页面任意位置;同一视口也能绑定多个控制条。JavaScript 可以用 controls.target = viewport 显式绑定,包括跨Shadow DOM 的组合。网站和 rustdoc 都用这两个组件,无需 egui:
进度条右侧的展开按钮会打开动画 timeline,显示与原生 egui 相同来源的动画树和时间片段;点击片段只选中并查看详情,点击或拖动上方时间刻度才改变进度。还可折叠子轨道、缩放时间轴。给控制条添加timeline-expanded,或设置 controls.timelineExpanded = true 可默认展开。视口在会话就绪时读取一次 WasmPreview.timeline(),通过 viewport.timeline提供元数据;播放时只更新指针,不会反复跨 WASM 读取整棵动画树。
场景有声音时,视口在初始化时读取一次 WasmPreview.audio() 的预混音声道,点击播放后通过 Web Audio 播放,画面时间跟随音频输出时钟。播放、暂停和拖动进度条会同时控制声音;暂停中定位保持安静,播放中定位从新位置继续。切换场景或卸载视口会关闭音频设备,无声音场景仍按墙钟播放。控制条的扬声器按钮可调音量或静音;同一视口的多个控制条共享设置,切换场景后保留音量。自定义宿主可设置 viewport.volume(0–1)和 viewport.muted,并监听 volumechange。
命令行:预览与渲染
项目二进制的 run_cli 提供全部命令行入口:
不渲染、只查询场景信息时使用 inspect:
tree 的 Scene 名称在只有一个 Scene 时可以省略;frame 必须指定 Scene 名称,--at 为采样时间(秒)。需要完整几何数据时给 frame 加 --verbose,机器可读输出加 --format json。
preview 可以接收一个可选 Scene 名称;output 可以接收零个或多个 Scene 名称,并渲染它们声明的所有 #[output(...)];render 接收恰好一个 Scene 名称,使用默认输出设置(1920x1080、60 fps、mp4)做一次临时渲染,不读取 #[output(...)]。完整说明见命令行入口。
在本仓库中运行示例:
带 required-features(如 typst、gltf)的示例需要同时开启对应 feature。