← Back to Lessons Lesson 43 of 48
Intermediate graphics

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●    ╲      ╱
        ╲    ╲    ╱
         ╲    ╲  ╱
          ╲    ╲╱
           ╲   ╱
            ╲ ╱
             ●end

Bezier 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 P2

Cubic 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 points

De 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 * right

Building 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 fps

This is faster than recreating the entire SVG because:

  1. Only the d attribute changes, not the DOM structure
  2. Wasm computes the path coordinates in microseconds
  3. 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.

Try It