ViteでRust+WebAssembly入門

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

``` bash
✅ 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に２項目を追加：
plugins: [svelte(), wasm(), topLevelAwait()],

✅ src/lib.rsを下記コードで置き換える(ChatGPT4oに書いてもらった）
```
<details><summary>lib.rs</summary>

```rust

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


```
</details>

```bash
✅ クレートの追加：
$ cargo add web-sys
✅ Cargo.tomlの[dependences]に以下を追加：
web-sys = { version = "0.3.61", features = ["CanvasRenderingContext2d", "ImageData", "console",] }

✅ src/App.svelteを下記コードで置き換える
```
<details><summary>App.svelte</summary>

```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>
```
</details>

```bash
✅ lib.rsをWebAssemblyにコンパイル：
$ wasm-pack build

✅ webサーバーを起動：
$ npm run dev

✅ ブラウザーで表示：
http://localhost:5173

```

以下のように表示される：![スクリーンショット 2025-01-29 11.51.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191459/fa81c305-e78e-228f-e401-d7e22d725955.png)

最後にプロジェクトのディレクトリ構造を示します(ゴミファイルはほとんど削除してます）：
```bash
$ 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

```
以上です。