This is supposed to show an animation! If you're viewing this in your email, please view this post in a browser.

Here at Rive Engineering our focus is to build Rive’s editor and create a runtime ecosystem that plays animations across a multitude of devices and operating systems, while keeping performance up and size down.

In the past, our runtimes ran atop Skia, an excellent and feature-rich rendering engine. Using such a comprehensive platform came at the price of large runtime sizes, typically running over a megabyte. This was a significant price to pay on platforms sensitive to space and bandwidth usage.

So for Rive 2, we took the opportunity to tackle this in two ways. The first was to redesign our animation files; we created a new binary format and used concepts such as LEB128 to squeeze data into less space. We didn’t just optimize for size; we wanted to strike a balance between that and simple data parsing. We think a nice balance was struck between the two, and we’re still tweaking the format to save more bytes.

The second was to relax the requirement for Skia and support multiple rendering systems. We reasoned that this would give a choice of renderers tailored to each platform, and provide an opportunity to have light-weight and feature-rich rendering options. For example, on the Web simple animations could be rendered using Context 2D, while complex animations involving shaders could use CanvasKit.

We don’t want to maintain separate codebases for every renderer and platform, so we experimented with an animation engine written in C++, to provide a low level, compact, and highly optimized foundation for platform and renderer-specific implementations.

Once the C++ engine was in place, the first platform we wanted to target was the Web. It would be challenging to get right given the Web’s diversity, and the most ubiquitous for those who wanted to embed animations in their web apps and games.

Wasm seemed like a perfect fit for our C++ engine. We wrote a small binding layer for Javascript interop and built it with Emscripten. This now forms the basis for our web package, available on NPM.

Results have been very promising: currently our Wasm code is 37kB, Javascript code is 14 kB, and most of our example animation files are in the low 10’s kB (gzipped). Embedding a Rive animation on a site is typically under 100kB. The runtime size will increase as more features are added, but this should not be significant and we’re still experimenting with how to compile and optimize Wasm/JS.

Our web runtime is a low-level package; you need to create a canvas, a context, and control the primary animation loop. This is by design, as it gives web developers complete control over how animations play and mix. It’s easy to wrap this in a higher-level API; for example, our beta website wraps this in a React component.

Playing Rive animations on the Web

Let’s dive into specifics and see how to embed a Rive animation on the Web. We’re going to use this animation, a funky steam-powered progress indicator. Feel free to use this (and other animations) in your own projects.

This is supposed to show an animation! If you're viewing this in your email, please view this post in a browser.

Here’s the code for playing an animation that loops:

<html>

<head>
  <title>Rive Canvas Example</title>
  <link rel="stylesheet" href="/stylesheets/style.css">
</head>

<body>
  <h1>Rive Context 2D Example</h1>
  <p>For more info, check out the <a href="https://www.npmjs.com/package/rive-canvas"> Rive web canvas package</a>.</p>
  <canvas id="riveCanvas" width=512 height=512></canvas>
</body>

<script src="https://unpkg.com/rive-canvas@0.0.10/rive.js"></script>

<!-- Simple script to run a looping animation -->
<script>
  ; (function () {
    // first, instantiate the Rive engine and load the WASM file(s)
    Rive({
      locateFile: (file) => 'https://unpkg.com/rive-canvas@0.0.10/' + file,
    }).then((rive) => {
      // Rive's ready to rock 'n roll
      // Let's load up a Rive animation file, typically ending in '.riv'
      const req = new Request('./rive/loader.riv');
      fetch(req).then((res) => {
        return res.arrayBuffer();
      }).then((buf) => {
        // we've got the raw bytes of the animation, let's load them into a Rive
        // file
        const file = rive.load(new Uint8Array(buf));
        // get the default artboard, where the animations we want to interact
        // with live in this file
        const artboard = file.defaultArtboard();
        // now we can access the animations; let's get one called 'vibration'
        const vibrationAnim = artboard.animation('vibration');
        const vibrationInstance = new rive.LinearAnimationInstance(vibrationAnim);
        // let's grab our canvas
        const canvas = document.getElementById('riveCanvas');
        const ctx = canvas.getContext('2d');
        // nw we can create a Rive renderer and wire it up to our 2D context
        const renderer = new rive.CanvasRenderer(ctx);
        // advance the artboard to render a frame
        artboard.advance(0);
        // Let's make sure our frame fits into our canvas
        ctx.save();
        renderer.align(rive.Fit.contain, rive.Alignment.center, {
          minX: 0,
          minY: 0,
          maxX: canvas.width,
          maxY: canvas.height
        }, artboard.bounds);
        // and now we can draw our frame to our canvas
        artboard.draw(renderer);
        ctx.restore();

        // track the last time a frame was rendered
        let lastTime = 0;

        // okay, so we have an animation and a renderer; how do we play an
        // animation? First, let's set up our animation loop with
        // requestFrameAnimation
        function draw(time) {
          // work out how many seconds have passed since a previous frame was
          // drawn
          if (!lastTime) {
            lastTime = time;
          }
          const elapsedTime = (time - lastTime) / 1000;
          lastTime = time;

          // advance our animation by the elapsed time
          vibrationInstance.advance(elapsedTime);
          // apply the animation to the artboard 
          vibrationInstance.apply(artboard, 1.0);
          // advance the artboard
          artboard.advance(elapsedTime);

          // render the animation frame
          // first, clear the canvas
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          // let's resize it to fit the canvas
          ctx.save();
          renderer.align(rive.Fit.contain, rive.Alignment.center, {
            minX: 0,
            minY: 0,
            maxX: canvas.width,
            maxY: canvas.height
          }, artboard.bounds);
          // and now we can draw our frame to our canvas
          artboard.draw(renderer);
          ctx.restore();

          // and kick off the next frame
          requestAnimationFrame(draw);
        }
        // now kick off the animation
        requestAnimationFrame(draw);
      });
    });
  })();
</script>

</html>

Let’s break this down step by step. First, we need to import the runtime package and load the Wasm:

<script src="https://unpkg.com/rive-canvas@0.0.10/rive.js">
</script>
…
Rive({
  locateFile: (file) => 'https://unpkg.com/rive-canvas@0.0.10/' + file,
}).then((rive) => { … 

With that in place, we can load in our Rive (.riv) animation file:

const req = new Request('./rive/loader.riv');
  fetch(req).then((res) => {
    return res.arrayBuffer();
  }).then((buf) => {
    const file = rive.load(new Uint8Array(buf));

Everything is now in place to access the animation. In Rive, animations are tied to artboards. A Rive file can have multiple artboards, but in the case of our example, there’s only one:

const artboard = file.defaultArtboard();

Artboards hold references to animations; our example file contains a few and here we’ll use the lopping vibration animation. Let’s access and create an instance of it:

const vibrationAnim = artboard.animation('vibration');
const vibrationInstance = new rive.LinearAnimationInstance(vibrationAnim);

An animation instance is useful if you want Rive to manage state, which is its time and direction. Rive manages looping via the instance. If you want to control the animation's time manually, then an instance isn't necessary; you can do this directly with the animation object.

We’re now ready to display and run the animation; the runtime needs a canvas and context for our renderer:

const canvas = document.getElementById('riveCanvas');
const ctx = canvas.getContext('2d');
const renderer = new rive.CanvasRenderer(ctx);

Before we begin animating, let’s look at how to render a single frame of animation. The artboard is advanced to trigger the renderer to create a frame. It advances in seconds; here we can advance it by zero time to trigger rendering:

artboard.advance(0);

The renderer has different fit and align options available to it. We’ll center and fit it to the canvas:

ctx.save();
renderer.align(rive.Fit.contain, rive.Alignment.center, {
  minX: 0,
  minY: 0,
  maxX: canvas.width,
  maxY: canvas.height
}, artboard.bounds);
artboard.draw(renderer);
ctx.restore();

We now have the first frame rendered to our canvas. To run the animation, we’ll create our own animation loop.

The recommended method for creating a rendering loop in Javascript is to use requestAnimationFrame. MDN has great content about this, so we’ll skip some details here, and focus instead on how the animation is driven within this loop:

let lastTime = 0;
function draw(time) {
  if (!lastTime) {
    lastTime = time;
  }
  const elapsedTime = (time - lastTime) / 1000;
  lastTime = time;

  vibrationInstance.advance(elapsedTime); 
  vibrationInstance.apply(artboard, 1.0);
  artboard.advance(elapsedTime);

  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.save();
  renderer.align(rive.Fit.contain, rive.Alignment.center, {
    minX: 0,
    minY: 0,
    maxX: canvas.width,
    maxY: canvas.height
  }, artboard.bounds);
  artboard.draw(renderer);
  ctx.restore();

  requestAnimationFrame(draw);
}

Most of this code handles calculating the loop’s timing. Through each iteration of the loop, we must advance the animation, apply it to the artboard, and then advance the artboard.

It’s helpful to understand the difference between an animation and an artboard. An animation is a set of keyframes that can be applied to a set of objects in an artboard. The animation instance tracks the keyframes as they change over time.

Advancing an animation instance updates the keyframe state and these changes are applied to the artboard. Advancing the artboard updates the objects with these changes.

Doing things in this fashion allows multiple animations to update (at potentially different time steps), and have their changes applied to a single artboard, which can then mix and resolve the changes, blending animation states together.

All that’s left to do is to render and draw the artboard as we did before. With this in place, we have our animation rendering nicely at 60fps in most browsers.

Programmatically controlling animations

This is supposed to show an animation! If you're viewing this in your email, please view this post in a browser.
Slide me!

The Rive runtimes are designed to provide fine-grained control of animation playback and mixing. This next example looks at controlling the timing and position of an animation.

<html>

<head>
  <title>Rive Canvas Example</title>
  <link rel="stylesheet" href="/stylesheets/style.css">
</head>

<body>
  <h1>Rive Context 2D Example</h1>
  <p>For more info, check out the <a href="https://www.npmjs.com/package/rive-canvas"> Rive web canvas package</a>.</p>
  <div class="slider">
    <input type="range" min="0" max="100" value="0" class="slider" id="sliderRange">
  </div>
  <canvas id="riveCanvas" width=512 height=512></canvas>
</body>

<script src="https://unpkg.com/rive-canvas@0.0.10/rive.js"></script>

<!-- control the time position of an animation -->
<script>
  ; (function () {
    // first, instantiate the Rive engine and load the WASM file(s)
    Rive({
      locateFile: (file) => 'https://unpkg.com/rive-canvas@0.0.10/' + file,
    }).then((rive) => {
      // Rive's ready to rock 'n roll
      // Let's load up a Rive animation file, typically ending in '.riv'
      const req = new Request('./rive/loader.riv');
      fetch(req).then((res) => {
        return res.arrayBuffer();
      }).then((buf) => {
        // we've got the raw bytes of the animation, let's load them into a Rive
        // file
        const file = rive.load(new Uint8Array(buf));
        // get the default artboard, where the animations we want to interact
        // with live in this file
        const artboard = file.defaultArtboard();
        // now we can access the animations; let's get one called 'vibration'
        const loadAnim = artboard.animation('load');
        const loadInstance = new rive.LinearAnimationInstance(loadAnim);
        // let's grab our canvas
        const canvas = document.getElementById('riveCanvas');
        const ctx = canvas.getContext('2d');
        // nw we can create a Rive renderer and wire it up to our 2D context
        const renderer = new rive.CanvasRenderer(ctx);
        // advance the artboard to render a frame
        artboard.advance(0);
        // Let's make sure our frame fits into our canvas
        ctx.save();
        renderer.align(rive.Fit.contain, rive.Alignment.center, {
          minX: 0,
          minY: 0,
          maxX: canvas.width,
          maxY: canvas.height
        }, artboard.bounds);
        // and now we can draw our frame to our canvas
        artboard.draw(renderer);
        ctx.restore();

        // let's grab our slider and listen for changes
        let slider = document.getElementById('sliderRange');
        let sliderValue = slider.value;
        slider.oninput = function () {
          // normalize the slider value to be between 0 and 1
          sliderValue = this.value / 100;
        }

        // track the last time a frame was rendered
        let lastTime = 0;

        // okay, so we have an animation and a renderer; how do we play an
        // animation? First, let's set up our animation loop with
        // requestFrameAnimation
        function draw(time) {
          // work out how many seconds have passed since a previous frame was
          // drawn
          if (!lastTime) {
            lastTime = time;
          }

          // Advance in the direction of the requested animation position
          const elapsedTime = (time - lastTime) / 1000 * 0.5;
          lastTime = time;
          // check what time point the animation is currently at
          const animationPosTime = loadInstance.time;

          // determine which direction in which to animate based on the current
          // slider position; positive is forward, negative is backwards, zero is don't advance
          let direction = 0;
          if (Math.abs(animationPosTime - sliderValue) > 0.01) {
            direction = animationPosTime < sliderValue ? 1 : -1;
          }

          // advance our animation by the elapsed time
          loadInstance.advance(elapsedTime * direction);
          // apply the animation to the artboard 
          loadInstance.apply(artboard, 1);

          // advance the artboard
          artboard.advance(elapsedTime * direction);

          // render the animation frame
          // first, clear the canvas
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          // let's resize it to fit the canvas
          ctx.save();
          renderer.align(rive.Fit.contain, rive.Alignment.center, {
            minX: 0,
            minY: 0,
            maxX: canvas.width,
            maxY: canvas.height
          }, artboard.bounds);
          // and now we can draw our frame to our canvas
          artboard.draw(renderer);
          ctx.restore();

          // and kick off the next frame
          requestAnimationFrame(draw);
        }
        // now kick off the animation
        requestAnimationFrame(draw);
      });
    });
  })();
</script>

</html>

Our steam-powered progress indicator contains an animation called load. By manipulating the elapsed time passed to the animation and knowing the animation’s length, it’s straightforward to drive the animation forwards and backwards. When time is updated, Rive will animate to the new position smoothly and the speed of the animation can be controlled by transforming the time value.

In our example, an animation has been hooked up to a slider that dictates the time step to which Rive should animate. The slider value is normalized to between 0 and 1 to mimic progress from 0 to 100.

let slider = document.getElementById('sliderRange');
let sliderValue = slider.value;
slider.oninput = function () {
  sliderValue = this.value / 100;
}

We can control the direction and current position of the animation by manipulating the time applied to it:

const elapsedTime = (time - lastTime) / 1000 * 0.5;
lastTime = time;
const animationPosTime = loadInstance.time;

let direction = 0;
if (Math.abs(animationPosTime - sliderValue) > 0.01) {
  direction = animationPosTime < sliderValue ? 1 : -1;
}

loadInstance.advance(elapsedTime * direction);
loadInstance.apply(artboard, 1);
artboard.advance(elapsedTime * direction);

We control the speed of the animation by playing with the animation loop’s elapsed time. In this case, we’re slowing the animation by 50%:

const elapsedTime = (time - lastTime) / 1000 * 0.5;

We have the animation’s current time:

const animationPosTime = loadInstance.time;

and now we put these three pieces together: the desired time position, the current time position, and the speed at which to animate. We can work out the direction to animate and apply the time delta.

We apply a small buffer of time (0.01s) to prevent jitter.

let direction = 0;
if (Math.abs(animationPosTime - sliderValue) > 0.01) {
  direction = animationPosTime < sliderValue ? 1 : -1;
}

loadInstance.advance(elapsedTime * direction);
loadInstance.apply(artboard, 1);
artboard.advance(elapsedTime * direction);

This is a fine-grained method of controlling an animation, but that’s by design. It provides flexible control over animation playback, and can easily be wrapped in a higher level library.

Mixing animations

This is supposed to show an animation! If you're viewing this in your email, please view this post in a browser.

Finally, let’s take a quick look at mixing multiple animations together:

<html>

<head>
  <title>Rive Canvas Example</title>
  <link rel="stylesheet" href="/stylesheets/style.css">
</head>

<body>
  <h1>Rive Context 2D Example</h1>
  <p>For more info, check out the <a href="https://www.npmjs.com/package/rive-canvas"> Rive web canvas package</a>.</p>
  <canvas id="riveCanvas" width=512 height=512></canvas>
</body>

<script src="https://unpkg.com/rive-canvas@0.0.10/rive.js"></script>

<!-- Mix multiple animations-->
<script>
    ; (function () {
      // first, instantiate the Rive engine and load the WASM file(s)
      Rive({
        locateFile: (file) => 'https://unpkg.com/rive-canvas@0.0.10/' + file,
      }).then((rive) => {
        // Rive's ready to rock 'n roll
        // Let's load up a Rive animation file, typically ending in '.riv'
        const req = new Request('./rive/loader.riv');
        fetch(req).then((res) => {
          return res.arrayBuffer();
        }).then((buf) => {
          // we've got the raw bytes of the animation, let's load them into a Rive
          // file
          const file = rive.load(new Uint8Array(buf));
          // get the default artboard, where the animations we want to interact
          // with live in this file
          const artboard = file.defaultArtboard();
          // now we can access the animations; let's get one called 'vibration'
          const vibrationAnim = artboard.animation('vibration');
          const vibrationInstance = new rive.LinearAnimationInstance(vibrationAnim);
          // let's get another one called 'load'
          const loadAnim = artboard.animation('load');
          const loadInstance = new rive.LinearAnimationInstance(loadAnim);
          // and another one called 'end'
          const endAnim = artboard.animation('end');
          const endInstance = new rive.LinearAnimationInstance(endAnim);
  
          // let's grab our canvas
          const canvas = document.getElementById('riveCanvas');
          const ctx = canvas.getContext('2d');
          // nw we can create a Rive renderer and wire it up to our 2D context
          const renderer = new rive.CanvasRenderer(ctx);
          // advance the artboard to render a frame
          artboard.advance(0);
          // Let's make sure our frame fits into our canvas
          ctx.save();
          renderer.align(rive.Fit.contain, rive.Alignment.center, {
            minX: 0,
            minY: 0,
            maxX: canvas.width,
            maxY: canvas.height
          }, artboard.bounds);
          // and now we can draw our frame to our canvas
          artboard.draw(renderer);
          ctx.restore();
  
          // track the last time a frame was rendered
          let lastTime = 0;
  
          // okay, so we have an animation and a renderer; how do we play an
          // animation? First, let's set up our animation loop with
          // requestFrameAnimation
          function draw(time) {
            // work out how many seconds have passed since a previous frame was
            // drawn
            if (!lastTime) {
              lastTime = time;
            }
            const elapsedTime = (time - lastTime) / 1000;
            lastTime = time;
  
            // If we're getting close to completing loading, then start vibrating
            if (loadInstance.time > 0.8 && loadInstance.time < 1.0) {
              console.log('Playing loading animation');
              vibrationInstance.advance(elapsedTime);
              vibrationInstance.apply(artboard, 1.0);
  
            }
            // When we get all the ways to the top, play the end animation
            if (loadInstance.time < 1.0) {
              loadInstance.advance(elapsedTime * 0.2);
              loadInstance.apply(artboard, 1.0);
  
            } else {
              console.log('Playing end animation');
              endInstance.advance(elapsedTime);
              endInstance.apply(artboard, 1.0);
            }
  
            // advance the artboard
            artboard.advance(elapsedTime);
  
            // render the animation frame
            // first, clear the canvas
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            // let's resize it to fit the canvas
            ctx.save();
            renderer.align(rive.Fit.contain, rive.Alignment.center, {
              minX: 0,
              minY: 0,
              maxX: canvas.width,
              maxY: canvas.height
            }, artboard.bounds);
            // and now we can draw our frame to our canvas
            artboard.draw(renderer);
            ctx.restore();
  
            // and kick off the next frame
            requestAnimationFrame(draw);
          }
          // now kick off the animation
          requestAnimationFrame(draw);
        });
      });
    })();
  </script>

</html>

Three animations are instanced: load, vibration, and end.

We play and mix load and vibration together at different stages in time as an example progress indicator, which we’re playing at one-fifth speed. Once progress is complete, we play the end animation.

if (loadInstance.time < 1.0) {
 loadInstance.advance(elapsedTime * 0.2);
 loadInstance.apply(artboard, 1.0);
} else {
 endInstance.advance(elapsedTime);
 endInstance.apply(artboard, 1.0);
}
if (loadInstance.time > 0.5 && loadInstance.time < 1.0) {
 vibrationInstance.advance(elapsedTime);
 vibrationInstance.apply(artboard, loadInstance.time);
}

When our load animation is past the halfway point, we begin mixing it with the vibration animation.The degree to which each animation mixes is set when applying them to the artboard; in this case, the vibration animation gains in strength the closer progress gets to complete (we reuse the load instance time here for convenience). Multiple animations can be mixed in this way.

Hopefully this will get you started mixing and playing Rive animations on the Web. We plan to add more features to the Wasm runtime in the future, and look forward to seeing what you create!