Skip to content

Instantly share code, notes, and snippets.

@lmmx
Last active March 21, 2025 16:52
Show Gist options
  • Save lmmx/379d374ebca871b9cc3e864ab6ffb571 to your computer and use it in GitHub Desktop.
Save lmmx/379d374ebca871b9cc3e864ab6ffb571 to your computer and use it in GitHub Desktop.
A minimal Rust + WASM demo simulating 2,000 vehicles (buses/trains) moving on a canvas, using a shared `RefCell` for state https://qrx.spin.systems/transport-network-sim/

Transport Network Simulation (Rust + WebAssembly)

This code simulates a transport network with 2,000 vehicles (buses and trains) in the browser. It uses a global, thread-local RefCell in Rust to store and update the vehicles’ shared state. Every half-second, each vehicle moves along its route (horizontal lines for buses, vertical lines for trains), and a <canvas> is redrawn to reflect their updated positions. The simulation logs the update/draw performance to the browser console.

This simple setup shows how to store & update thousands of moving vehicles in Rust, draw them in an HTML <canvas>, and log performance in the browser console—all while using a single global RefCell for shared state. Enjoy tinkering with it!

Features

  1. Shared State via RefCell

    • We store all vehicle data in a single, global RefCell<SharedState>. This allows safe interior mutability without passing around mutable references.
  2. 2,000 Vehicles

    • 1,000 buses across 100 routes (horizontal lines).
    • 1,000 trains across 10 routes (vertical lines).
  3. Canvas Visualization

    • Each vehicle is drawn as a small circle on an HTML <canvas>.
    • Buses move left→right at their assigned horizontal line (y = route_index * 10).
    • Trains move top→bottom at their assigned vertical line (x = route_index * 10).
    • When a vehicle reaches the end, it wraps around to the start.
  4. Periodic Updates

    • A Rust closure (scheduled via setInterval) updates all vehicles every 500 ms.
    • The code measures and logs how long each update cycle (and canvas redraw) takes.
  5. Performance Logging

    • Check the browser console to see messages like "Update & draw took X.XXX ms".

How It Works

  1. Vehicle Initialization

    • On startup, 2,000 vehicles (buses/trains) are created with random initial positions and speeds.
  2. Vehicle Movement

    • Each vehicle has:
      • A vehicle_type (Bus or Train).
      • A route_index, which determines which line they move along.
      • A position in the range [0..1], which maps to 0..1000 on the canvas.
      • A speed that advances position each cycle.
    • If position exceeds 1.0, it wraps back around.
  3. Rendering

    • The <canvas> is cleared each cycle.
    • We loop over all vehicles, draw a small circle at (vehicle.x, vehicle.y), then fill it.
  4. Timing & Logging

    • We note the current time (js_sys::Date::now()) before and after updating/drawing.
    • The difference is logged to the console in milliseconds.

Running the Demo

  1. Install Trunk (or another toolchain that can build and serve Wasm).
  2. Build & Serve:
    trunk serve
  3. Open your browser at the URL Trunk provides (e.g., http://127.0.0.1:8080).
  4. You should see:
    • A 1000×1000 <canvas> with numerous horizontal and vertical lines of moving dots.
    • The browser console showing messages like “Update & draw took 5.000 ms”.

File Overview

  • index.html
    Contains the <canvas> element and a <script data-trunk> tag that points to our Rust/Wasm code.

  • lib.rs
    The main Rust code. Key parts:

    • Vehicle struct with vehicle_type, route_index, position, speed, x, y.
    • SharedState struct storing a Vec<Vehicle>.
    • thread_local! for a global RefCell<SharedState>.
    • init_vehicles() to create all vehicles, assign routes and speeds.
    • update_all() to move each vehicle and recalculate (x, y).
    • A timer callback (set via set_interval_with_callback...) that updates positions, draws them on <canvas>, and logs performance.

Why RefCell?

  • Even though this is single-threaded Wasm, using a global RefCell is a convenient, Rust-safe way to share and mutate data across multiple scopes (e.g., callbacks, initialization, rendering) without needing to pass around references.

Customizing

  • Routes: You can change the number of bus/train routes or how they’re laid out.
  • Speeds: Adjust the random generation range if you want faster or slower movement.
  • Drawing: Swap out circle drawing for images, or scale positions differently for a more complex map layout.
  • Update Frequency: Change the interval (e.g., from 500 ms to 100 ms) if you want smoother movement.
[package]
name = "trunk-hello-world"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
path = "lib.rs"
[dependencies]
console_error_panic_hook = "0.1.7"
js-sys = "0.3.77"
once_cell = { version = "1.21.1", default-features = false }
wasm-bindgen = "0.2.100"
web-sys = { version = "0.3.77", features = ["Window", "HtmlInputElement", "Text", "Event", "console", "HtmlCanvasElement", "CanvasRenderingContext2d", "Document"] }
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WASM Transport Simulation</title>
</head>
<body>
<h1>WASM Transport Simulation</h1>
<p>Open your browser console to see performance logs.</p>
<!-- We draw our vehicles here -->
<canvas id="myCanvas" width="1000" height="1000"
style="border:1px solid #ccc;">
Your browser doesn't support HTML canvas.
</canvas>
<!-- Trunk will compile `lib.rs` into Wasm & JS and inject them here. -->
<script data-trunk src="lib.rs"></script>
</body>
</html>
use console_error_panic_hook as set_panic_hook;
use js_sys::Math;
use std::cell::RefCell;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{JsCast, JsValue, closure::Closure};
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, console};
#[derive(Debug, PartialEq)]
enum VehicleType {
Bus,
Train,
}
#[derive(Debug)]
struct Vehicle {
vehicle_type: VehicleType,
route_index: usize, // For bus: y-line, for train: x-line
position: f32, // 0..1 along the route
speed: f32, // How much to move each update
x: f32, // Current X coordinate on the canvas
y: f32, // Current Y coordinate on the canvas
}
impl Vehicle {
/// Update the position along the route, wrap if necessary, and set (x, y).
fn update_position(&mut self) {
// Advance
self.position += self.speed;
// Wrap around if off the end
if self.position >= 1.0 {
self.position -= 1.0;
}
match self.vehicle_type {
VehicleType::Bus => {
// Buses move horizontally from x=0..1000
// Their route_index sets which horizontal line they ride
self.x = self.position * 1000.0;
self.y = self.route_index as f32 * 10.0; // e.g. spaced by 10px
}
VehicleType::Train => {
// Trains move vertically from y=0..1000
self.x = self.route_index as f32 * 10.0; // e.g. spaced by 10px
self.y = self.position * 1000.0;
}
}
}
}
/// Global shared state with all vehicles
struct SharedState {
vehicles: Vec<Vehicle>,
}
impl SharedState {
fn new() -> Self {
Self {
vehicles: Vec::new(),
}
}
/// Create 2,000 vehicles:
/// - 1,000 buses (horizontal lines)
/// - 1,000 trains (vertical lines)
fn init_vehicles(&mut self) {
let rng = || Math::random() as f32; // convenience closure
// 1) Buses (100 routes × 10 vehicles each = 1,000)
// We’ll create 1,000 vehicles, each assigned a route_index in [0..99].
for _ in 0..1000 {
let route_index = (rng() * 100.0) as usize; // 0..99
let position = rng(); // 0..1
let speed = 0.001 + rng() * 0.004; // e.g., 0.001..0.005
let mut v = Vehicle {
vehicle_type: VehicleType::Bus,
route_index,
position,
speed,
x: 0.0,
y: 0.0,
};
v.update_position(); // Initialize x,y
self.vehicles.push(v);
}
// 2) Trains (10 routes × 100 vehicles each = 1,000)
// We’ll create 1,000 vehicles, each assigned a route_index in [0..9].
for _ in 0..1000 {
let route_index = (rng() * 10.0) as usize; // 0..9
let position = rng(); // 0..1
let speed = 0.001 + rng() * 0.004; // e.g., 0.001..0.005
let mut v = Vehicle {
vehicle_type: VehicleType::Train,
route_index,
position,
speed,
x: 0.0,
y: 0.0,
};
v.update_position(); // Initialize x,y
self.vehicles.push(v);
}
}
/// Move every vehicle according to its speed/route
fn update_all(&mut self) {
for v in self.vehicles.iter_mut() {
v.update_position();
}
}
}
thread_local! {
static GLOBAL_STATE: RefCell<SharedState> = RefCell::new(SharedState::new());
}
#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
set_panic_hook::set_once();
// 1) Initialize the 2,000 vehicles
GLOBAL_STATE.with(|cell| {
cell.borrow_mut().init_vehicles();
});
// 2) Set up a repeating callback in Rust (e.g., every 500 ms)
let closure = Closure::wrap(Box::new(move || {
// Time how long it takes to apply updates
let t_start = js_sys::Date::now();
// Update all vehicle positions
GLOBAL_STATE.with(|cell| {
cell.borrow_mut().update_all();
});
// Grab the canvas & context
let window = web_sys::window().expect("no global `window` exists");
let document = window.document().expect("should have a document");
let canvas_el = document
.get_element_by_id("myCanvas")
.expect("document should have a #myCanvas");
let canvas: HtmlCanvasElement = canvas_el
.dyn_into()
.expect("Failed to cast to HtmlCanvasElement");
let ctx = canvas
.get_context("2d")
.unwrap()
.unwrap()
.dyn_into::<CanvasRenderingContext2d>()
.unwrap();
// Clear the canvas
let w = canvas.width() as f64;
let h = canvas.height() as f64;
ctx.clear_rect(0.0, 0.0, w, h);
// Draw each vehicle
GLOBAL_STATE.with(|cell| {
for v in &cell.borrow().vehicles {
ctx.begin_path();
ctx.arc(v.x as f64, v.y as f64, 2.0, 0.0, 6.28)
.unwrap();
ctx.fill();
}
});
let t_end = js_sys::Date::now();
let ms = t_end - t_start;
console::log_1(&format!("Update & draw took {:.3} ms", ms).into());
}) as Box<dyn FnMut()>);
// 3) Schedule updates with setInterval(..., 500 ms)
let window = web_sys::window().ok_or("no window")?;
window.set_interval_with_callback_and_timeout_and_arguments_0(
closure.as_ref().unchecked_ref(),
1,
)?;
// Keep the closure alive for repeated calls
closure.forget();
Ok(())
}
[build]
public_url = "/transport-network-sim/"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment