【譯】使用 Rust 和 WebAssembly 構建離線畫圖頁面


Dev 網站的離線畫圖頁很有趣。我們能用 Rust 和 WebAssembly 來實現嗎?

答案是肯定的。讓我們現在就來實現它。

首先,我們通過 Webpack 創建了一個基於 Rust 和 WebAssembly 的簡單應用。

npm init rust-webpack dev-offline-canvas

Rust 和 WebAssembly 生態提供了 web_sys,它在 Web API 上提供了很多需要的綁定。可以從這里檢出。

示例應用已經引入了 web_sys 依賴。web_sys crate 中包含了所有可用的 WebAPI 綁定。

如果引入所有的 WebAPI 綁定將會增加綁定文件的大小。按需引入必要的 API 是比較重要的。

我們移除已經存在的 feature 列表(位於 toml 文件中)

features = [
    'console'
]

並使用下面的替代:

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

上面的 features 列表是我們將在本例中需要使用的一些 features。

開始寫段 Rust 代碼

打開文件 src/lib.rs

使用下面的代碼替換掉文件中的 start() 函數:

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

   Ok()
}

一旦實例化了 WebAssembly 模塊,#[wasm_bindgen(start)] 就會調用這個函數。可以查看規范中關於 start 函數的詳細信息

我們在 Rust 中將得到 window 對象。

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

接着從 window 對象中獲取 document。

let document = window.document().expect("window should have a document");

創建一個 Canvas 元素,將其插入到 document 中。

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

document.body().unwrap().append_child(&canvas)?;

設置 canvas 元素的寬、高和邊框。

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

在 Rust 中,一旦離開當前上下文或者函數已經 return,對應的內存就會被釋放。但在 JavaScript 中,window, document 在頁面的啟動和運行時都是活動的(位於生命周期中)。

因此,為內存創建一個引用並使其靜態化,直到程序運行結束,這一點很重要。

獲取 Canvas 渲染的上下文,並在其外層包裝一個 wrapper,以保證它的生命周期。

RC 表示 Reference Counted

Rc 類型提供在堆中分配類型為 T 的值,並共享其所有權。在 Rc 上調用 clone 會生成指向堆中相同值的新的指針。當指向給定值的最后一個 Rc 指針即將被釋放時,它指向的值也將被釋放。 —— RC 文檔

這個引用被 clone 並用於回調方法。

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.
因為我們要響應 mouse 事件。因此我們將創建一個名為 pressed 的布爾類型的變量。pressed 用於保存 mouse click(鼠標點擊)的當前值。

let pressed = Rc::new(Cell::new(false));

現在,我們需要為 mouseDownmouseUpmouseMove 創建一個閉包(回調函數)。

{ mouse_down(&context, &pressed, &canvas); }
{ mouse_move(&context, &pressed, &canvas); }
{ mouse_up(&context, &pressed, &canvas); }

我們將把這些事件觸發時需要執行的操作定義為獨立的函數。這些函數接收 canvas 元素的上下文和鼠標按下狀態作為參數。

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();
}

他們非常類似於你平時寫的 JavaScript 的 API,但它們是用 Rust 編寫的。

現在我們都設置好了。我們可以運行應用程序並在畫布中畫畫。 🎉 🎉 🎉

但我們還沒有設定顏色。

添加多個顏色

增加顏色樣本,創建一個 div 列表,並使用它們作為顏色選擇器。

start 函數中定義我們需要的顏色列表。

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

    Ok()
}

然后遍歷顏色列表,為所有顏色創建一個 div,並將其加入到 document 中。對於每個 div,還需要添加一個 onClick 處理程序來更改畫板顏色。

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)?;
}

其中 click 函數實現如下所示:

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();
}

現在稍微美化一下。打開 static/index.html 文件。在其中添加 div 樣式。

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

這就是我們的畫板了,我們已經創建好了這個應用。🎉

可以從這里檢出示例應用。

希望這個例子能給你開啟美妙的 WebAssembly 旅程帶來靈感。如果你有任何的問題、建議、感受,歡迎給我留言評論。

你可以在 Twitter 關注我。

如果你喜歡這個文章,請給這個文章點贊或留言。❤️

還可以閱讀我的其他 WebAssembly 文章,點擊這兒


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM