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
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'
]
and replace it with the following:
features = [
'CanvasRenderingContext2d',
'CssStyleDeclaration',
'Document',
'Element',
'EventTarget',
'HtmlCanvasElement',
'HtmlElement',
'MouseEvent',
'Node',
'Window',
]
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()
}
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");
Then get the document from the window
object.
let document = window.document().expect("window should have a document");
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)?;
Set width, height, and the border for the canvas element.
canvas.set_width(640);
canvas.set_height(480);
canvas.style().set_property("border", "solid")?;
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);
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));
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); }
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();
}
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()
}
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)?;
}
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();
}
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>
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.
Top comments (17)
This is awesome! I'm trying to learn WASM and having my own code translated to it is super helpful. Thank you!!!!!!
So glad it helped π. Thanks π
WASM is ASM go for it...
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?
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
It seems that I should add "
DomTokenList
" to the feature listSoooo cool!
Thanks π
I was going to try and do this with Ruby and WebAssembly but gave up. So hard to get Ruby to WebAssembly.
Oh, that is interesting! What is the most painful part of it?
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.
Oh,does it has any image for the final page....
I saw that-- dev.to/offline
Thanks for sharing~
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
Wow thats awesome ππ
Amazing post!
I want to deep dive into wasm myself and this gives me to push to do it :)
Yay! Go for it. WASM is AWSM.