← Back to Lessons Lesson 8 of 8
Advanced simulationgraphics

Particle Simulation

Introduction

This is where Rust/Wasm truly shines — running thousands of physics calculations per frame at near-native speed, with JavaScript handling only the Canvas rendering. This separation lets you achieve 60fps with 10,000+ particles.

Architecture

┌─────────────┐    positions()    ┌──────────────┐
│  Rust/Wasm   │ ───────────────▶ │  JavaScript  │
│  Simulation  │                  │  Canvas 2D   │
│  (physics)   │ ◀─────────────── │  (rendering) │
└─────────────┘    tick()         └──────────────┘

The key insight: Rust owns the simulation state. JavaScript only reads position data and draws to the Canvas. This avoids expensive data serialization every frame.

Setup

[dependencies]
wasm-bindgen = "0.2"

[dependencies.web-sys]
version = "0.3"
features = [
    "Window",
    "Document",
    "HtmlCanvasElement",
    "CanvasRenderingContext2d",
]

Data transfer: flat arrays

The most efficient way to send bulk data from Rust to JS is a flat Vec<f64>:

// Instead of returning Vec<Particle> (can't cross Wasm boundary),
// return a flat array: [x0, y0, r0, x1, y1, r1, ...]
pub fn positions(&self) -> Vec<f64> {
    self.particles.iter()
        .flat_map(|p| vec![p.x, p.y, p.radius])
        .collect()
}

In JavaScript, this becomes a Float64Array — directly iterable, no JSON parsing:

const positions = simulation.positions();
for (let i = 0; i < positions.length; i += 3) {
    const x = positions[i];
    const y = positions[i + 1];
    const radius = positions[i + 2];
    // draw...
}

JavaScript rendering loop

import init, { Simulation } from './pkg/particles.js';

async function run() {
    await init();

    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
    const sim = new Simulation(canvas.width, canvas.height, 1000);

    function frame() {
        // Physics in Rust
        sim.tick();

        // Read positions
        const positions = sim.positions();

        // Render in JS
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.fillStyle = '#654ff0';

        for (let i = 0; i < positions.length; i += 3) {
            ctx.beginPath();
            ctx.arc(positions[i], positions[i+1], positions[i+2], 0, Math.PI * 2);
            ctx.fill();
        }

        requestAnimationFrame(frame);
    }

    requestAnimationFrame(frame);
}

run();

Performance optimization tips

1. Reuse buffers — avoid allocating every frame

pub struct Simulation {
    particles: Vec<Particle>,
    position_buffer: Vec<f64>,  // reuse this
    // ...
}

pub fn positions(&mut self) -> Vec<f64> {
    self.position_buffer.clear();
    for p in &self.particles {
        self.position_buffer.push(p.x);
        self.position_buffer.push(p.y);
        self.position_buffer.push(p.radius);
    }
    self.position_buffer.clone()
}

2. Use SharedArrayBuffer for zero-copy (advanced)

Instead of copying data each frame, share memory between Rust and JS:

// Expose a pointer to the particle data
pub fn positions_ptr(&self) -> *const f64 { /* ... */ }
pub fn positions_len(&self) -> usize { /* ... */ }
// JS reads directly from Wasm memory (zero copy!)
const ptr = sim.positions_ptr();
const len = sim.positions_len();
const view = new Float64Array(wasm.memory.buffer, ptr, len);

3. Spatial partitioning for collisions

For particle-particle collisions, use a grid or quadtree to avoid O(n²) checks:

pub fn tick_with_collisions(&mut self) {
    // Grid-based broad phase
    let cell_size = 20.0;
    let mut grid: HashMap<(i32, i32), Vec<usize>> = HashMap::new();

    for (i, p) in self.particles.iter().enumerate() {
        let key = ((p.x / cell_size) as i32, (p.y / cell_size) as i32);
        grid.entry(key).or_default().push(i);
    }

    // Only check particles in same/neighboring cells
    // ...
}

Performance benchmarks

Particle count Pure JS (fps) Rust/Wasm (fps)
1,000 60 60
5,000 35 60
10,000 15 60
50,000 3 45

The gap widens dramatically with complex physics (collisions, forces, constraints).

Try It

Add gravity, particle collisions, or mouse interaction to the simulation.

Try It

Chapter Quiz

Pass all questions to complete this lesson