Rust+WebAssemblyの入門記事です。
環境はこの記事を書いている最新版で、
vite: 5.4.11, node: v22.12.0, npm: 11.0.0, cargo: 1.83.0, wasm-pack: 0.13.1, macos: 15.3
以下のyoutube記事を参考にしています
https://www.youtube.com/watch?v=8zDYoprO358
✅ wasm-packが未インストールであればインストール:
$ cargo install wasm-pack
✅ wasm-packプロジェクトの生成(プロジェクト名は任意で、今回はwasm-mandelbrot):
$ wasm-pack new wasm-mandelbrot
✅ 同一ディレクトリ内にViteプロジェクトを生成:
$ cd wasm-mandelbrot
$ npm init vite . # 最後のピリオドに注意
今回は、メニューから以下を選択:
“Ignore existing files and continue” > Svelte > JavaScript
✅ node依存モジュールを追加 (これらが無いとブラウザーで派手なエラーが出る):
$ npm install -D vite-plugin-wasm vite-plugin-top-level-await
✅ vite.config.jsに以下を追加:
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";
そしてpluginsに2項目を追加:
plugins: [svelte(), wasm(), topLevelAwait()],
✅ src/lib.rsを下記コードで置き換える(ChatGPT4oに書いてもらった)
lib.rs
mod utils;
use wasm_bindgen::prelude::*;
use web_sys::{ImageData, console};
use wasm_bindgen::Clamped;
#[wasm_bindgen]
pub fn generate_mandelbrot(width: u32, height: u32, color: &str) -> ImageData {
let mut data = vec![0; (width * height * 4) as usize];
let (r, g, b) = hex_to_rgb(color);
for y in 0..height {
for x in 0..width {
let cx = x as f64 / width as f64 * 3.5 - 2.5;
let cy = y as f64 / height as f64 * 2.0 - 1.0;
let mut zx = 0.0;
let mut zy = 0.0;
let mut i = 0;
while zx * zx + zy * zy < 4.0 && i < 255 {
let tmp = zx * zx - zy * zy + cx;
zy = 2.0 * zx * zy + cy;
zx = tmp;
i += 1;
}
let pixel_index = (y * width + x) as usize * 4;
data[pixel_index] = (r * i as f64) as u8;
data[pixel_index + 1] = (g * i as f64) as u8;
data[pixel_index + 2] = (b * i as f64) as u8;
data[pixel_index + 3] = 255;
}
}
console::log_1(&"Mandelbrot generation complete".into());
ImageData::new_with_u8_clamped_array_and_sh(Clamped(&data[..]), width, height).unwrap()
}
fn hex_to_rgb(hex: &str) -> (f64, f64, f64) {
let hex = hex.trim_start_matches('#');
let r = u8::from_str_radix(&hex[0..2], 16).unwrap() as f64 / 255.0;
let g = u8::from_str_radix(&hex[2..4], 16).unwrap() as f64 / 255.0;
let b = u8::from_str_radix(&hex[4..6], 16).unwrap() as f64 / 255.0;
(r, g, b)
}
✅ クレートの追加:
$ cargo add web-sys
✅ Cargo.tomlの[dependences]に以下を追加:
web-sys = { version = "0.3.61", features = ["CanvasRenderingContext2d", "ImageData", "console",] }
✅ src/App.svelteを下記コードで置き換える
App.svelte
<script>
import { onMount } from 'svelte';
import { generate_mandelbrot } from '../pkg/wasm_mandelbrot';
let canvas;
let width = 1280;
let height = 800;
let color = '#ffff00';
const drawMandelbrot = async () => {
const ctx = canvas.getContext('2d');
console.log("Canvas context:", ctx);
const imageData = generate_mandelbrot(width, height, color);
console.log("Generated ImageData:", imageData);
ctx.putImageData(imageData, 0, 0);
};
onMount(drawMandelbrot);
</script>
<div>
<label>
Width:
<input type="number" bind:value={width} min="1" />
</label>
<label>
Height:
<input type="number" bind:value={height} min="1" />
</label>
<label>
Color:
<input type="color" bind:value={color} />
</label>
<button on:click={drawMandelbrot}>Draw</button>
</div>
<canvas bind:this={canvas} width={width} height={height}></canvas>
✅ lib.rsをWebAssemblyにコンパイル:
$ wasm-pack build
✅ webサーバーを起動:
$ npm run dev
✅ ブラウザーで表示:
http://localhost:5173
最後にプロジェクトのディレクトリ構造を示します(ゴミファイルはほとんど削除してます):
$ tree -L 2
├── Cargo.lock
├── Cargo.toml
├── LICENSE_APACHE
├── LICENSE_MIT
├── README.md
├── index.html
├── jsconfig.json
├── node_modules
│ ├── @ampproject
│ ~
│ └── zimmerframe
├── package-lock.json
├── package.json
├── pkg
│ ├── README.md
│ ├── package.json
│ ├── wasm_mandelbrot.d.ts
│ ├── wasm_mandelbrot.js
│ ├── wasm_mandelbrot_bg.js
│ ├── wasm_mandelbrot_bg.wasm
│ └── wasm_mandelbrot_bg.wasm.d.ts
├── public
├── src
│ ├── App.svelte
│ ├── lib.rs
│ ├── main.js
│ ├── utils.rs
│ └── vite-env.d.ts
├── svelte.config.js
├── target
│ ├── CACHEDIR.TAG
│ ├── debug
│ ├── release
│ ├── tmp
│ └── wasm32-unknown-unknown
├── tests
│ └── web.rs
└── vite.config.js
以上です。