Create Dev's offline page with Rust and WebAssembly ๐Ÿฆ„๐Ÿ’กโœจ

Sendil Kumar - Jul 3 '19 - - Dev Community

Dev's offline page is fun. Can we do that with Rust and WebAssembly?

The answer is yes. Let us do it.

First, we will create a simple Rust and WebAssembly application with Webpack.

npm init rust-webpack dev-offline-canvas
Enter fullscreen mode Exit fullscreen mode

The Rust and WebAssembly ecosystem provides web_sys that provides the necessary binding over the Web APIs. Check it out here.

The sample application already has web_sys dependency. The web_sys crate includes all the available WebAPI bindings.

Including all the WebAPI bindings will increase the binding file size. It is very important to include only the APIs that we need.

We will remove the existing feature

features = [
    'console'
]
Enter fullscreen mode Exit fullscreen mode

and replace it with the following:

features = [
  'CanvasRenderingContext2d',
  'CssStyleDeclaration',
  'Document',
  'Element',
  'EventTarget',
  'HtmlCanvasElement',
  'HtmlElement',
  'MouseEvent',
  'Node',
  'Window',
]
Enter fullscreen mode Exit fullscreen mode

The above list of features is the entire set of features that we will be using in this example.

Lets write some Rust

Open the src/lib.rs.

replace the start() function with the following:



#[wasm_bindgen(start)]
pub fn start() -> Result<(), JsValue> {

   Ok()
}
Enter fullscreen mode Exit fullscreen mode

The #[wasm_bindgen(start)] calls this function as soon as the WebAssembly Module is instantiated. Check out more about the start function in the spec here.

We will get the window object in the Rust.

    let window = web_sys::window().expect("should have a window in this context");
Enter fullscreen mode Exit fullscreen mode

Then get the document from the window object.

     let document = window.document().expect("window should have a document");
Enter fullscreen mode Exit fullscreen mode

Create a Canvas element and append it to the document.

    let canvas = document
         .create_element("canvas")?
         .dyn_into::<web_sys::HtmlCanvasElement>()?;

    document.body().unwrap().append_child(&canvas)?;
Enter fullscreen mode Exit fullscreen mode

Set width, height, and the border for the canvas element.

    canvas.set_width(640);
    canvas.set_height(480);
    canvas.style().set_property("border", "solid")?;
Enter fullscreen mode Exit fullscreen mode

In the Rust, the memories are discarded once the execution goes out of context or when the method returns any value. But in JavaScript, the window, document is alive as long as the page is up and running.

So it is important to create a reference for the memory and make it live statically until the program is completely shut down.

Get the Canvas' rendering context and create a wrapper around it in order to preserve its lifetime.

RC stands for Reference Counted.

The type Rc provides shared ownership of a value of type T, allocated in the heap. Invoking clone on Rc produces a new pointer to the same value in the heap. When the last Rc pointer to a given value is destroyed, the pointed-to value is also destroyed. - RC docs

This reference is cloned and used for callback methods.

let context = canvas
        .get_context("2d")?
        .unwrap()
        .dyn_into::<web_sys::CanvasRenderingContext2d>()?;

let context = Rc::new(context);
Enter fullscreen mode Exit fullscreen mode

Since we are going to capture the mouse events. We will create a boolean variable called pressed. The pressed will hold the current value of mouse click.

let pressed = Rc::new(Cell::new(false));
Enter fullscreen mode Exit fullscreen mode

Now we need to create a closure (call back function) for mouseDown | mouseUp | mouseMove.

    { mouse_down(&context, &pressed, &canvas); }
    { mouse_move(&context, &pressed, &canvas); }
    { mouse_up(&context, &pressed, &canvas); }
Enter fullscreen mode Exit fullscreen mode

We will define the actions that we need to do during those events as separate functions. These functions take the context of the Canvas element and pressed status.


fn mouse_up(context: &std::rc::Rc<web_sys::CanvasRenderingContext2d>, pressed: &std::rc::Rc<std::cell::Cell<bool>>, canvas: &web_sys::HtmlCanvasElement) {
    let context = context.clone();
    let pressed = pressed.clone();
    let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
        pressed.set(false);
        context.line_to(event.offset_x() as f64, event.offset_y() as f64);
        context.stroke();
    }) as Box<dyn FnMut(_)>);
    canvas.add_event_listener_with_callback("mouseup", closure.as_ref().unchecked_ref()).unwrap();
    closure.forget();
}

fn mouse_move(context: &std::rc::Rc<web_sys::CanvasRenderingContext2d>, pressed: &std::rc::Rc<std::cell::Cell<bool>>, canvas: &web_sys::HtmlCanvasElement){
    let context = context.clone();
    let pressed = pressed.clone();
    let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
        if pressed.get() {
            context.line_to(event.offset_x() as f64, event.offset_y() as f64);
            context.stroke();
            context.begin_path();
            context.move_to(event.offset_x() as f64, event.offset_y() as f64);
        }
    }) as Box<dyn FnMut(_)>);
    canvas.add_event_listener_with_callback("mousemove", closure.as_ref().unchecked_ref()).unwrap();
    closure.forget();
}

fn mouse_down(context: &std::rc::Rc<web_sys::CanvasRenderingContext2d>, pressed: &std::rc::Rc<std::cell::Cell<bool>>, canvas: &web_sys::HtmlCanvasElement){
    let context = context.clone();
    let pressed = pressed.clone();

    let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
        context.begin_path();
        context.set_line_width(5.0);
        context.move_to(event.offset_x() as f64, event.offset_y() as f64);
        pressed.set(true);
    }) as Box<dyn FnMut(_)>);
    canvas.add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref()).unwrap();
    closure.forget();
}

Enter fullscreen mode Exit fullscreen mode

They are very similar to how your JavaScript API will look like but they are written in Rust.

Now we are all set. We can run the application and draw inside the canvas. ๐ŸŽ‰ ๐ŸŽ‰ ๐ŸŽ‰

But we do not have any colours.

Lets add some colours.

To add the colour swatches. Create a list of divs and use them as a selector.

Define the list of colours that we need to add inside the start program.

#[wasm_bindgen(start)]
pub fn start() -> Result<(), JsValue> {
    // ....... Some content
    let colors = vec!["#F4908E", "#F2F097", "#88B0DC", "#F7B5D1", "#53C4AF", "#FDE38C"];

   Ok()
}
Enter fullscreen mode Exit fullscreen mode

Then run through the list and create a div for all the colours and append it to the document. For every div add an onClick handler too to change the colour.


    for c in colors {
        let div = document
            .create_element("div")?
            .dyn_into::<web_sys::HtmlElement>()?;
        div.set_class_name("color");
        {
            click(&context, &div, c.clone());  // On Click Closure.
        }

        div.style().set_property("background-color", c);
        let div = div.dyn_into::<web_sys::Node>()?;
        document.body().unwrap().append_child(&div)?;
    }
Enter fullscreen mode Exit fullscreen mode

The click hander is as follows:

fn click(context: &std::rc::Rc<web_sys::CanvasRenderingContext2d>, div: &web_sys::HtmlElement, c: &str) {
    let context = context.clone();
    let c = JsValue::from(String::from(c));
    let closure = Closure::wrap(Box::new(move || {
        context.set_stroke_style(&c);            
    }) as Box<dyn FnMut()>);

    div.set_onclick(Some(closure.as_ref().unchecked_ref()));
    closure.forget();
}
Enter fullscreen mode Exit fullscreen mode

Now a little beautification. Open the static/index.html and add the style for the colour div.

 <style>
       .color {
            display: inline-block;
            width: 50px;
            height: 50px;
            border-radius: 50%;
            cursor: pointer;
            margin: 10px;
       }
 </style>
Enter fullscreen mode Exit fullscreen mode

That is it, we have created the application. ๐ŸŽ‰

Check out the demo application available here.

I hope this gives you a motivation to start your awesome WebAssembly journey. If you have any questions/suggestions/feel that I missed something feel free to add a comment.

You can follow me on Twitter.

If you like this article, please leave a like or a comment. โค๏ธ

for the article.

Check out my more WebAssembly articles here.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player