The previous post was a step-by-step tutorial on how to use wasm using Rust and run it in a browser. We built a simple web page with only an input field and a button. In this post, we will make something more interesting by creating a particle system in which 1000 randomly colored particles move in random directions at varying speeds. We will draw particles on HTML Canvas with all the logic done using the Rust wasm module.
A few years ago, I created the same particle effects using JavaScript and I thought it would be a nice way to replicate this with WebAssembly using Rust.
The JavaScript version can be seen here:
See the Pen
Simple Particle System by HowToSolutions (@HowToSolutions)
on CodePen.
1) Requirements
This tutorial assumes you are somewhat familiar with creating a wasm module in Rust. If not, check the How to create a simple Rust wasm module in Windows article. But in short, we will need the following:
- We will need to have Rust installed and because we will be creating a wasm module, we also need to add a wasm target to the Rust compiler.
- Next, we need to have the wasm-binder tool installed. This tool is used to bind the JavaScript part with the Rust module by generating the necessary JavaScript files.
- The finished web page will run on the local web server and we can create one quickly with a Python command. Check installing Python on Windows if you don't have Python installed yet.
- Even though it's not required, it's a good idea to have a coding editor that supports Rust, such as VS Code. If you don't already have it yet, consider installing VS Code on Windows.
2) Creating Rust library
The first step is to create a library-type project:
Go inside the created project folder and let's get started.
3) Modifying Cargo.toml
Our Cargo.toml
configuration file will contain the following:
Let's go through each section:
-
Lines 8-12 - [dependencies]
- wasm-bindgen
The wasm-bindgen crate is needed so we can annotate some of the functions with#[wasm_bindgen]
macro. These are the functions we want to share between JavaScript and the Rust wasm module. - lazy_static
The wasm module will have a static global variable that will store a struct with a vector of particles. Because statics in Rust is limited to constant functions, tuple structs, and tuple variants, we use the lazy_static crate that will allow us to have the initialization executed at runtime. - rand
The rand crate is a well-known Rust library for generating random numbers. Each randomly colored particle will begin at a random location and will move in a random direction and speed. - getrandom
The wasm target wasm32-unknown-unknown is not automatically supported by rand crate, so we also add a direct dependency to getrandom crate with the ["js"] feature. This feature tells the getrandom crate that we are building for JavaScript environments, such as browsers.
- wasm-bindgen
-
Lines 14-15 - [lib]
Here we set the library to be built as a dynamic system library so that we get the
.wasm
file after the compilation. -
Lines 17-18 - [profile.dev]
Because the
.wasm
file will be used on the front-end, we use size optimization for the dev profile (default profile for debug and development) to reduce the size of the file.
4) Modifying src/lib.rs - creating particle system
A code in src/lib.rs
will consist of several parts.
- Importing the dependencies that we specified in
Cargo.toml
file. - Having a global static variable with the
Particles
struct in it. - A Particle struct representing a single particle.
- A Particles struct containing a vector of
Particle
instances. - Helper function that returns a random color.
- Importing a JavaScript function with
#[wasm_bindgen]
attribute that draws a particle on HTML5 Canvas. - Exporting two Rust functions with
#[wasm_bindgen]
attribute so that they can be called from JavaScript. One is used to populate particles, while the other is used to draw and update them.
Let's learn more about each part, but first, let's take a look at the complete code inside src/lib.rs
for the particle system. It contains about 100 lines of code.
use std::sync::Mutex;
use wasm_bindgen::prelude::*;
use rand::Rng;
#[macro_use]
extern crate lazy_static;
//Global variable
lazy_static! {
static ref PARTICLES: Mutex<Particles> = {
let p = Particles { particles: Vec::new(), canvas_width: 0.0, canvas_height: 0.0 };
Mutex::new(p)
};
}
//Particle
pub struct Particle {
x: f64,
y: f64,
vx: f64,
vy: f64,
color: String,
}
impl Particle {
fn new(x_starting_range: f64, y_starting_range: f64) -> Particle {
let mut rng = rand::thread_rng();
Particle {
x: x_starting_range * rng.gen::<f64>(),
y: y_starting_range * rng.gen::<f64>(),
vx: 4.0 * rng.gen::<f64>() - 2.0,
vy: 4.0 * rng.gen::<f64>() - 2.0,
color: get_random_rgb(),
}
}
}
//Particles
pub struct Particles {
particles: Vec<Particle>,
canvas_width: f64,
canvas_height: f64,
}
impl Particles {
fn create(&mut self, num: i32) {
for _ in 0..num {
self.particles.push(Particle::new(self.canvas_width, self.canvas_height));
}
}
fn draw(&self) {
for p in self.particles.iter() {
draw_particle(p.x, p.y, &p.color, 2);
}
}
fn update(&mut self) {
for p in self.particles.iter_mut() {
p.x += p.vx;
p.y += p.vy;
if p.x < 0.0 || p.x > self.canvas_width {
p.vx = -p.vx;
}
if p.y < 0.0 || p.y > self.canvas_height {
p.vy = -p.vy;
}
}
}
}
//Helper function
fn get_random_rgb() -> String {
let mut r = 0;
let mut g = 0;
let mut b = 0;
let mut rng = rand::thread_rng();
while r < 100 && g < 100 && b < 100 {
r = rng.gen_range(0..256);
g = rng.gen_range(0..256);
b = rng.gen_range(0..256);
}
format!("rgb({r},{g},{b})").to_string()
}
//Imported JS function from index.html
#[wasm_bindgen]
extern "C" {
fn draw_particle(x: f64, y: f64, s: &str, size: i32);
}
//Exported Rust functions used by index.html
#[wasm_bindgen]
pub fn create_particles(w: f64, h: f64, num: i32) {
PARTICLES.lock().unwrap().canvas_width = w;
PARTICLES.lock().unwrap().canvas_height = h;
PARTICLES.lock().unwrap().create(num);
}
#[wasm_bindgen]
pub fn render_particles() {
PARTICLES.lock().unwrap().draw();
PARTICLES.lock().unwrap().update();
}
We will build the project in the next step, but first, let's examine the code in more detail.
Importing the dependencies
In the beginning, we import the dependencies we declared in Cargo.toml
file.
use std::sync::Mutex;
use wasm_bindgen::prelude::*;
use rand::Rng;
#[macro_use]
extern crate lazy_static;
We also import the Mutex from the standard library to use in a global variable.
Global Static mutable variable
Next, we declare a static global mutable variable.
lazy_static! {
static ref PARTICLES: Mutex<Particles> = {
let p = Particles { particles: Vec::new(), canvas_width: 0.0, canvas_height: 0.0 };
Mutex::new(p)
};
}
Here we make use of lazy_static!
macro that allows the code to be executed at runtime in order to be initialized.
The static variable will hold the Particles
struct wrapped in Mutex, which makes the variable mutable in a "safe way" as it only lets one process change it at a time.
Before we look at Particles
struct, let's examine the Particle
struct first.
Particle struct
A single particle is represented by the Particle
struct. It contains the x
,y
coordinates, the movement for both directions, and also a color
string.
pub struct Particle {
x: f64,
y: f64,
vx: f64,
vy: f64,
color: String,
}
For Particle
struct, we implement a single method, fn new()
, which is used to construct new instances of Particle
objects that will have a random starting position, random direction & speed, and also a random color.
impl Particle {
fn new(x_starting_range: f64, y_starting_range: f64) -> Particle {
let mut rng = rand::thread_rng();
Particle {
x: x_starting_range * rng.gen::<f64>(),
y: y_starting_range * rng.gen::<f64>(),
vx: 4.0 * rng.gen::<f64>() - 2.0,
vy: 4.0 * rng.gen::<f64>() - 2.0,
color: get_random_rgb(),
}
}
}
To ensure that the Particle starting position starts at a random position on the Canvas, we provide it with maximum possible random values with the x_starting_range & y_starting_range parameters which in our case will be the HTML5 Canvas width & height.
The vx
& vy
represent the particle's direction and velocity for each animation frame, which in our case will range between -2 to 2.
The color will be the random color from the get_random_rgb() helper function in the rgb(x,y,z) string format.
Particles struct
The Particles struct contains the vector of Particle instances as well as the values of Canvas width and height. They are passed as the arguments when a new Particle instance is created.
pub struct Particles {
particles: Vec<Particle>,
canvas_width: f64,
canvas_height: f64,
}
The Particles struct has three methods implemented.
- fn create(&mut self, num: i32)
fn create(&mut self, num: i32) { for _ in 0..num { self.particles.push(Particle::new(self.canvas_width, self.canvas_height)); } }
Here, num Particle instances are created and added to the
Particles.particles
vector. - fn draw(&self)
fn draw(&self) { for p in self.particles.iter() { draw_particle(p.x, p.y, &p.color, 2); } }
And this function iterates through all of the
Particle
instances in a vector and calls JavaScriptdraw_particle()
function that draws a particle on a Canvas. The last parameter 2 is the size in pixels. This function will be inside theindex.html
file that will be created in step 6. - fn update(&mut self)
fn update(&mut self) { for p in self.particles.iter_mut() { p.x += p.vx; p.y += p.vy; if p.x < 0.0 || p.x > self.canvas_width { p.vx = -p.vx; } if p.y < 0.0 || p.y > self.canvas_height { p.vy = -p.vy; } } }
Here we are iterating through all the
Particle
instances and updating theirx
&y
coordinates with the velocity ofvx
&vy
. If the particle will go over the Canvas boundaries, we inverse the value of the velocity to redirect it in an opposite direction. This way, the particles will bounce off from the Canvas border.
Get random color helper function
fn get_random_rgb() -> String {
let mut r = 0;
let mut g = 0;
let mut b = 0;
let mut rng = rand::thread_rng();
while r < 100 && g < 100 && b < 100 {
r = rng.gen_range(0..256);
g = rng.gen_range(0..256);
b = rng.gen_range(0..256);
}
format!("rgb({r},{g},{b})").to_string()
}
When we create a new Particle
object, we use the get_random_rgb()
function to assign it a random color. It generates R, G, B colors at random and returns a string in the format "rgb(R,G,B)". If the color is too dark, the randomization is repeated (line 77).
Importing JavaScript draw_particle() function
#[wasm_bindgen]
extern "C" {
fn draw_particle(x: f64, y: f64, s: &str, size: i32);
}
Using #[wasm_bindgen]
attribute and extern
keyword, we import a JavaScript function draw_particle()
, so that it is accessible when we call it from the Particles.draw() method.
This custom JavaScript function will be created in step 6 when working on index.html
file.
Exporting Rust functions that will be called from JavaScript
#[wasm_bindgen]
pub fn create_particles(w: f64, h: f64, num: i32) {
PARTICLES.lock().unwrap().canvas_width = w;
PARTICLES.lock().unwrap().canvas_height = h;
PARTICLES.lock().unwrap().create(num);
}
#[wasm_bindgen]
pub fn render_particles() {
PARTICLES.lock().unwrap().draw();
PARTICLES.lock().unwrap().update();
}
If we annotate the public function with #[wasm_bindgen]
attribute, it can be imported by JavaScript. We have two of such functions. They will be called from the index.html
file.
With create_particles()
, we create num number of particles that are randomly placed at Canvas. we get the Canvas width and height from the w and h parameters to ensure that the particle starting position is not placed outside the Canvas.
The render_particles()
is called in each frame of the animation loop. First it draws the particles and then updates their position in a Canvas.
5) Building library & binding wasm module with JavaScript
We now need to compile the library project and tell the Rust compiler to use wasm as a target:
After the built is complete, we should have the .wasm
file in target/wasm32-unknown-unknown/debug/
folder.
Next, we need to bind the wasm module with JavaScript and we do that by using the wasm-bindgen-cli
tool. Check the installing wasm-binder tool on Windows if you don't have it installed yet on your system.
Run the following command:
This will create the generated JavaScript files in the web
folder.
Next, we will build the web page that uses the generated JavaScript and add custom JavaScript code that calls the functions in wasm module.
6) Creating index.html
In the web
folder, create a new file index.html
with the following content:
Let's check the highlighted parts of the page.
- Line 10
In this line, we are importing the functions from the wasm module, which are public Rust functions with
#[wasm_bindgen]
attribute macro. - Line 14-16
Here, we call one of the imported functions create_particles() and pass it Canvas width and height as well as the number of particles we want to create.
- Line 18-23
And here, we have JS
loop()
function used as a callback to requestAnimationFrame() method that tells the browser we want to run animation and to call the sameloop()
function before the next repaint.Within this callback function, we first erase the entire Canvas before calling the imported render_particles() from wasm module which renders and updates the particles.
- Line 29-30
Canvas is a drawing surface and we need to get its context to be able to draw graphics into it.
- Line 32-35
Here, we make use of the Canvas context by setting a color and drawing a rectangle of the same size at a specific position in a Canvas with 0,0 being in the top left corner. The
draw_particle()
is called from the wasm module.
7) Running the page from a local server
In previous steps, we have created a web folder and added an index.html
file, but we can't just open index.html
in a browser, because we will get the "Cross-Origin Request Blocked" error. This is due to the generated JS file specifying the wasm module as a local file.
To test the index.html
, we need to run a local web server and the easiest way is to run the following Python command from a terminal.
Now we need to open a browser at the following address.
The webpage should open with particles flying in all directions at different speeds.
Conclusion
In this Rust WebAssembly tutorial, we created a particle system that runs in a browser. This particle effect uses HTML Canvas to draw the particle, but all of the logic is written in the Rust wasm module. First, 1000 of particles with random starting position, direction and speed are generated. Then we use the JavaScript requestAnimationFrame() method to play the animation of particles moving around the Canvas.