Low-level API Usage
Using low-level JS APIs to construct Rive scenes
This article is out of date! Find the new version here.
Background
While the JS runtime offers a high-level API that allows for integrating Rives into web applications quickly, the runtime also allows for a smaller advanced low-level API that allows for constructing and controlling Rive(s) in your own render loop. There are several reasons and benefits to using this lower-level API:
Construct a scene of multiple Rive files, artboards, linear animations, and state machines, all in one
<canvas>
element. This is useful if you're building a game!Control the render loop, which involves how you advance each artboard, animation, and state machine over time (including speed)
Ability to tap into several transform property values on nodes/bones in the draw hierarchy
Smaller dependency size
...and more!
See a simple game example here.
Premise
Here's the basic render workflow using the low-level API to render Rives:
Load the Rive Web Assembly (WASM) file, which contains the module with lower-level APIs
Load the Rive file in
Create instances for Artboards, LinearAnimations, and StateMachines
Build the render loop function to manipulate the instances created above
Advance any animation instances and apply it
Advance any state machine instances
Advance the artboard
Render the updated artboard on the canvas
Request the next animation frame
Clean-up created instances when finished
Getting Started
If you’ve decided that the low-level JS APIs are what you need for your app, read below for a guide on how to set everything up, or you can skip to the end to see some examples in action.
Loading in WASM
The first step to setting up the low-level Rive APIs is to load in the Rive WASM file from either the @rive-app/canvas-advanced
or @rive-app/webgl-advanced
libraries (by default, we recommend @rive-app/canvas-advanced
for a smaller dependency, unless you need to use WebGL). When the WASM file is loaded into your app, you'll gain access to necessary APIs such as the renderer for canvas/WebGL, along with relevant JS classes generated from underlying CPP bindings via rive-cpp, the core c++ runtime used as the base for several other Rive runtimes. You'll use these classes to construct your rendering scene in the canvas below.
You can load the Rive WASM file via unpkg (hosts our NPM modules for the JS runtimes), which will make a network call to the CDN, or you can choose to host the WASM file on your own servers. With unpkg
, the URL will look something like this:
You'll want to ensure that the version at the end of @rive-app/canvas-advanced@
or @rive-app/webgl-advanced@
matches the version of the dependency you installed in your app. For example, if you installed @rive-app/canvas-advanced@1.1.5
in package.json
, the Rive WASM file you request from unpkg
would be https://unpkg.com/@rive-app/canvas-advanced@1.1.5/rive.wasm
.
See docs here if you'd like to preload WASM in.
To start, import the default module from the library and then call it with an object where you only need to set a single parameter, locateFile
, which is a function that returns the URI of the WASM file. This can be either the unpkg
URL or the URI to your self-hosted version of it. Simply await
for the call to resolve, and then you'll get a reference to the low-level Rive runtime APIs.
Creating the Renderer
Once the WASM is loaded in, the next step is to create the renderer with the makeRenderer()
API and pass in the canvas element on which Rive should render. The renderer draws Rive onto the <canvas>
element with a rendering context. If you're using @rive-app/canvas-advanced
, it will create a Canvas2D rendering context. If you're using @rive-app/webgl-advanced
, it will create a WebGL rendering context.
Loading in Rive Files
After the renderer is created, you can also start to load in the Rive file(s) as an ArrayBuffer, which you'll feed into the runtime's load()
API. You can fetch this at a URL or from somewhere within your project.
Make sure to await the .load()
call, as it synchronously tries to load assets from the File
. Additionally, pass in the ArrayBuffer
to a Uint8Array
view before sending it as a param to .load()
Setting up the Instances
Once you have a reference to the loaded File
object, you can begin instancing all the artboards, state machines, and linear animations from the Rive file. Instancing creates an underlying CPP reference and allows you to control how each entity advances over time. More on that further down this guide.
The main components you will most likely want to instance are:
Artboard
- Instance 1 or more artboards from the Rive file you want to drawStateMachineInstance
- Instance a state machine from a given artboardLinearAnimationInstance
- Instance a single timeline animation from a given artboard
Start by instancing an artboard, and then you can create a state machine and linear animation instances from the artboard reference like below.
The great thing here is if you want to display multiple artboards or even copies of the same one on the canvas, you can easily do so (as opposed to the high-level API, which only displays one at a time).
Beyond instancing the relevant pieces for the render loop, you can also extract references to nodes, targets, and bones within the drawing hierarchy. This is useful if you need to track any transform property values on a given node for any calculations or even to get world-space or parent transforms (i.e., tracking the x, y-coordinate, or rotation value of a node over the lifetime of an animation). See some of the examples at the bottom of the guide to see this in action.
Constructing the Render Loop
You may be familiar with constructing a render loop using requestAnimationFrame
(rAF) to build animations frame-by-frame in between the browser's repaint cycle. If not, check out this guide as a starting point for building a render loop.
In the case of a Rive render loop, you'll be using a custom Rive API that wraps rAF, so you'll need to use rive.requestAnimationFrame()
as well as rive.cancelAnimationFrame()
. The structure should be similar to any other rAF loop you build for other animations, but you'll be advancing the instances you created above and aligning the artboard to the canvas as you see fit.
Start by creating your callback loop for the rAF cycle and tracking the last time since the previous rAF callback to get an elapsed time in seconds. Then, clear the canvas by using the renderer's .clear()
API.
Advancing Animations
A LinearAnimationInstance
has a set of keyframes to apply to objects in an artboard. In the render loop, you'll want to call .advance()
on the created animation instances to get those keyframes and, like the API is named, advance the animation by a certain amount of time (in seconds).
Normally, you would want to advance the animation by the elapsed time calculated above to playback at “normal” speed (or rather, whatever speed is set for that timeline animation). With the low-level APIs, by controlling the render loop, you can advance the instance by a custom time value, such as half the elapsed time (to playback the animation at 0.5x speed) or even twice the elapsed time (to playback the animation at 2x speed). You could even multiply the elapsed time by -1 to run the animation direction backward.
In addition to advancing a linear animation, you need to apply the keyframe values to the properties of relevant objects in the artboard for that animation and specify the animation's mix value using the .apply()
call. When the animation applies the interpolated values from the keyframes, it blends these values with the current values on the artboard objects. This allows you to "blend" into an animation, which is helpful if you have two animation instances applying a keyframe value on the same property of an object. The default mix value to replace the old property values with the new keyframe values should be 1
.
After applying an animation’s values to the artboard, advance the artboard (more on that below) to update the artboard's objects and resolve the property value changes.
To summarize all of this, the order of operations in advancing a linear animation is as follows:
See the below snippet for an example:
Advancing State Machines
A StateMachineInstance
is similar to the LinearAnimationInstance
flow above, with a few differences. With state machines, you don't need to apply a mix value since you should only have one state machine instance correlated to an artboard, and mix values are determined by the transitions set between timeline animations. Additionally, the .advance()
method updates the properties of objects on the artboard. Therefore, the order of operations for advancing a state machine is simplified to:
See the below snippet for an example:
Advancing the Artboard
As you've seen above, advancing the artboard will do the work of updating the relevant objects in the hierarchy after the values have been applied through animations and/or state machines. If you're controlling multiple animations at once, you only need to advance the artboard once in the render loop. If you're controlling multiple artboards for your scene in the canvas, advance each artboard as needed in the render loop.
Align and Render
The last bit to consider in the render loop is to set the alignment of the artboard(s), set the bounds for the drawing area and artboard, and then finally pass the rendering context to the artboard so that the artboard gets drawn in the canvas.
After advancing the artboard, call the save()
API on the rendering context to save the state of the canvas. Then call the align()
API on the context to provide:
Fit
andAlignment
valuesThe bounds of the canvas space to draw to
The bounds of the Rive content to draw within that space
See here for options for Fit
and Alignment
. For the latter two parameters, provide an axis-aligned bounding box (AABB). See the snippet below for an example of the align()
API.
Finally, after calling the align()
API, pass the renderer to the artboard via the draw()
method to draw the artboard on the canvas, then end with a call to the restore()
API on the renderer to restore the saved state of the canvas.
If you're using @rive-app/webgl-advanced
, you will need to add an additional call on the renderer to flush()
to empty different buffer commands.
The last thing to do is to call on Rive's requestAnimationFrame
with this callback to queue up the next callback for the next frame.
Altogether, this looks like the following:
At this point, you should be able to render Rive on the canvas!
Cleaning Up Instances
For each of the created CPP instances, you’ll want to delete them when you are finished so that you don’t have any memory leaks in your application. Unfortunately, this is a manual operation as we cannot yet rely on the new finalizer API in browsers to be called for garbage collection. Call the .delete()
API on any instances created from the Rive runtime when they are no longer needed. An example is shown below:
Examples
See below for links to examples demonstrating the use of low-level JS APIs:
API References
See our types file for the advanced API to understand the API signatures and return types.
Caveats
The high-level JS runtime APIs are built with the low-level APIs specified above. Along with this, the high-level JS runtime has additional affordances that make it easy for users to do some of the following things:
Ease of playback control with APIs such as
.play()
,.pause()
,.stop()
, etc.Callbacks such as
onStateChange
,onLoad
, etc. that would allow you to hook into specific Rive lifecycle eventsHooking up gesture events to Rive Listeners
When using Rive’s advanced JS APIs to customize how you use Rive, you will have to set up some of these affordances yourself. Take a look at how the high-level Rive API is built here to get a sense of how to replicate some of these high-level affordances should you need these.
Integrating Rive into Existing rAF Loop
If you're looking to add Rive to your existing render loop (the JS API requestAniationFrame()
) and do not want to use the Rive-wrapped requestAnimationFrame()
API, you can do so with an extra API call at the end of your render loop. Call the rive.resolveAnimationFrame()
API at the end of the render loop before calling requestAnimationFrame()
again.
See more on usage in the Rive Parameters doc.
Last updated