项目初始化

要求需要先安装好 Node.js,和 Rust 环境。

  1. 建一个新的文件夹,将工作目录切换到该目录下
1
npm init rust-webpack

如果能看到下面的图案就是成功了:

1
🦀 Rust + 🕸 WebAssembly + Webpack = ❤
  1. 安装依赖
1
npm install
  1. 安装 wasm-pack

Linux 和 Mac OSX 的操作系统可以使用 cURL 进行安装:

1
curl https://rustwasm.github.io/wasm-pack/installer/init . sh -sSf | sh

Windows 可以下载单独的 exe,进行安装:下载地址

  1. 运行服务器
1
npm run start

会自动安装 Rust 所需的依赖包,如果成功的话,开发者工具中终端界面可以看到 Hello, World

  1. 更新 Rust 版本

目前模板的 Rust 版本为 2018(2022年7月5日时),在 cargo.toml 将版本改成 2021

1
edition = "2021"
  1. 更新依赖的版本

cargo.toml 中的依赖也不是最新的,可以更新到新的版本。

Visusal Studio Code 中有 Crates 插件,可以获取依赖的版本信息。

1
2
3
4
5
[dev-dependencies]
wasm-bindgen-test = "0.3.31"
futures = "0.3.21"
js-sys = "0.3.22"
wasm-bindgen-futures = "0.4.31"
  1. 更新 console_error_panic_hook

这是个非常有用的库,一看名字就知道是用来 Debug 的,目前最新是 0.1.7

1
2
[target."cfg(debug_assertions)".dependencies]
console_error_panic_hook = "0.1.7"

绘制图形到 Canvas

可以使用 <canvas> 将图形绘制到浏览器窗口中,在 static\index.html<body> 后添加 <canvas>

1
2
3
<body>
<canvas id="canvas" tabindex="0" height="600" width="600">Your browser does not support the canvas.</canvas>
<!-- ... 不要删除后面的 <script> ... -->

终于到了写 Rust 代码的时候了!

lib.rs 引入依赖

1
use wasm_bindgen::JsCast;

原有代码里的 #[cfg(debug_assertions)] 可以删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::console;

// When the `wee_alloc` feature is enabled, this uses `wee_alloc` as the global
// allocator.
//
// If you don't want to use `wee_alloc`, you can safely delete this.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

// This is like the `main` function, except for JavaScript.
#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
// This provides better error messages in debug mode.
// It's disabled in release mode so it doesn't bloat up the file size.

console_error_panic_hook::set_once();

let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let canvas = document
.get_element_by_id("canvas")
.unwrap()
.dyn_into::<web_sys::HtmlCanvasElement>()
.unwrap();

let context = canvas
.get_context("2d")
.unwrap()
.unwrap()
.dyn_into::<web_sys::CanvasRenderingContext2d>()
.unwrap();

context.move_to(300.0, 0.0);
context.begin_path();
context.line_to(0.0, 600.0);
context.line_to(600.0, 600.0);
context.line_to(300.0, 0.0);
context.close_path();
context.stroke();
context.fill();

Ok(())
}

需要注意的是,web_sys 使用了 features 来减小大小。所以使用的时候,必须查看文档,看属于哪个 features

console 后添加所需要的 features

1
2
3
[dependencies.web-sys]
version = "0.3.22"
features = ["console", "Window", "Document", "HtmlCanvasElement", "CanvasRenderingContext2d"]

Rust 作为静态语言,确实要比 JavaScript 更加繁杂。如果这个图形纯粹使用 JavaScript 的话,看起来就就简单多了:

  • JavaScript 中,window 有可能是 null 或者 undefined,对于 Rust 来讲,就是 Option<Window>,可以使用 unwrap 来获取 window

  • 使用 get_element_by_id 获取 canvas 后,得到的是 Option<Element>,但 Element 并没有函数关联到 canvas。JavaScript 中,可以使用 get_context 去尝试获取,如果没有的话,会抛出异常。对于 Rust,则需要使用 dyn_into 强制转换到 HtmlCanvasElement

  • get_context("2d") 之后使用了两个 unwrap,这是因为它返回的是 Result<Option<Object>, JsValue>

绘制谢尔宾斯基三角形

谢尔宾斯基(Sierpiński)三角形是分形中的经典图形之一,用递归来实现其实非常简单的。

先把绘制三角形的代码抽象为一个函数,便于多次调用:

1
2
3
4
5
6
7
8
9
10
fn draw_triangle(context: &web_sys::CanvasRenderingContext2d, points: [(f64, f64); 3]) {
let [top, left, right] = points;
context.move_to(top.0, top.1);
context.begin_path();
context.line_to(left.0, left.1);
context.line_to(right.0, right.1);
context.line_to(top.0, top.1);
context.close_path();
context.stroke();
}

那么就只需要调用函数进行三角形的绘制,

1
draw_triangle(&context, [(300.0, 0.0), (0.0, 600.0), (600.0, 600.0)]);

由于函数没有加上 context.fill(),所以图案并没有填充成为黑色。

谢尔宾斯基三角形非常简单,只要 取边长中点 (两个坐标之和除以 2)继续进行相同的绘制过程。

1
2
3
4
draw_triangle(&context, [(300.0, 0.0), (0.0, 600.0), (600.0, 600.0)]);
draw_triangle(&context, [(300.0, 0.0), (150.00, 300.0), (450.0, 300.0)]);
draw_triangle(&context, [(150.0, 300.0), (0.0, 600.0), (300.0, 600.0)]);
draw_triangle(&context, [(450.0, 300.0), (300.0, 600.0), (600.0, 600.0)]);

我们将上述过程作为谢尔宾斯基三角形绘制的基础,使用递归绘制图案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn midpoint(point_1: (f64, f64), point_2: (f64, f64)) -> (f64, f64) {
((point_1.0 + point_2.0) / 2.0, (point_1.1 + point_2.1) / 2.0)
}

fn sierpinski(context: &web_sys::CanvasRenderingContext2d, points: [(f64, f64); 3], depth: u8) {
draw_triangle(&context, points);

let depth = depth - 1;
let [top, left, right] = points;

if depth > 0 {
// 计算中点
let left_middle = midpoint(top, left);
let right_middle = midpoint(top, right);
let bottom_middle = midpoint(left, right);

sierpinski(&context, [top, left_middle, right_middle], depth);
sierpinski(&context, [left_middle, left, bottom_middle], depth);
sierpinski(&context, [right_middle, bottom_middle, right], depth);
}
}

调用这个函数,并且设置 depth 为 4:

1
sierpinski(&context, [(300.0, 0.0), (0.0, 600.0), (600.0, 600.0)], 4);

如果想要跟最开始图案一样填充黑色,只需要在最后一层进行填充颜色。可以考虑加入一个 bool 值作为是否填充的判断:

1
2
3
4
5
6
7
8
9
fn draw_triangle(context: &web_sys::CanvasRenderingContext2d, points: [(f64, f64); 3], fill: bool) {
let [top, left, right] = points;

// ...

if fill {
context.fill();
}
}

之后判断是否是最后一层,

1
2
let depth = depth - 1;
let fill = if depth == 0 { true } else { false };

小结

通过本文的铺垫,我们就可以考虑使用 Rust 开发 Web 游戏了(思考)。