← 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