前回、imageクレートを使ってシダを描画して、PNG画像として保存するプログラムを作ってみました。今回は、このプログラムをブラウザ上で動くようにWebAssemblyにしてみましょう。Rustを使うと比較的簡単にブラウザで動くように移植できるので手順を確認してみましょう。

  • RustのプログラムをWebAssemblyにビルドして、ブラウザ上でシダを描画したところ

    RustのプログラムをWebAssemblyにビルドして、ブラウザ上でシダを描画したところ

前回のおさらいとWebAssemblyについて

前回は、imageクレートを使って、シダを描画するプログラムを作りました。imageクレートを使う事で、手軽に画像を生成できることを確認しました。今回は、前回作ったシダを描画するプログラムをブラウザで動くように改良します。

そして、ブラウザでRustのプログラムを動かすには、WebAssembly形式にコンパイルして使います。WebAssemblyとは、主にWebブラウザ上で高速に動作するバイトコードの仕様のことです。主要なモダンブラウザでサポートされており、RustやC言語、Go言語など多くのプログラミング言語で記述したプログラムをブラウザで動かすことができます。なおWebAssemblyはWASMと略されますので、本稿でもWASMと略します。

そして、Rustで作成したWASMバイナリは小サイズであり、使い回しの良い物となっています。果たして、シダを描画するRustのプログラムがどのくらいのサイズにコンパイルされるかも楽しみに作業をしましょう。

Rustのプログラムをブラウザで動かす手順とポイント

Rustで作ったプログラムをブラウザで動かすには、まずRustのプログラムをWebAssembly形式にコンパイルします。そして、ブラウザからWASMバイナリを読み込んで実行するという手順になります。wasm-packというツールを利用することで、手軽にWebAssemblyを生成できます。

なお、Rustのプログラムをブラウザで動かす場合、JavaScriptのプログラムとどのように組み合わせるかという点がポイントとなります。今回のようにシダの画像を生成するプログラムの場合、生成した画像をどのようにして、JavaScriptに与えるかがポイントになります。

簡単にRustのプログラムをWASMとしてビルドする手順を確認してみましょう。

- (1) ライブラリとしてプロジェクトを作成
- (2) wasm-packをインストールし、プロジェクトにwasm-bindgenクレートを追加
- (3) 設定ファイルのCargo.tomlにcrate-typeを指定
- (4) プログラムを作成してwasm-packコマンドでビルドする
- (5) JavaScriptからWASMを読み込み、関数を呼び出す
- (6) Webサーバー上でHTMLを表示してテスト

手順が多いように感じますが、それほど難しいものではありません。一つずつ作業していきましょう。

(手順1) プロジェクトの作成

最初に、プロジェクトを作成しましょう。ターミナル(WindowsではPowerShell、macOSではターミナル.app)を起動して、以下のコマンドを実行しましょう。

# プロジェクトを作成
cargo init --lib

ブラウザ上でRustのプログラムを動かす場合には、ライブラリとして作成します。そのため、cargo initでプロジェクトを初期化する際に「--lib」オプションを指定します。 すると、下記のようなディレクトリ構成でひな形が作成されます。

.
├── Cargo.lock
├── Cargo.toml
└── src
    └── lib.rs

(手順2) ライブラリのインストール

WASMを作成するには、wasm-packが必要なのでインストールしましょう。そして、プロジェクトで利用するクレート(ライブラリ)もインストールしましょう。ターミナルで以下を実行します。

# WASMのためのライブラリをインストール
cargo install wasm-pack

# 必要なクレートをインストール
cargo add image
cargo add lazyrand
cargo add wasm-bindgen

(手順3) 設定ファイルを編集

最初に、設定ファイルの「Cargo.toml」を開いて、[lib]にcrate-typeを追加します。それで、Cargo.tomlを次のように書き換えます。

[package]
name = "fern"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
image = "0.24.7"
lazyrand = "0.1.9"
wasm-bindgen = "0.2.87"

(手順4) プログラムを記述してwasm-packでビルドしよう

そして、前回作ったシダを描画するプログラムを、src/lib.rsに書き込みます。ただし、PNG形式でファイルに保存する部分を削除して、代わりに、Vecを関数の戻り値として返すように修正します。

また、描画サイズを手軽に調整できるように、再帰的に呼び出す関数drawの引数を追加しました。そして、RGB画像のオブジェクトを表す「RgbImage」をHTMLのCavnvasで手軽に描画できるように「RgbaImage」に置換しました。

以下が、プログラム「src/lib.rs」の内容です。上記の修正以外は、前回作ったものとほとんど同じです。

use wasm_bindgen::prelude::*;
use image::{Rgba, RgbaImage};

// JavaScriptから呼び出せるよう #[wasm_bindgen] を追加して関数を定義 --- (*1)
#[wasm_bindgen]
pub fn draw_image(width: u32, height: u32) -> Vec<u8> {
    // 画像オブジェクトを作成
    let mut img = RgbaImage::new(width, height);
    // シダを描画
    draw(&mut img, 23, 0.0, 0.0, width, height);
    img.into_vec() // Vec<u8>に変換して返す --- (*2)
}

// シダを描画する関数 --- (*3)
fn draw(img: &mut RgbaImage, k: i64, x: f64, y: f64, w: u32, h: u32) {
    // 計算用のクロージャを定義
    let w1x = |x, y| 0.836 * x + 0.044 * y;
    let x1y = |x, y| -0.044 * x + 0.836 * y + 0.169;
    let w2x = |x, y| -0.141 * x + 0.302 * y;
    let w2y = |x, y| 0.302 * x + 0.141 * y + 0.127;
    let w3x = |x, y| 0.141 * x - 0.302 * y;
    let w3y = |x, y| 0.302 * x + 0.141 * y + 0.169; 
    let w4x = |_x, _y| 0.0;
    let w4y = |_x, y| 0.175337 * y;
    if k > 0 {
        // 再帰的に描画
        draw(img, k - 1, w1x(x, y), x1y(x, y), w, h);
        if lazyrand::rand_f64() < 0.3 {
            draw(img, k - 1, w2x(x, y), w2y(x, y), w, h);
        }
        if lazyrand::rand_f64() < 0.3 {
            draw(img, k - 1, w3x(x, y), w3y(x, y), w, h);
        }
        if lazyrand::rand_f64() < 0.3 {
            draw(img, k - 1, w4x(x, y), w4y(x, y), w, h);
        }
    }
    // 座標を計算
    let ss = h as f64 * 0.97;
    let xx = (x * ss + (w as f64) * 0.5) as u32 - 1;
    let yy = ((h as f64) - y * ss) as u32 - 1;
    // 描画 --- (*4)
    img.put_pixel(xx, yy, Rgba([0, 170, 0, 255]));
}

プログラムの(*1)ではJavaScriptからRustの関数が呼び出せるように、#[wasm_bindgen]を付けて関数draw_imageを定義します。この関数では、画像オブジェクトを生成して、シダを描画します。そして、(*2)でシダの画像データをVec型に変換して返します。

(*3)では再帰的にシダを描画する関数drawを定義します。実際に描画を行っているのが(*4)です。赤・緑・青に透明色を加えたRgba関数を指定して描画する色を指定します。

それではプロジェクトをビルドしてみましょう。ターミナルで以下のコマンドを実行しましょう。

wasm-pack build --target web --release

すると、pkgディレクトリにWASMバイナリの「fern_bg.wasm」と、手軽にWASMを読み込むJavaScriptのライブラリ「fern.js」が作成されます。

ビルドできたら、シダを描画するWASMバイナリの「fern_bg.wasm」のファイルサイズを確認してみましょう。わずか26KBです。非常にコンパクトなバイナリにコンパイルすることができました。

(手順5) JavaScriptでWASMを読み込もう

次にWASMを読み込んで実行するHTMLファイル「index.html」をプロジェクトのルートディレクトリに作成しましょう。以下のソースコードは、WASMを読み出して、Rustで記述した関数draw_imageを呼び出し、HTMLのキャンバスに描画するというものです。

<!DOCTYPE html><html><head>
    <meta charset="utf-8" /><title>シダを描画</title>
</head><body style="background-color: black;">
    <h3 style="color:white">ブラウザにシダを描画</h3>
    <canvas id="canvas" style="border: 1px dotted white"></canvas>
    <script type="module">
        // WASMを読み込む
        import init, { draw_image } from "./pkg/fern.js";
        init().then(() => {
            // WASMで定義した関数draw_imageを呼び出す
            const width = 614
            const imageData = draw_image(width, width)
            // 画像データをCanvasに描画する
            const canvas = document.getElementById("canvas")
            canvas.width = canvas.height = width
            const ctx = canvas.getContext("2d")
            const img = new ImageData(new Uint8ClampedArray(imageData), width)
            ctx.putImageData(img, 0, 0, 0, 0, width, width)
        });
    </script>
</body></html>

ここで改めてプロジェクトのファイル構成を確認してみましょう。次のような構成となります。

├── Cargo.lock
├── Cargo.toml
├── index.html ...... 先ほど作ったHTMLファイル
├── target ...... 中間ファイルなどが生成される
├── pkg ...... 生成されたWASMの出力先
│   ├── fern.d.ts
│   ├── fern.js
│   ├── fern_bg.wasm
│   ├── fern_bg.wasm.d.ts
│   └── package.json
└── src
    └── lib.rs

(手順6) Webサーバーを起動してHTMLをブラウザで表示しよう

最後に、ローカルにWebサーバーを起動して動作テストしてみましょう。ここでは、動作テストにPythonを利用します。Pythonがインストールされていなければこちらからダウンロードしてインストールしてください。

ターミナルで以下のコマンドを実行します。これによりPythonのWebサーバーが起動します。

# Windowsの場合
python -m http.server 8888
# macOSの場合
python3 -m http.server 8888

そして、ブラウザで「http://localhost:8888」にアクセスします。すると、次のようなシダの画像が描画されます。再帰を利用した計算量の多いプログラムですが、一瞬で描画完了するのを確認できるでしょう。

  • ブラウザにシダが描画される

    ブラウザにシダが描画される

ところで、どうしてWebサーバーが必要なのかと言えば、ブラウザのセキュリティ制約のためです。ブラウザからローカル領域に配置したWASMを直接読み込むことはできないようになっています。そのため、必ず、上記のようにWebサーバーをローカルに起動して、サーバー経由でWASMを読み込む必要があります。

この記事は
Members+会員の方のみ御覧いただけます

ログイン/無料会員登録

会員サービスの詳細はこちら