- 原文地址:dev.to/sendilkumar…
- 原文仓库:github.com/sendilkumar…
- 原文做者:Sendil Kumar N
- 译文出自:github.com/suhanyujie
- 本文永久连接:(缺省)
- 译者:suhanyujie
- 翻译不当之处,还请指出,谢谢!
- 文中的页面效果能够参考这里:离线画图页
Dev 网站的离线画图页颇有趣。咱们能用 Rust 和 WebAssembly 来实现吗?css
答案是确定的。让咱们如今就来实现它。html
首先,咱们经过 Webpack 建立了一个基于 Rust 和 WebAssembly 的简单应用。webpack
npm init rust-webpack dev-offline-canvas
复制代码
Rust 和 WebAssembly 生态提供了 web_sys
,它在 Web API 上提供了不少须要的绑定。能够从这里检出。git
示例应用已经引入了 web_sys
依赖。web_sys
crate 中包含了全部可用的 WebAPI 绑定。github
若是引入全部的 WebAPI 绑定将会增长绑定文件的大小。按需引入必要的 API 是比较重要的。web
咱们移除已经存在的 feature 列表(位于 toml 文件中)shell
features = [
'console'
]
复制代码
并使用下面的替代:npm
features = [
'CanvasRenderingContext2d',
'CssStyleDeclaration',
'Document',
'Element',
'EventTarget',
'HtmlCanvasElement',
'HtmlElement',
'MouseEvent',
'Node',
'Window',
]
复制代码
上面的 features 列表是咱们将在本例中须要使用的一些 features。canvas
打开文件 src/lib.rs
。api
使用下面的代码替换掉文件中的 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));
复制代码
如今,咱们须要为 mouseDown
、mouseUp
、mouseMove
建立一个闭包(回调函数)。
{ 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 文章,点击这儿。