Wasm + SVG Manipulation
Why Generate SVG from Wasm?
SVG (Scalable Vector Graphics) is the web's native vector format. Generating it from Rust/Wasm combines the best of both worlds:
┌───────────────────────────────────────────────────┐
│ Wasm (Rust) │
│ │
│ ● Compute-heavy path generation │
│ ● Bezier curve math │
│ ● Data → chart conversion │
│ ● SVG path optimization │
│ ● Coordinate transformations │
│ │
│ Output: SVG string or path data │
└─────────────────────┬─────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────┐
│ Browser (JavaScript + DOM) │
│ │
│ ● Insert SVG into DOM │
│ ● Handle mouse/touch events │
│ ● CSS styling and animations │
│ ● Accessibility (ARIA labels) │
│ ● Responsive layout │
└───────────────────────────────────────────────────┘| Approach | Pros | Cons |
|---|---|---|
| JS chart library | Easy API, many options | Heavy bundle, slow for big data |
| Canvas (Wasm) | Pixel-perfect, very fast | Not scalable, no DOM events |
| SVG from Wasm | Scalable, accessible, fast math | String building overhead |
| WebGL from Wasm | Fastest rendering | Complex, no text/accessibility |
SVG Path Commands Reference
SVG paths use a mini-language of commands:
Command Parameters Description
──────── ──────────────── ──────────────────────────────────
M x y Move to Start a new sub-path
L x y Line to Draw straight line
H x Horizontal line Horizontal shortcut
V y Vertical line Vertical shortcut
Q cx cy x y Quadratic Bezier with 1 control point
C c1x c1y c2x c2y x y Cubic Bezier with 2 control points
A rx ry rot large sweep x y Elliptical arc
Z Close path Line back to start
Lowercase = relative coordinates (dx, dy)
Uppercase = absolute coordinates (x, y) Cubic Bezier: C c1x c1y, c2x c2y, x y
cp1●
╲
╲ ●cp2
start● ╲ ╱
╲ ╲ ╱
╲ ╲ ╱
╲ ╲╱
╲ ╱
╲ ╱
●endBezier Curves: The Math
Quadratic Bezier (3 points)
B(t) = (1-t)^2 * P0 + 2(1-t)t * P1 + t^2 * P2
P1 (control)
●
╱ ╲
╱ ╲
P0 ●╱ ╲● P2
(start) (end)
t=0.0 → at P0
t=0.5 → near P1 (but not on it!)
t=1.0 → at P2Cubic Bezier (4 points)
B(t) = (1-t)^3 * P0 + 3(1-t)^2 * t * P1
+ 3(1-t) * t^2 * P2 + t^3 * P3
P1 ●─────● P2
│ │
│ curve│
P0 ● ● P3
Properties:
● Always passes through P0 and P3
● Tangent at P0 points toward P1
● Tangent at P3 points toward P2
● Curve is contained in convex hull of control pointsDe Casteljau's Algorithm
The most numerically stable way to evaluate a Bezier curve:
Level 0: P0 P1 P2 P3
\ / \ / \ /
Level 1: A=lerp B=lerp C=lerp
\ / \ /
Level 2: D=lerp E=lerp
\ /
Level 3: result=lerp(D,E,t)
Each level: new_point = (1-t) * left + t * rightBuilding Chart Libraries
Line chart with data points and grid
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
);
// Background
svg.push_str(&format!(
r#"<rect width="{}" height="{}" fill="#fafafa"/>"#,
width, height
));
// Grid lines
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
));
}
// Data path
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
));
// Data points
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
}Pie chart with arc paths
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; // Start at top
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 Optimization
Generated SVGs can be large. Common optimizations:
Before optimization After optimization
──────────────────── ────────────────────
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
Savings: ~40% smaller| Optimization | Size Reduction | Implementation |
|---|---|---|
| Remove unnecessary decimals | 10-30% | Round to 1 decimal |
| Use relative coordinates (lowercase) | 5-15% | Delta encoding |
| Merge consecutive same-type commands | 5-10% | "L x y" chaining |
| Remove redundant whitespace | 5-10% | Minification |
| Simplify paths (Douglas-Peucker) | 10-50% | Point reduction |
Path simplification (Douglas-Peucker algorithm)
fn simplify_path(points: &[Point], epsilon: f64) -> Vec<Point> {
if points.len() < 3 { return points.to_vec(); }
// Find point with maximum distance from line (first→last)
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(); // Remove duplicate point
left.extend(right);
left
} else {
vec![points[0], *points.last().unwrap()]
}
}Dynamic Visualizations
The pattern for real-time SVG updates from Wasm:
import { generate_chart_path } from './pkg/my_crate';
function animate() {
const data = getLatestData(); // e.g., from WebSocket
const pathData = generate_chart_path(
new Float64Array(data),
canvas.width,
canvas.height
);
// Update existing SVG path (no DOM recreation!)
pathElement.setAttribute('d', pathData);
requestAnimationFrame(animate);
} Data Flow:
WebSocket ──→ JavaScript ──→ Wasm (path math) ──→ SVG DOM update
(collect (generate_chart_path) (setAttribute)
data) 60 fpsThis is faster than recreating the entire SVG because:
- Only the
dattribute changes, not the DOM structure - Wasm computes the path coordinates in microseconds
- The browser's SVG renderer handles anti-aliasing and scaling
Summary
Generating SVG from Wasm lets you combine Rust's computational power with the browser's native vector renderer. Use Wasm for the math-heavy parts (Bezier curves, coordinate transforms, path generation) and let the browser handle rendering, events, and styling. The SVG path mini-language is compact and efficient, and techniques like Douglas-Peucker simplification keep the output small. For real-time visualizations, update path d attributes directly rather than recreating DOM elements.