← レッスン一覧に戻る レッスン 43 / 48
中級 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 fpsSVG全体を再作成するよりも高速な理由:
d属性のみが変わり、DOM構造は変わらない- Wasmがパス座標をマイクロ秒単位で計算
- ブラウザのSVGレンダラーがアンチエイリアシングとスケーリングを処理
まとめ
WasmからのSVG生成により、Rustの計算能力とブラウザのネイティブベクターレンダラーを組み合わせることができます。計算負荷の高い部分(ベジェ曲線、座標変換、パス生成)にはWasmを使い、描画、イベント処理、スタイリングはブラウザに任せましょう。SVGパスのミニ言語はコンパクトで効率的であり、Douglas-Peucker法などの簡略化手法により出力を小さく保てます。リアルタイムビジュアライゼーションでは、DOM要素を再作成するのではなく、パスのd属性を直接更新しましょう。