跳到主要内容

低级 API 用法

背景

虽然 JS 运行时提供了高级 API,允许快速将 Rive 集成到 Web 应用中,但运行时也允许使用更小的低级 API,可以在你自己的渲染循环中构建和控制 Rive。使用此低级 API 有几个理由和好处:

  • 在一个 <canvas> 元素中构建包含多个 Rive 文件、画板、线性动画和状态机的场景。这在构建游戏时非常有用!
  • 控制渲染循环,这涉及你如何随时间推进每个画板、动画和状态机(包括速度)
  • 能够访问绘制层级中节点/骨骼上的各种变换属性值
  • 更小的依赖大小
  • ……以及更多!

前提

以下是使用低级 API 渲染 Rive 的基本渲染工作流:

加载 Rive Web Assembly (WASM) 文件

加载 Rive 文件

创建画板、线性动画和状态机的实例

构建渲染循环函数以操作上面创建的实例

推进任何动画实例并应用

推进任何状态机实例

推进画板

在 canvas 上渲染更新的画板

请求下一个动画帧

完成后清理创建的实例

开始使用

如果你决定应用需要低级 JS API,请阅读下方关于如何设置所有内容的指南,或者你可以跳到结尾查看一些运行中的示例。

加载 WASM

设置低级 Rive API 的第一步是从 @rive-app/canvas-advanced@rive-app/webgl2-advanced 库加载 Rive WASM 文件(默认情况下,我们建议使用 @rive-app/canvas-advanced 以获得更小的依赖,除非你需要使用 WebGL2)。当 WASM 文件加载到你的应用中后,你将获得必要的 API,如 Canvas/WebGL 的渲染器,以及通过 rive-cpp 从底层 CPP 绑定生成的相关 JS 类,这是用作多个其他 Rive 运行时基础的核心 C++ 运行时。你将使用这些类在下面的 canvas 中构建渲染场景。

你可以通过 unpkg(托管我们的 JS 运行时的 NPM 模块)加载 Rive WASM 文件,这将向 CDN 发起网络调用,或者你可以选择在自己的服务器上托管 WASM 文件。使用 unpkg 时,URL 看起来会像这样:

https://unpkg.com/@rive-app/[email protected]/rive.wasm

你需要确保 @rive-app/canvas-advanced@ 或 @rive-app/webgl2-advanced@ 末尾的版本与你在应用中安装的依赖版本匹配。例如,如果你在 package.json 中安装了 @rive-app/[email protected],那么你从 unpkg 请求的 Rive WASM 文件将是 https://unpkg.com/@rive-app/[email protected]/rive.wasm。

参见预加载 WASM 了解如何预加载 WASM。

首先,从库中导入默认模块,然后用一个对象调用它,你只需要设置一个参数 locateFile,这是一个返回 WASM 文件 URI 的函数。这可以是 unpkg URL 或指向你自托管版本的 URI。只需 await 调用解析,然后你将获得低级 Rive 运行时 API 的引用。

import RiveCanvas from '@rive-app/canvas-advanced';

async function main() {
const rive = await RiveCanvas({
locateFile: (_) => 'https://unpkg.com/@rive-app/[email protected]/rive.wasm'
});
}
main();

创建渲染器

一旦 WASM 加载完成,下一步是使用 makeRenderer() API 创建渲染器,并传入 Rive 应渲染的 canvas 元素。渲染器使用渲染上下文将 Rive 绘制到 <canvas> 元素上。如果你使用 @rive-app/canvas-advanced,它将创建 Canvas2D 渲染上下文。如果你使用 @rive-app/webgl2-advanced,它将创建 WebGL 渲染上下文。

const canvas = document.getElementById('your-canvas-element');
const renderer = rive.makeRenderer(canvas);

加载 Rive 文件

创建渲染器后,你也可以开始将 Rive 文件作为 ArrayBuffer 加载,并将其馈入运行时的 load() API。你可以从 URL 或项目中的某处获取。

const bytes = await (
await fetch(new Request('basketball.riv'))
).arrayBuffer();

// 从 Rive 依赖中以命名导入方式导入 File
const file = (await rive.load(new Uint8Array(bytes))) as File;

确保 await .load() 调用,因为它会同步尝试从 File 加载资源。此外,在将 ArrayBuffer 作为参数传递给 .load() 之前,先将其传递给 Uint8Array 视图。

设置实例

一旦你有了加载的 File 对象的引用,你可以开始从 Rive 文件中实例化所有画板、状态机和线性动画。实例化会创建一个底层 CPP 引用,并允许你控制每个实体如何随时间推进。更多内容见本指南下文。

你可能想要实例化的主要组件是:

  • Artboard - 从 Rive 文件中实例化 1 个或多个你想要绘制的画板
  • StateMachineInstance - 从给定画板实例化一个状态机
  • LinearAnimationInstance - 从给定画板实例化一个单个时间线动画

首先实例化一个画板,然后可以从画板引用创建状态机和线性动画实例,如下所示。

const artboard = file.artboardByName('New Artboard');
const animation = new rive.LinearAnimationInstnace(
artboard.animationByName('idle'),
artboard
);
const stateMachine = new rive.StateMachineInstance(
artboard.stateMachineByName('your-state-machine-name'),
artboard
);

这里的好处是,如果你想在 canvas 上显示多个画板甚至是同一画板的副本,你可以轻松做到(相对于高级 API,它一次只显示一个)。

除了实例化渲染循环的相关部分,你还可以提取绘制层级中的节点、目标和骨骼的引用。如果你需要跟踪给定节点上的任何变换属性值以进行任何计算,或者获取世界空间或父级变换(例如,跟踪动画生命周期中节点的 x、y 坐标或旋转值),这非常有用。参见指南底部的示例以查看此操作。

构建渲染循环

你可能熟悉使用 requestAnimationFrame (rAF) 构建渲染循环,以在浏览器的重绘周期之间逐帧构建动画。如果不熟悉,请查看此指南作为构建渲染循环的起点。

在 Rive 渲染循环的情况下,你将使用包装 rAF 的自定义 Rive API,因此你需要使用 rive.requestAnimationFrame() 以及 rive.cancelAnimationFrame()。结构应类似于你为其他动画构建的任何 rAF 循环,但你将推进上面创建的实例,并根据需要将画板与 canvas 对齐。

首先创建 rAF 循环的回调循环,并跟踪自上次 rAF 回调以来的最后时间以获取经过时间(秒)。然后,使用渲染器的 .clear() API 清除 canvas。

let lastTime = 0;
function renderLoop(time) {
if (!lastTime) {
lastTime = time;
}
const elapsedTimeMs = time - lastTime;
const elapsedTimeSec = elapsedTimeMs / 1000;
lastTime = time;

renderer.clear();

...

rive.requestAnimationFrame(renderLoop);
}
rive.requestAnimationFrame(renderLoop);

推进动画

LinearAnimationInstance 有一组关键帧应用于画板中的对象。在渲染循环中,你需要对创建的动画实例调用 .advance() 来获取这些关键帧,并如 API 名称所示,将动画推进一定的时间量(以秒为单位)。

通常,你需要按上文计算的经过时间来推进动画,以「正常」速度播放(或者更确切地说,以该时间线动画设置的任何速度播放)。使用低级 API,通过控制渲染循环,你可以以自定义时间值推进实例,例如一半的经过时间(以 0.5 倍速度播放动画)或甚至两倍的经过时间(以 2 倍速度播放动画)。你甚至可以将经过时间乘以 -1 来反向运行动画方向。

除了推进线性动画外,还需要使用 .apply() 调用将该动画的关键帧值应用于画板中相关对象的属性,并指定动画的混合值。当动画应用关键帧的插值值时,它会将这些值与画板对象上的当前值混合。这允许你「混合」到动画中,这在你有两个动画实例在对象的同一属性上应用关键帧值时很有帮助。替换旧属性值为新关键帧值的默认混合值应为 1

在将动画值应用于画板后,推进画板(详见下文)以更新画板的对象并解决属性值更改。

总而言之,推进线性动画的操作顺序如下:

advance animation -> apply animation values -> advance artboard

参见下面的片段示例:

function renderLoop(time) {
if (!lastTime) {
lastTime = time;
}
const elapsedTimeMs = time - lastTime;
const elapsedTimeSec = elapsedTimeMs / 1000;
lastTime = time;

renderer.clear();
animation.advance(elapsedTimeSec);
animation.apply(1);
artboard.advance(elapsedTimeSec);
}

推进状态机

StateMachineInstance 类似于上述的 LinearAnimationInstance 流程,但有一些不同。对于状态机,你不需要应用混合值,因为你应该只有一个与画板相关的状态机实例,并且混合值由时间线动画之间设置的过渡决定。此外,.advance() 方法更新画板上的对象属性。因此,推进状态机的操作顺序简化为:

advance state machine -> advance artboard

参见下面的片段示例:

function renderLoop(time) {
if (!lastTime) {
lastTime = time;
}
const elapsedTimeMs = time - lastTime;
const elapsedTimeSec = elapsedTimeMs / 1000;
lastTime = time;

renderer.clear();
stateMachine.advance(elapsedTimeSec);
artboard.advance(elapsedTimeSec);
}

推进画板

如上所示,推进画板将在通过动画和/或状态机应用值后,进行更新层级中相关对象的工作。如果你同时控制多个动画,你只需要在渲染循环中推进画板一次。如果你在 canvas 中为场景控制多个画板,则在渲染循环中根据需要推进每个画板。

对齐和渲染

渲染循环中最后需要考虑的是设置画板的对齐方式,设置绘制区域和画板的边界,然后将渲染上下文传递给画板,以便画板在 canvas 中绘制。

在推进画板之后,调用渲染上下文的 save() API 来保存 canvas 的状态。然后调用上下文的 align() API 提供:

  1. FitAlignment
  2. 要绘制的 canvas 空间边界
  3. 在该空间内绘制的 Rive 内容边界

参见此处了解 FitAlignment 的选项。对于后两个参数,提供轴对齐边界框 (AABB)。参见下面的片段了解 align() API 的示例。

最后,在调用 align() API 之后,通过 draw() 方法将渲染器传递给画板以在 canvas 上绘制画板,然后以调用渲染器的 restore() API 结束,以恢复 canvas 的已保存状态。

如果你使用 @rive-app/webgl2-advanced,则需要调用 renderer.flush() 来清空不同的缓冲区命令。

最后要做的是使用 Rive 的 requestAnimationFrame 与此回调来为下一帧排队下一个回调。

组合起来,如下所示:

function renderLoop(time) {
if (!lastTime) {
lastTime = time;
}
const elapsedTimeMs = time - lastTime;
const elapsedTimeSec = elapsedTimeMs / 1000;
lastTime = time;

...

renderer.clear();
stateMachine.advance(elapsedTimeSec);
artboard.advance(elapsedTimeSec);
renderer.save();
renderer.align(
rive.Fit.contain,
rive.Alignment.center,
{
minX: 0,
minY: 0,
maxX: canvas.width,
maxY: canvas.height
},
artboard.bounds,
);
artboard.draw(renderer);
renderer.restore();
// 如果使用 WebGL 可选调用以下内容
// renderer.flush()
rive.requestAnimationFrame(renderLoop);
}
rive.requestAnimationFrame(renderLoop);

此时,你应该能够在 canvas 上渲染 Rive 了!

清理实例

对于每个已创建的 CPP 实例,当你完成时需要删除它们,以免在应用中产生内存泄漏。不幸的是,这是一个手动操作,因为我们还不能依赖浏览器中新的 finalizer API 被调用进行垃圾回收。当不再需要时,对从 Rive 运行时创建的任何实例调用 .delete() API。示例如下:

// 已创建的实例
const renderer = rive.makeRenderer(canvas);
const bytes = await (
await fetch(new Request('basketball.riv'))
).arrayBuffer();
const file = (await rive.load(new Uint8Array(bytes))) as File;
const artboard = file.artboardByName('New Artboard');
const animation = new rive.LinearAnimationInstnace(
artboard.animationByName('idle'),
artboard
);
const stateMachine = new rive.StateMachineInstance(
artboard.stateMachineByName('your-state-machine-name'),
artboard
);

...

renderer.delete();
file.delete();
artboard.delete();
animation.delete();
stateMachine.delete();

示例

请参见以下链接,了解演示低级 JS API 使用的示例:

API 参考

请参见我们的类型文件了解高级 API,以理解 API 签名和返回类型。

注意事项

高级 JS 运行时 API 是使用上面指定的低级 API 构建的。同时,高级 JS 运行时具有额外的便利功能,使用户能够轻松执行以下操作:

  • 通过 .play().pause().stop() 等 API 简化播放控制
  • onStateChangeonLoad 等回调,允许你挂载到特定的 Rive 生命周期事件
  • 将手势事件挂载到 Rive 监听器

当使用 Rive 的高级 JS API 自定义你使用 Rive 的方式时,你需要自行设置这些便利功能中的一些。查看高级 Rive API 是如何构建的以了解如何在你需要时复现其中一些高级便利功能。

将 Rive 集成到现有 rAF 循环

如果你希望将 Rive 添加到你现有的渲染循环(JS API requestAnimationFrame())中,并且不想使用 Rive 包装的 requestAnimationFrame() API,你可以在渲染循环结束时添加一个额外的 API 调用。在再次调用 requestAnimationFrame() 之前,在渲染循环结束时调用 rive.resolveAnimationFrame() API。