October 2024
Scroll animations with Rive
Rive is a great tool for embedding animations on a website. I recently wanted to bind the progress of a rive animation to the scroll of a page, kinda like you see above.
The file setup part is almost as important as the code itself, as I haven't found a great way to just use the timeline of a single animation, and instead rely on animation based on an input variable.
File Setup
In order to properly animate the rive file in the browser, we need to be able to control it somehow. This is done through the use of variables in the rive file. I'll explain it based on a simple example of a radio with a frequency dial.
4. Select Timeline 1 and set it to "Blend 1D"
Select Timeline 1
in the state machine and set it to Blend 1D
in the bottom right panel. This will allow you to blend between the initial state and the animated state of Timeline 2. In the Timelines
section, add both timelines and set one to 100, like shown in the screenshot above.
5. Try out the animation
If you press play, you should now be able to set the progress of your animation using the input variable you created.
Code
I've originally implemented this in an Astro site, so the code is just regular JS without any framework.
First, you'll need to create a canvas element somewhere. Make sure to match the width and height of the canvas to the artboard size of your rive file.
<!-- index.html -->
<canvas id="rive-canvas" width="798" height="436"></canvas>
Then, we'll create a script which does the following:
- Load the Rive renderer
- Create a rive instance for the animation file
- sync it to the scroll position
- add a throttled window resize listener to update the canvas resolution without too much lag
Also, I'm using the canvas-lite version of rive to reduce the file size. When using vite, you'll need to add ?url
to the rive wasm file import. Here's the full code:
Click to show the raw JS code.
// index.ts
import riveWASMResource from "@rive-app/canvas-lite/rive.wasm?url";
import {
Rive,
StateMachineInput,
RuntimeLoader,
} from "@rive-app/canvas-lite";
RuntimeLoader.setWasmUrl(riveWASMResource);
let stateMachineLoadInput: StateMachineInput;
const canvas = document.getElementById("rive-canvas");
if (!canvas || !(canvas instanceof HTMLCanvasElement))
throw new Error("canvas element not found");
// 🚨 make sure to change the artboard and state machine names to match your rive file
const r = new Rive({
src: "/player.riv",
canvas: canvas,
autoplay: true,
stateMachines: "State Machine 1",
artboard: "Artboard",
onLoad: () => {
stateMachineLoadInput = r.stateMachineInputs("State Machine 1")[0];
stateMachineLoadInput.value = 0;
r.resizeDrawingSurfaceToCanvas();
},
});
// listening to window resize events to resize the canvas,
// without this, the canvas will be blurry when resizing the window
let resizeTimeout: any | null = null;
window.addEventListener(
"resize",
() => {
if (resizeTimeout === null) {
resizeTimeout = setTimeout(() => {
r.resizeDrawingSurfaceToCanvas();
resizeTimeout = null;
}, 200);
}
},
{ passive: true }
);
// this part actually does the scroll syncing. If here you can change
// the window event listener to anything else, for example
// an intersection observer, mouse movement, etc.
window.addEventListener(
"scroll",
() => {
if (!stateMachineLoadInput) return;
const scrollPercentage =
(window.scrollY /
(document.documentElement.scrollHeight - window.innerHeight)) *
100;
stateMachineLoadInput.value = scrollPercentage;
},
{ passive: true }
);
You will probably have to change the loading of the rive player to fit your framework / bundler, as well as the rive file import. Also, make sure to check if the state machine and artboard names match the ones in your rive file.
I've also included a React Example:
Click to show the React component code.
import React, { useEffect } from "react";
import { useRef } from "react";
import riveWASMResource from "@rive-app/canvas-lite/rive.wasm";
import { Rive, StateMachineInput, RuntimeLoader } from "@rive-app/canvas-lite";
import riveFile from "./t3.riv";
const RiveScroll = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
RuntimeLoader.setWasmUrl(riveWASMResource);
let stateMachineLoadInput: StateMachineInput;
if (!canvasRef.current) throw new Error("canvas element not found");
// 🚨 make sure to change the artboard and state machine names to match your rive file
const r = new Rive({
src: riveFile,
canvas: canvasRef.current,
autoplay: true,
stateMachines: "State Machine 1",
artboard: "Artboard",
onLoad: () => {
stateMachineLoadInput = r.stateMachineInputs("State Machine 1")[0];
stateMachineLoadInput.value = 0;
r.resizeDrawingSurfaceToCanvas();
},
});
let resizeTimeout: any | null = null;
const onResize = () => {
if (resizeTimeout === null) {
resizeTimeout = setTimeout(() => {
r.resizeDrawingSurfaceToCanvas();
resizeTimeout = null;
}, 200);
}
};
window.addEventListener("resize", onResize, { passive: true });
const onScroll = () => {
if (!stateMachineLoadInput) return;
const scrollPercentage = (window.scrollY / window.innerHeight) * 100;
stateMachineLoadInput.value = scrollPercentage;
};
window.addEventListener("scroll", onScroll, { passive: true });
return () => {
window.removeEventListener("resize", onResize);
window.removeEventListener("scroll", onScroll);
};
}, [canvasRef.current]);
return (
<div>
<canvas
style={{
width: "100%",
}}
ref={canvasRef}
id="rive-canvas"
width="798"
height="436"
/>
</div>
);
};
export default RiveScroll;
Again, importing the rive wasm and .riv file depends on your bundler, e.g. for webpack you might need to change your config to load the wasm file using the file-loader
.
That's it! You should now have a working scroll-linked animation using Rive. Thanks for reading, I hope this was helpful!