Basic Animation in WASM with Rust
14 Dec 2024Introduction
In a previous post we covered the basic setup on
drawing to a <canvas>
object via WebAssembly (WASM). In today’s article, we’ll create animated graphics directly on a
HTML5 canvas.
We’ll break down the provided code into digestible segments and walk through each part to understand how it works. By the end of this article, you’ll have a clear picture of how to:
- Set up an HTML5 canvas and interact with it using Rust and WebAssembly.
- Generate random visual effects with Rust’s
rand
crate. - Build an animation loop with
requestAnimationFrame
. - Use shared, mutable state with
Rc
andRefCell
in Rust.
Let’s get started.
Walkthrough
I won’t cover the project setup and basics here. The previous post has all of that information for you. I will cover some dependencies that you need for your project here:
There’s a number of features
in use there from web-sys
. These will become clearer as we go through the code. The
getrandom
dependency has web assembly support so
we can use this to make our animations slightly generative.
Getting Browser Access
First thing we’ll do is to define some helper functions that will try and acquire different features in the browser.
We need to be able to access the browser’s window
object.
This function requests the common window
object from the Javascript environment. The expect
will give us an error
context if it fails, telling us that no window exists.
We use this function to get access to requestAnimationFrame from the browser.
The function being requested here is documented as the callback
.
The
window.requestAnimationFrame()
method tells the browser you wish to perform an animation. It requests the browser to call a user-supplied callback function before the next repaint.
This will come in handy to do our repaints.
Now, in our run
function, we can start to access parts of the HTML document that we’ll need references for. Sitting in
our HTML template, we have the <canvas>
tag that we want access to:
We can get a handle to this <canvas>
element, along with the 2d drawing context with the following:
Create our Double-Buffer
When we double-buffer graphics, we need to allocate the block of memory that will act as our “virtual screen”. We draw to that virtual screen, and then “flip” or “blit” that virtual screen (piece of memory) onto video memory to give the graphics movement.
The size of our buffer will be width * height * number_of_bytes_per_pixel
. With a red, green, blue, and alpha channel
that makes 4 bytes.
Animation Loop
We can now setup our animation loop.
This approach allows the closure to reference itself so it can schedule the next frame, solving Rust’s strict ownership and borrowing constraints.
This pattern is common in Rust for managing shared, mutable state when working with closures in scenarios where you need to reference a value multiple times or recursively, such as with event loops or callback-based systems. Let me break it down step-by-step:
The Components
Rc
(Reference Counted Pointer):Rc
allows multiple ownership of the same data by creating a reference-counted pointer. When the last reference to the data is dropped, the data is cleaned up.- In this case, it enables both
f
andg
to share ownership of the sameRefCell
.
RefCell
(Interior Mutability):RefCell
allows mutable access to data even when it is inside an immutable container likeRc
.- This is crucial because
Rc
itself does not allow mutable access to its contents by design (to prevent race conditions in a single-threaded context).
Closure
:- A closure in Rust is a function-like construct that can capture variables from its surrounding scope.
- In the given code, a
Closure
is being stored in theRefCell
for later use.
What’s Happening Here?
- Shared Ownership:
Rc
is used to allow multiple references (f
andg
) to the same underlyingRefCell
. This is required because the closure may need to referencef
while being stored in it, which is impossible without shared ownership.
- Mutation with RefCell:
RefCell
enables modifying the underlying data (None
→Some(Closure)
) despiteRc
being immutable.
- Setting the Closure:
- The closure is created and stored in the
RefCell
via*g.borrow_mut()
. - This closure may reference
f
for recursive or repeated access.
- The closure is created and stored in the
We follow this particular pattern here because the closure needs access to itself in order to recursively schedule calls
to requestAnimationFrame
. By storing the closure in the RefCell
, the closure can call itself indirectly.
If we didn’t use this pattern, we’d have some lifetime/ownership issues. Referencing the closure while defining it would create a circular reference problem that Rust wouldn’t allow.
Drawing
We’re going to find a random point on our virtual screen to draw, and we’re going to pick a random shade of grey. We’re going to need a random number generator:
rng
is now a thread-local generator of random numbers.
We get a random location in our virtual screen, and calculate the offset o
to draw at using those values.
Now, it’s as simple as setting 4 bytes from that location:
Blitting
Blitting refers to copying pixel data from the backbuffer to the canvas in a single operation. This ensures the displayed image updates smoothly
Now we need to blit that back buffer onto our canvas. We need to create an ImageData
object in order to do this.
Passing in our backbuffer
object, we can create one with the following:
We then use our 2d context to simply draw the image:
Conclusion
And there you have it—a complete walkthrough of creating dynamic canvas animations with Rust and WebAssembly! We covered how to:
- Set up the canvas and prepare a backbuffer for pixel manipulation.
- Use Rust’s
rand
crate to generate random visual effects. - Manage mutable state with
Rc
andRefCell
for animation loops. - Leverage
requestAnimationFrame
to achieve smooth, frame-based updates.
This approach combines Rust’s strengths with the accessibility of modern web technologies, allowing you to build fast, interactive graphics directly in the browser.
A gist of the full code is also available.