Visualizing Wave Equations in the Browser — Interference, Diffraction, and Resonance
Go deeper on this topic
Creative Coding
Mathematical simulations, nature visualization, and generative art in the browser
Article 5 of 12 in this series.
Next reads
The N-Body Problem in the Browser — Implementing Gravitational Simulation
Implementation details of gravitational calculations, vector operations, integration methods, and orbit rendering used in the NBody, SolarSystem, and GravityWell components.
Exploring the Reaction-Diffusion Parameter Space — Four Faces of the Gray-Scott Model
Varying feed/kill rates in the Gray-Scott Reaction-Diffusion model produces entirely different patterns: spots, stripes, coral, and cell division. Implementation on a 200x200 grid with exploration notes.
Sandpile Model and Self-Organized Criticality — Running the BTW Model in the Browser
Visualizing self-organized criticality in the browser through an implementation of the Bak-Tang-Wiesenfeld sandpile model. Measuring power-law avalanche distributions and the formation of fractal patterns.
Use the topic hub as a map, then continue to the next article in the series. New here? Start at the home hub →
Discretizing the Wave Equation
The continuous wave equation d^2u/dt^2 = c^2 nabla^2 u is discretized using the finite difference method. Time and space are divided into a grid, and derivatives are approximated with differences.
1D Wave Equation
const N = 500
const c = 2.0 // wave propagation speed
const dt = 0.01
const dx = 1.0
const u = new Float32Array(N) // current
const uPrev = new Float32Array(N) // one step back
const uNext = new Float32Array(N) // next step
function step1D() {
const r = (c * dt / dx) ** 2
for (let i = 1; i < N - 1; i++) {
uNext[i] = 2 * u[i] - uPrev[i] + r * (u[i + 1] - 2 * u[i] + u[i - 1])
}
// Boundary conditions (fixed ends)
uNext[0] = 0
uNext[N - 1] = 0
// Buffer swap
uPrev.set(u)
u.set(uNext)
}r = (c*dt/dx)^2 is the CFL Courant number. The solution becomes numerically unstable (diverges) if r > 1.
2D Wave Equation
Used in WaveEquation and ChladniPlate:
const W = 200
const H = 200
const u2d = new Float32Array(W * H)
const u2dPrev = new Float32Array(W * H)
const u2dNext = new Float32Array(W * H)
const damping = 0.999
function step2D() {
const r = (c * dt / dx) ** 2
for (let y = 1; y < H - 1; y++) {
for (let x = 1; x < W - 1; x++) {
const idx = y * W + x
const laplacian =
u2d[idx + 1] + u2d[idx - 1] +
u2d[idx + W] + u2d[idx - W] -
4 * u2d[idx]
u2dNext[idx] = (2 * u2d[idx] - u2dPrev[idx] + r * laplacian) * damping
}
}
u2dPrev.set(u2d)
u2d.set(u2dNext)
}damping = 0.999 provides energy dissipation. Without it, waves would reflect endlessly and become chaotic.
WaveInterference -- Interference Patterns
Two wave sources emit concentric waves, and the interference pattern is visualized:
function addSource(u: Float32Array, x: number, y: number, amplitude: number, t: number) {
const freq = 5.0
const idx = y * W + x
u[idx] += amplitude * Math.sin(2 * Math.PI * freq * t * dt)
}Placing two sources close together reveals constructive and destructive interference stripes (Young's interference fringes). The fringe spacing is determined by the ratio of the distance between sources to the wavelength.
ChladniPlate -- Resonance Patterns
Chladni figures visualize the resonance modes of thin plates. Driving a 2D wave equation at specific frequencies reveals lines of zero amplitude (nodal lines):
function renderChladni(ctx: CanvasRenderingContext2D, u: Float32Array) {
const imageData = ctx.createImageData(W, H)
for (let i = 0; i < W * H; i++) {
const amplitude = Math.abs(u[i])
// Nodal lines (near-zero amplitude) are white, antinodes are dark
const brightness = amplitude < 0.01 ? 255 : Math.max(0, 255 - amplitude * 500)
const px = i * 4
imageData.data[px] = brightness
imageData.data[px + 1] = brightness
imageData.data[px + 2] = brightness
imageData.data[px + 3] = 255
}
ctx.putImageData(imageData, 0, 0)
}In physical Chladni experiments, sand is sprinkled on a metal plate and the plate is bowed. The sand collects along nodal lines, revealing beautiful geometric patterns. This is the browser reproduction of that phenomenon.
Relationship with SoundWaves
The SoundWaves component uses Web Audio API's AnalyserNode to capture and render real-time waveform data. Unlike wave equation simulation, this is visualization of actual audio signals:
function drawWaveform(ctx: CanvasRenderingContext2D, dataArray: Uint8Array) {
ctx.beginPath()
const sliceWidth = ctx.canvas.width / dataArray.length
for (let i = 0; i < dataArray.length; i++) {
const v = dataArray[i] / 128.0
const y = (v * ctx.canvas.height) / 2
if (i === 0) ctx.moveTo(0, y)
else ctx.lineTo(i * sliceWidth, y)
}
ctx.stroke()
}Even under the shared theme of "waves," physics simulation (WaveEquation) and signal processing (SoundWaves) have entirely different implementations.
Performance Optimization
A 200x200 grid 2D simulation updates 40,000 cells per frame. Why Float32Array is used:
- Memory contiguity: TypedArray has the same memory layout as C arrays, yielding higher cache hit rates
- Fixed numeric type: The JIT compiler can skip type inference
- No GC: Fixed size means no garbage collection runs
Loop processing can be 2-3x faster compared to regular number[] in some cases.
Summary: Technologies and Tools Used for Wave Equations
For understanding the mathematical background of wave phenomena, the books in the toolshelf are helpful. Stability conditions for difference methods and the CFL condition are essential knowledge for simulations in general.