DEV Community

loading...
Cover image for Create Dev's offline page with Rust and WebAssembly πŸ¦„πŸ’‘βœ¨

Create Dev's offline page with Rust and WebAssembly πŸ¦„πŸ’‘βœ¨

Sendil Kumar N
An explorer wandering in the land of programs. I am passionate about Open Source. "Docendo discimus"
Originally published at sendilkumarn.com ・Updated on ・5 min read

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. ❀️

aspittel image

for the article.

Check out my more WebAssembly articles here.

Discussion (17)

Collapse
aspittel profile image
Ali Spittel

This is awesome! I'm trying to learn WASM and having my own code translated to it is super helpful. Thank you!!!!!!

Collapse
sendilkumarn profile image
Sendil Kumar N Author

So glad it helped πŸ™‚. Thanks πŸ‘

Collapse
sendilkumarn profile image
Sendil Kumar N Author

WASM is ASM go for it...

Collapse
philnash profile image
Phil Nash

It is really cool to learn WASM by recreating existing web experiences in a different language and compiling. This is a great tutorial for that!

Is this a good idea for WASM in general though? The web_sys crate looks useful, but are you just jumping back and forth across the JavaScript/WASM boundary in order to achieve this, negating any performance benefits of WASM itself?

Collapse
sendilkumarn profile image
Sendil Kumar N Author

That is an awesome question.Boundary crossing is an important factor for any WASM app.

Performance wise it will be slightly slower every crossing here adds few nano seconds overhead. but browsers like Firefox is optimised well enough. You will have a problem only when you transfer huge chunk of data.

General advise will be use WASM where you need to have computation heavy operation and minimize boundary crossing

Collapse
suhanyujie profile image
Samuel Su

It seems that I should add "DomTokenList" to the feature list

Collapse
ben profile image
Ben Halpern

Soooo cool!

Collapse
sendilkumarn profile image
Sendil Kumar N Author

Thanks πŸ™‚

Collapse
andrewbrown profile image
Andrew Brown πŸ‡¨πŸ‡¦

I was going to try and do this with Ruby and WebAssembly but gave up. So hard to get Ruby to WebAssembly.

Collapse
sendilkumarn profile image
Sendil Kumar N Author

Oh, that is interesting! What is the most painful part of it?

Collapse
andrewbrown profile image
Andrew Brown πŸ‡¨πŸ‡¦

You have to use MRuby which is an embedded ruby that has serious limitations. The ruby-wasm gem appears to need to compile Mruby to Emscripten. Then it wants Java for some reason. When compiling obscure errors occur.

So lots of moving parts, no time to debug.

Collapse
suhanyujie profile image
Samuel Su • Edited

Oh,does it has any image for the final page....

Collapse
suhanyujie profile image
Samuel Su

I saw that-- dev.to/offline
Thanks for sharing~

Collapse
suhanyujie profile image
Samuel Su • Edited

Emmm,the article is great! And i have translated it into Chinese. dev.to/suhanyujie/rust-webassembly...
If you don't like this,i'll delete it follow your mind...
Thank u

Collapse
sendilkumarn profile image
Sendil Kumar N Author

Wow thats awesome πŸŽ‰πŸ‘

Collapse
lampewebdev profile image
Michael "lampe" Lazarski

Amazing post!

I want to deep dive into wasm myself and this gives me to push to do it :)

Collapse
sendilkumarn profile image
Sendil Kumar N Author

Yay! Go for it. WASM is AWSM.