中級 graphics

Wasm + SVG操作

なぜWasmからSVGを生成するのか?

SVG(Scalable Vector Graphics)はWebネイティブのベクター形式です。Rust/Wasmから生成することで、両方の長所を活かせます:

  ┌───────────────────────────────────────────────────┐
  │  Wasm (Rust)                                      │
  │                                                   │
  │  ● 計算負荷の高いパス生成                           │
  │  ● ベジェ曲線の数学                                │
  │  ● データ → チャート変換                            │
  │  ● SVGパスの最適化                                 │
  │  ● 座標変換                                       │
  │                                                   │
  │  出力: SVG文字列またはパスデータ                     │
  └─────────────────────┬─────────────────────────────┘
                        │
                        ▼
  ┌───────────────────────────────────────────────────┐
  │  ブラウザ (JavaScript + DOM)                       │
  │                                                   │
  │  ● SVGをDOMに挿入                                 │
  │  ● マウス/タッチイベントの処理                       │
  │  ● CSSスタイリングとアニメーション                   │
  │  ● アクセシビリティ(ARIAラベル)                    │
  │  ● レスポンシブレイアウト                            │
  └───────────────────────────────────────────────────┘
アプローチ 長所 短所
JSチャートライブラリ 簡単なAPI、多くの選択肢 重いバンドル、大データで遅い
Canvas (Wasm) ピクセル単位の精度、非常に高速 スケーラブルでない、DOMイベントなし
WasmからのSVG スケーラブル、アクセシブル、高速な計算 文字列構築のオーバーヘッド
WasmからのWebGL 最速の描画 複雑、テキスト/アクセシビリティなし

SVGパスコマンドリファレンス

SVGパスはコマンドのミニ言語を使用します:

  コマンド  パラメータ         説明
  ──────── ──────────────── ──────────────────────────────────
  M x y    移動             新しいサブパスを開始
  L x y    線を引く          直線を描画
  H x      水平線           水平方向のショートカット
  V y      垂直線           垂直方向のショートカット
  Q cx cy x y  2次ベジェ    制御点1つのベジェ曲線
  C c1x c1y c2x c2y x y    3次ベジェ(制御点2つ)
  A rx ry rot large sweep x y  楕円弧
  Z        パスを閉じる      始点に戻る線

  小文字 = 相対座標 (dx, dy)
  大文字 = 絶対座標 (x, y)
  3次ベジェ: C c1x c1y, c2x c2y, x y

       cp1●
          ╲
           ╲        ●cp2
  始点●     ╲      ╱
       ╲    ╲    ╱
        ╲    ╲  ╱
         ╲    ╲╱
          ╲   ╱
           ╲ ╱
            ●終点

ベジェ曲線の数学

2次ベジェ(3点)

  B(t) = (1-t)^2 * P0 + 2(1-t)t * P1 + t^2 * P2

          P1(制御点)
          ●
         ╱ ╲
        ╱   ╲
  P0 ●╱     ╲● P2
     (始点)   (終点)

  t=0.0 → P0上
  t=0.5 → P1付近(しかしP1上ではない!)
  t=1.0 → P2上

3次ベジェ(4点)

  B(t) = (1-t)^3 * P0 + 3(1-t)^2 * t * P1
       + 3(1-t) * t^2 * P2 + t^3 * P3

  P1 ●─────● P2
     │       │
     │  曲線 │
  P0 ●       ● P3

  性質:
  ● 常にP0とP3を通過する
  ● P0での接線はP1方向を向く
  ● P3での接線はP2方向を向く
  ● 曲線は制御点の凸包に含まれる

ド・カステリョのアルゴリズム

ベジェ曲線を評価する最も数値的に安定した方法です:

  レベル0:  P0          P1          P2          P3
              \        / \        / \        /
  レベル1:   A=lerp    B=lerp    C=lerp
                \      / \      /
  レベル2:     D=lerp   E=lerp
                  \    /
  レベル3:       結果=lerp(D,E,t)

  各レベル: new_point = (1-t) * left + t * right

チャートライブラリの構築

データポイントとグリッド付き折れ線グラフ

fn generate_chart_svg(
    data: &[(f64, f64)],
    width: f64,
    height: f64,
) -> String {
    let mut svg = format!(
        r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {} {}">"#,
        width, height
    );

    // 背景
    svg.push_str(&format!(
        r#"<rect width="{}" height="{}" fill="#fafafa"/>"#,
        width, height
    ));

    // グリッド線
    for i in 0..=4 {
        let y = 20.0 + (height - 40.0) * i as f64 / 4.0;
        svg.push_str(&format!(
            r#"<line x1="20" y1="{:.0}" x2="{:.0}" y2="{:.0}" stroke="#eee"/>"#,
            y, width - 20.0, y
        ));
    }

    // データパス
    let path = data_to_path(data, width, height, 20.0);
    svg.push_str(&format!(
        r#"<path d="{}" fill="none" stroke="#4e79a7" stroke-width="2"/>"#,
        path
    ));

    // データポイント
    for (x, y) in normalize_points(data, width, height, 20.0) {
        svg.push_str(&format!(
            r#"<circle cx="{:.1}" cy="{:.1}" r="3" fill="#4e79a7"/>"#,
            x, y
        ));
    }

    svg.push_str("</svg>");
    svg
}

円弧パスによる円グラフ

fn pie_chart(values: &[f64], colors: &[&str], radius: f64) -> String {
    let total: f64 = values.iter().sum();
    let cx = radius + 10.0;
    let cy = radius + 10.0;
    let mut angle = -std::f64::consts::FRAC_PI_2; // 上部から開始

    let mut paths = Vec::new();
    for (i, &val) in values.iter().enumerate() {
        let sweep = 2.0 * std::f64::consts::PI * val / total;
        let x1 = cx + radius * angle.cos();
        let y1 = cy + radius * angle.sin();
        let x2 = cx + radius * (angle + sweep).cos();
        let y2 = cy + radius * (angle + sweep).sin();
        let large_arc = if sweep > std::f64::consts::PI { 1 } else { 0 };

        paths.push(format!(
            r#"<path d="M {:.1} {:.1} L {:.1} {:.1} A {:.1} {:.1} 0 {} 1 {:.1} {:.1} Z" fill="{}"/>"#,
            cx, cy, x1, y1, radius, radius, large_arc, x2, y2, colors[i % colors.len()]
        ));
        angle += sweep;
    }
    paths.join("\n")
}

SVGの最適化

生成されたSVGは大きくなることがあります。一般的な最適化手法を示します:

  最適化前                      最適化後
  ────────────────────        ────────────────────
  M 100.000000 200.000000     M100 200
  L 150.000000 180.000000     L150 180 200 160
  L 200.000000 160.000000     250 170 300 190Z
  L 250.000000 170.000000
  L 300.000000 190.000000
  Z

  削減量: 約40%小さく
最適化手法 サイズ削減 実装方法
不要な小数の削除 10-30% 小数第1位に丸める
相対座標(小文字)の使用 5-15% 差分エンコーディング
連続する同種コマンドの統合 5-10% "L x y"の連結
冗長な空白の除去 5-10% ミニフィケーション
パスの簡略化(Douglas-Peucker法) 10-50% 点の削減

パス簡略化(Douglas-Peuckerアルゴリズム)

fn simplify_path(points: &[Point], epsilon: f64) -> Vec<Point> {
    if points.len() < 3 { return points.to_vec(); }

    // 直線(始点→終点)から最大距離の点を見つける
    let (max_dist, max_idx) = points[1..points.len()-1].iter().enumerate()
        .map(|(i, p)| (point_line_distance(p, &points[0], points.last().unwrap()), i + 1))
        .max_by(|a, b| a.0.partial_cmp(&b.0).unwrap())
        .unwrap();

    if max_dist > epsilon {
        let mut left = simplify_path(&points[..=max_idx], epsilon);
        let right = simplify_path(&points[max_idx..], epsilon);
        left.pop(); // 重複点を除去
        left.extend(right);
        left
    } else {
        vec![points[0], *points.last().unwrap()]
    }
}

動的ビジュアライゼーション

Wasmからのリアルタイムなシーンの更新パターン:

import { generate_chart_path } from './pkg/my_crate';

function animate() {
    const data = getLatestData(); // 例: WebSocketから
    const pathData = generate_chart_path(
        new Float64Array(data),
        canvas.width,
        canvas.height
    );

    // 既存のSVGパスを更新(DOM再作成なし!)
    pathElement.setAttribute('d', pathData);
    requestAnimationFrame(animate);
}
  データフロー:

  WebSocket ──→ JavaScript ──→ Wasm(パス計算) ──→ SVG DOM更新
                (データ収集)    (generate_chart_path) (setAttribute)
                                                     60 fps

SVG全体を再作成するよりも高速な理由:

  1. d属性のみが変わり、DOM構造は変わらない
  2. Wasmがパス座標をマイクロ秒単位で計算
  3. ブラウザのSVGレンダラーがアンチエイリアシングとスケーリングを処理

まとめ

WasmからのSVG生成により、Rustの計算能力とブラウザのネイティブベクターレンダラーを組み合わせることができます。計算負荷の高い部分(ベジェ曲線、座標変換、パス生成)にはWasmを使い、描画、イベント処理、スタイリングはブラウザに任せましょう。SVGパスのミニ言語はコンパクトで効率的であり、Douglas-Peucker法などの簡略化手法により出力を小さく保てます。リアルタイムビジュアライゼーションでは、DOM要素を再作成するのではなく、パスのd属性を直接更新しましょう。

試してみる