在正式開始介紹 Rive Low-level API 之前,我們要先名詞解釋一下,目前我們看到的 Rive 語法 aka Rive 官方提供給我們使用的 API,叫做 High-level API。顧名思義,他們的語法比較簡單易懂,適合初學者與輕量使用者使用,在通常的情況下,High-level API 就非常夠用了。
但 High-level API 還是有一些缺點,例如每一個 .riv 檔都要對應到一個 元素,性能上比較差一點。而且他的操控性比較差,比較難做到快轉、倒轉、條件渲染、列表渲染等進階的需求。因此 Rive 官方另外開了一組操控性比較高,但比較難使用的 API 給我們,這就是 Low-level API。
也就是說,High-level API & Low-level API 是使用 Rive 的兩種語法,可以自己選一種使用,通常除非是要做出這種等級的遊戲,否則不太需要用到 Low-level API。但如果有了解 render loop 的概念,再稍微熟悉一下語法,那 Low-level API 其實也沒有很難,所以我是覺得就算沒有用到,也可以稍微參考一下。
昨天有提到 render loop 的架構,本質上就是往 init-update-render-cleanup 四個 hook 裡面填東西而已,所以以下會根據這四個部分,依序用相關的 Ligh-level API 填東西進去。
init 是在初始化 render loop,為正式開始遞迴做準備。首先我們要安裝另一個 Rive library 叫做 canavs-advanced,接著初始化 Rive 物件、canvas 元素,以及渲染器,語法大概長這樣。
import RiveCanvasConstructor from '@rive-app/canvas-advanced'
import riveWasmUrl from '@rive-app/canvas-advanced/rive.wasm?url'
// 注意 locateFile 一定要吃一個 url 當參數
// [在 Vite 裡面可以用 ?url 引入](https://vitejs.dev/guide/assets#explicit-url-imports),其他 built tool 要再自己查一下
// https://vitejs.dev/guide/assets#explicit-url-imports
const riveCanvas = await RiveCanvasConstructor({ locateFile: () => riveWasmUrl })
const canvas = document.getElementById(options.canvasId)
const renderer = riveCanvas.makeRenderer(canvas)
接著要載入 .riv 檔,並拿出 artboard, stateMachine, animation 備用
const bytes = await (await fetch(new Request('file/path/filename.riv'))).arrayBuffer()
const file = await riveCanvas.load(new Uint8Array(bytes))
// artboardName & stateMachineName 都要請設計師給你,或是把 file 印出來慢慢找
const artboard = file.artboardByName(artboardName)
const stateMachine = new riveCanvas.StateMachineInstance(
artboard.stateMachineByName(stateMachineName),
artboard
)
const animation = new riveCanvas.LinearAnimationInstance(
artboard.animationByIndex(0),
artboard
)
成功拿到 riveCanvas, canvas, renderer 以及 artboard, stateMachine, animation 這兩組變數之後,init 階段就結束了,準備進入到下一階段。
要特別提到的是,這部分的語法非常不直覺,我是覺得 API 的命名非常糟糕以至於很難理解,所以一開始打不起來是很正常的,通常是有哪邊搞錯或漏掉了,請多試幾次。另外因為 Rive 太新,所以問 AI 通常沒什麼用,可能還有反效果,還是自己慢慢 console 比較保險。
在 update & render 之前,我們要先把 loop 的架構寫好,通常是用遞迴的 reuqestAnimaitonFrame 做出 loop。
let lastTime = 0
const loop = (time) => {
if (!lastTime) lastTime = time
const elapsedTimeSec = (time - lastTime) / 1000
lastTime = time
// update & render
riveCanvas.reuqestAnimationFrame(loop)
}
riveCanvas.reuqestAnimationFrame(loop)
做出 loop 之後,往裡面填 update 的邏輯就好,也就是那兩行 advance。
renderer.clear()
stateMachine.advance(time)
artboard.advance(time)
update 的邏輯到這邊就結束了,其實語法根本沒有這麼複雜,官方文件寫太多啦哩拉紮的東西了。
render 的邏輯也非常簡單,重點只有 draw 那一行,剩下的都不太重要。
renderer.save()
renderer.align(
riveCanvas.Fit.fitWidth,
riveCanvas.Alignment.topLeft,
{
minX: 0,
minY: 0,
maxX: canvas.width,
maxY: canvas.height
},
artboard.bounds
)
artboard.draw(renderer)
renderer.restore()
填完 init, update, render 之後,你的動畫應該要跑得起來了。但通常因為官方 API 命名跟文件真的寫得太爛,很多社群上的範例也很沒重點 3 行可以講完的東西硬要寫 30 行 87% 是印度人寫的,所以前幾次跑不起來非常正常,請保持耐心多試幾次🥹🥹
最後一步 cleanup 也很簡單,跟前面的 update & render 一樣,只要呼叫內建的 API 就好。但我們為了保險起見,也會事先把 requestAnimationFrame 的 id 存起來,在這邊再一起 cancelAnimationFrame。
if (id) riveCanvas.cancelAnimationFrame(id)
renderer.delete()
file.delete()
artboard.delete()
stateMachine.delete()
這部分就自己斟酌一下,我是覺得理論上因為我們是透過 Rive 呼叫 rAF 的,不是自己手造的輪子,所以 Rive 應該要手動幫我們處理這一塊,但還是寧願錯殺不可放過,因為太多問題都是因為沒有 cleanup 乾淨引起的了。
這應該是這 30 天中最煩人的一篇文章,因為太多瑣碎的東西了,但其實概念沒有很難,如果真的需要用 Low-level API 的話,可能要有耐心慢慢讀幾遍。
成功用 Low-level API 寫完 render loop 之後,因為某些我還無法理解為什麼官方不統一包一層乾淨整齊的 interface 的原因,State Mahcine, Rive Text, Rive Event 的語法也跟 High-level API 不太一樣,明天再一起整理給大家。