Unit 6.1 - Embedded wasm apps
Exercise 6.1.1: Web Assembly animation
Embed a wasm animation into a static web page.
We will use wasm-bindgen to interface with javascript.
Setup
cargo install wasm-pack
or get it here: https://rustwasm.github.io/wasm-pack/installer/.
Project skeleton
wasm-animation/
├── Cargo.toml
├── description.md
├── src
│ ├── lib.rs
│ └── animation.rs
└── static
└── index.html
At the beginning, the project contains a rust wasm library exporting a function
draw_play_button
which does nothing but to pop up an alert box.
The source tree also contains src/animation.rs
, but this file is not used
by lib.rs
at the beginning.
To compile the project (here, without using npm), use
wasm-pack build --target web --release --no-pack --no-typescript -d public/pkg
The file static/index.html
serves as the entry point, but currently
does not yet load the wasm bundle. Instead it displays "Click to start"
on a <canvas>
element.
If the user clicks on the canvas, the elapsed time after
loading the page will be displayed in an animation loop.
The animation loop is implemented by using
requestAnimationFrame().
Note: You cannot directly open index.html
in your web browser due
to CORS
limitations. Instead, you can set up a quick development
environment using either Python's built-in HTTP server:
python -m http.server -d static/
or by using miniserve:
cargo install miniserve
miniserve static --index "static/index.html" -p 8080
Step 1: Integrate the wasm bundle into index.html
-
At the beginning of
<script type="module">
, addimport init, { draw_play_button } from "./pkg/wasm_animation.js";
-
Call
await init();
as the first statement ofasync function run() {
, -
Call
draw_play_button();
as the 2nd statement, -
Symlink or copy
static/index.html
topublic/
, - Compile / test the result in the browser. A alert popup should appear when loading the page.
Step 2: Replace the alert popup by actual logic
Here is javascript code which draws a play button on the canvas element:
function drawPlayButton() {
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "rgba(255, 255, 255, 0.5)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
const cx = canvas.width/2
const cy = canvas.height/2;
const r = 0.4*Math.min(canvas.width, canvas.height);
ctx.lineWidth = 0.2 * r;
ctx.arc(cx, cy, r, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();
ctx.fillStyle = "black";
ctx.beginPath();
const r_triangle = 0.7 * r;
const sin_60 = Math.sqrt(3)/2;
const cos_60 = 0.5;
ctx.moveTo(cx-r_triangle*cos_60, cy+r_triangle*sin_60);
ctx.lineTo(cx-r_triangle*cos_60, cy-r_triangle*sin_60);
ctx.lineTo(cx+r_triangle, cy);
ctx.closePath();
ctx.fill();
}
Translate it to rust, replacing alert("TODO");
.
-
Remove
alert("TODO");
and theextern "C"
block, -
Start with
let document = web_sys::window().unwrap().document().unwrap(); let canvas = document.get_element_by_id("canvas").unwrap(); let canvas = canvas.dyn_into::<web_sys::HtmlCanvasElement>().unwrap(); let ctx = canvas .get_context("2d") .unwrap() .unwrap() .dyn_into::<web_sys::CanvasRenderingContext2d>() .unwrap();
-
Translation involves:
ctx.fillStyle = ...
->ctx.set_fill_style(&JsValue::from_str(...))
,ctx.lineWidth = ...
->ctx.set_line_width(...)
,canvas.width
->canvas.width() as f64
(same forheight
),ctx.camelCase(...)
->ctx.snake_case(...)
,ctx.arc
needs an.unwrap()
.
-
In
static/index.html
: Replace the calls todraw_initial_text()
withdraw_play_button();
and remove its definition, - Compile / test it in the browser.
Step 3: Replace the js "animation" by the prepared rust animation
-
Make the
animation
module available tolib
, by inserting
at the top of// wasm independent logic: mod animation;
src/lib.rs
, -
Wrap the
animation::Animation
struct in a new structAnimation
inlib.rs
- exporting it withwasm_bindgen
:
Here, we also persist a#[wasm_bindgen] pub struct Animation { animation: animation::Animation, context: web_sys::CanvasRenderingContext2d, }
web_sys::CanvasRenderingContext2d
instance, so we do not have to fetch it all the time from the DOM. -
Add exported functionality to
Animation
:#[wasm_bindgen] impl Animation { pub fn new() -> Result<Animation, JsValue> { // Initialize context and animation Ok(Self { animation, context }) } /// Advance one time step and draw pub fn step(&mut self, dt: f64) { // Call self.animation.step and self.render } /// Render the current state of the animation to the canvas pub fn render(&self) { // - Clear the canvas // - Draw a bounding box (canvas width / height) // - Draw all balls in `self.animation.balls` } }
-
In
index.html
, addAnimation
to theimport
statement (inside the curly braces), -
Call
const animation = Animation.new();
andanimation.render();
directly afterawait init();
, -
Remove
function animation()
, -
Replace the call
animation(t)
withanimation.step(t - state.t0)
, - Test it in the browser.
Step 4 (challenge): Translate the rest of the js code to rust
Goal: the <script>
element in index.html
should look like:
<script type="module">
import init from "./pkg/wasm_demo.js";
init();
</script>
and all the initializing logic should go to
#[wasm_bindgen(start)]
fn run() {
// init logic
}
Hint: Have a look at this example.