Update a canvas from wasm

by Geoffroy Couprie


Caution: This will display a flickering image.


The Code

This example shares a buffer between the Javascript side and the Rust side, and uses it to update the raw pixels on a canvas.

Download main.rs

use std::mem;
use std::slice;
use std::os::raw::c_void;

// We need to provide an (empty) main function,
// as the target currently is compiled as a binary.
fn main() {}

// In order to work with the memory we expose (de)allocation methods
#[no_mangle]
pub extern "C" fn alloc(size: usize) -> *mut c_void {
    let mut buf = Vec::with_capacity(size);
    let ptr = buf.as_mut_ptr();
    mem::forget(buf);
    return ptr as *mut c_void;
}

#[no_mangle]
pub extern "C" fn dealloc(ptr: *mut c_void, cap: usize) {
    unsafe  {
        let _buf = Vec::from_raw_parts(ptr, 0, cap);
    }
}

// the Javascript side passes a pointer to a buffer, the size of the corresponding canvas
// and the current timestamp
#[no_mangle]
pub extern "C" fn fill(pointer: *mut u8, max_width: usize, max_height: usize, time: f64) {

    // pixels are stored in RGBA, so each pixel is 4 bytes
    let byte_size = max_width * max_height * 4;
    let sl = unsafe { slice::from_raw_parts_mut(pointer, byte_size) };

    for i in 0..byte_size {

        // get the position of current pixel
        let height = i / 4 / max_width;
        let width  = i / 4 % max_width;

        if i%4 == 3 {
            // set opacity to 1
            sl[i] = 255;
        } else if i%4 == 0 {
            // create a red ripple effect from the top left corner
            let len = ((height*height + width*width) as f64).sqrt();
            let nb = time  + len / 4.0;
            let a = 128.0 + nb.cos() * 128.0;
            sl[i] = a as u8;
        } else if i % 4 == 2 {
            // create a blue ripple effect from the top right corner
            let width = 500 - width;
            let len = ((height*height + width*width) as f64).sqrt();
            let nb = time  + len / 4.0;
            let a = 128.0 + nb.cos() * 128.0;
            sl[i] = a as u8;
        }
    }
}

The JavaScript code loads the WebAssembly module and has access to the exported functions and fields, including the allocation and memory. We create an ImageData backed by memory allocated on the Rust side, and regularly update it.

fetch("canvas.wasm").then(response =>
  response.arrayBuffer()
).then(bytes =>

  // the Rust side needs a cos function
  WebAssembly.instantiate(bytes, { env: { cos: Math.cos } })
).then(results => {
  let module = {};
  let mod = results.instance;
  module.alloc   = mod.exports.alloc;
  module.dealloc = mod.exports.dealloc;
  module.fill    = mod.exports.fill;

  var width  = 500;
  var height = 500;


  var canvas = document.getElementById('screen');
  if (canvas.getContext) {
    var ctx = canvas.getContext('2d');

    let byteSize = width * height * 4;
    var pointer = module.alloc( byteSize );

    var usub = new Uint8ClampedArray(mod.exports.memory.buffer, pointer, byteSize);
    var img = new ImageData(usub, width, height);

    var start = null;
    function step(timestamp) {
      var progress;
      if (start === null) start = timestamp;
      progress = timestamp - start;
      if (progress > 100) {
        module.fill(pointer, width, height, timestamp);

        start = timestamp

        window.requestAnimationFrame(draw);
      } else {
        window.requestAnimationFrame(step);
      }
    }

    function draw() {
      ctx.putImageData(img, 0, 0)
      window.requestAnimationFrame(step);
    }

    window.requestAnimationFrame(step);
  }

});