DEV Community

Samuel Su
Samuel Su

Posted on

1

【译】使用 Rust 和 WebAssembly 构建离线画图页面

【译】使用 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 文章,点击这儿

Billboard image

Monitoring as code

With Checkly, you can use Playwright tests and Javascript to monitor end-to-end scenarios in your NextJS, Astro, Remix, or other application.

Get started now!

Top comments (0)

Image of Timescale

Timescale – the developer's data platform for modern apps, built on PostgreSQL

Timescale Cloud is PostgreSQL optimized for speed, scale, and performance. Over 3 million IoT, AI, crypto, and dev tool apps are powered by Timescale. Try it free today! No credit card required.

Try free

👋 Kindness is contagious

Explore a sea of insights with this enlightening post, highly esteemed within the nurturing DEV Community. Coders of all stripes are invited to participate and contribute to our shared knowledge.

Expressing gratitude with a simple "thank you" can make a big impact. Leave your thanks in the comments!

On DEV, exchanging ideas smooths our way and strengthens our community bonds. Found this useful? A quick note of thanks to the author can mean a lot.

Okay