← Back to Lessons Lesson 11 of 28
Intermediate graphicsdom
Canvas & 2D Graphics
Introduction
HTML Canvas is one of the best use cases for Rust/Wasm — Rust handles the computation (physics, game logic, procedural generation), while the Canvas API handles rendering. This split gives you near-native performance for the heavy parts.
Setup
[dependencies.web-sys]
version = "0.3"
features = [
"Window",
"Document",
"HtmlCanvasElement",
"CanvasRenderingContext2d",
]Getting the Canvas Context
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
fn get_context(canvas_id: &str) -> Result<CanvasRenderingContext2d, JsValue> {
let document = web_sys::window().unwrap().document().unwrap();
let canvas = document
.get_element_by_id(canvas_id)
.unwrap()
.dyn_into::<HtmlCanvasElement>()?;
canvas
.get_context("2d")?
.unwrap()
.dyn_into::<CanvasRenderingContext2d>()
}Drawing Primitives
// Rectangle
ctx.set_fill_style_str("#654ff0");
ctx.fill_rect(10.0, 10.0, 100.0, 50.0);
// Circle
ctx.begin_path();
ctx.arc(200.0, 200.0, 40.0, 0.0, std::f64::consts::PI * 2.0)?;
ctx.set_fill_style_str("#ce422b");
ctx.fill();
// Line
ctx.begin_path();
ctx.move_to(0.0, 0.0);
ctx.line_to(300.0, 150.0);
ctx.set_stroke_style_str("#34d399");
ctx.set_line_width(2.0);
ctx.stroke();
// Text
ctx.set_font("24px Inter");
ctx.set_fill_style_str("#f1f5f9");
ctx.fill_text("Hello Wasm!", 50.0, 50.0)?;Animation Loop
The animation runs in JavaScript using requestAnimationFrame, calling Rust for each frame:
import init, { Canvas } from './pkg/my_app.js';
async function run() {
await init();
const canvas = new Canvas("game-canvas");
let x = 0;
function frame() {
canvas.clear();
canvas.draw_circle(x, 300, 20, "#654ff0");
x = (x + 2) % 800;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
run();Mouse Input
use web_sys::MouseEvent;
use wasm_bindgen::closure::Closure;
pub fn setup_mouse(canvas: &HtmlCanvasElement) -> Result<(), JsValue> {
let closure = Closure::wrap(Box::new(move |event: MouseEvent| {
let x = event.offset_x() as f64;
let y = event.offset_y() as f64;
// Handle click at (x, y)
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback(
"click",
closure.as_ref().unchecked_ref(),
)?;
closure.forget();
Ok(())
}Image Drawing
use web_sys::HtmlImageElement;
pub fn draw_image(ctx: &CanvasRenderingContext2d, src: &str, x: f64, y: f64)
-> Result<(), JsValue>
{
let img = HtmlImageElement::new()?;
img.set_src(src);
let ctx = ctx.clone();
let closure = Closure::once(move || {
ctx.draw_image_with_html_image_element(&img, x, y).unwrap();
});
img.set_onload(Some(closure.as_ref().unchecked_ref()));
closure.forget();
Ok(())
}Performance: Canvas vs WebGL
| Feature | Canvas 2D | WebGL |
|---|---|---|
| Setup complexity | Low | High |
| Draw calls | ~1,000/frame | ~10,000/frame |
| Shaders | No | Yes |
| 3D | No | Yes |
| Best for | 2D games, charts, UI | 3D, heavy particle systems |
For most 2D Rust/Wasm projects, Canvas 2D is sufficient and much simpler.
Try It
The starter code shows a Canvas wrapper struct with basic drawing methods. In a real project, you'd call these from a JavaScript animation loop.
Try It
Chapter Quiz
Pass all questions to complete this lesson