Wave Interference in Rust + WebAssembly
This post explores embedding a Rust-compiled WebAssembly module directly into a Jekyll page. The computation — evaluating a damped wave interference pattern on a 100×100 grid — runs entirely in Rust/WASM. Three.js renders the 3D surface; Plotly renders a 2D cross-section. Both update live as you adjust the parameters.
The demo
Two damped radial waves emanate from configurable source positions and interfere:
\[z(x,y) = e^{-d\,r_1}\cos(k\,r_1) + e^{-d\,r_2}\cos(k\,r_2 + \varphi)\]where $r_1$ is the distance from the origin and $r_2$ is the distance from the second source. Use the GUI panel (top-right) to adjust parameters. Orbit with left-click drag, zoom with scroll, pan with right-click drag.
Left-drag: orbit · Scroll: zoom · Right-drag: pan · Blue = trough · Red = crest · Yellow line = cross-section
How it works
The Rust library exposes three functions via wasm-bindgen:
#[wasm_bindgen]
pub fn compute_surface(
n: usize, range: f32,
k: f32, damping: f32, phase: f32,
src2_x: f32, src2_y: f32,
) -> Vec<f32> { ... }
#[wasm_bindgen]
pub fn compute_cross_section(
n: usize, range: f32,
k: f32, damping: f32, phase: f32,
src2_x: f32, src2_y: f32, y_slice: f32,
) -> Vec<f32> { ... }
#[wasm_bindgen]
pub fn linspace(n: usize, range: f32) -> Vec<f32> { ... }
wasm-bindgen converts Vec<f32> returns to a JavaScript Float32Array — directly usable as a Three.js BufferAttribute without any additional copying or conversion. The N=100 grid means 10,000 evaluations of exp + cos per parameter change, all in compiled native code.
The JS side only handles rendering: it updates vertex positions and colors in the existing BufferGeometry and calls computeVertexNormals(), then passes the cross-section array to Plotly.
Building from source
# Install prerequisites (once)
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
# Build and copy assets
cd rust/
bash build.sh
The build script runs wasm-pack build --target web, which produces an ES module (wave_surface.js) and the WASM binary (wave_surface_bg.wasm), then copies both to assets/wasm/. Jekyll serves them as static files; the post imports them with <script type="module">.
The --target web flag is the key choice for a no-bundler setup: it generates an ES module with a top-level init() that fetches and instantiates the .wasm file by URL, compatible with any static file server.
Basem Rajjoub