Tutorial · Fluid Dynamics · Meteorology · JavaScript
📅 July 2026 ⏱ ≈ 40 min 🎯 Intermediate

Shallow Water Equations: Build a Tsunami Simulation in an Hour

The shallow water equations are the workhorse of tsunami modelling, storm-surge forecasting and dam-break analysis. They're also simple enough to derive, discretise and get running on a Canvas 2D heightmap in well under an hour — this tutorial walks through every step, from the physics to the finite-difference code.

1. The shallow water assumption

A tsunami in the open Pacific has a wavelength of 100-200 km but the ocean is only ~4 km deep — a depth-to-wavelength ratio of roughly 1:30. Whenever a wave's wavelength is much larger than the fluid depth, the vertical velocity and acceleration inside the fluid column are negligible compared to the horizontal ones. This lets us collapse the full 3D incompressible Navier-Stokes equations (see our companion article on Navier-Stokes in the atmosphere for the general form) into a much simpler 2D system, tracking only water-surface height h(x, y, t) and a depth-averaged horizontal velocity field u(x, y, t) — no vertical dimension at all.

Why "shallow" doesn't mean shallow: the term refers to the depth being small relative to the wavelength, not to any absolute number. Tsunamis in 4 km deep ocean are shallow-water waves; a 10 cm ripple in a 5 cm deep puddle is not (its wavelength is comparable to the depth) — that's a deep-water wave regime with different dispersion physics.

2. The shallow water equations

Dropping vertical structure gives a compact system of three coupled PDEs:

Continuity (mass conservation): ∂h/∂t + ∂(hu)/∂x + ∂(hv)/∂y = 0 Momentum (x): ∂u/∂t + u ∂u/∂x + v ∂u/∂y = -g ∂h/∂x Momentum (y): ∂v/∂t + u ∂v/∂x + v ∂v/∂y = -g ∂h/∂y

h is the total water column height, u and v are the depth-averaged horizontal velocity components, and g is gravitational acceleration (9.81 m/s²). The right-hand sides are the pressure-gradient force — hydrostatic pressure at any point is proportional to the water column above it, so a sloping surface pushes fluid downhill exactly like a pressure-gradient force in the full Navier-Stokes momentum equation.

For small-amplitude waves, we can drop the non-linear advection terms (u∂u/∂x etc.) entirely, giving the linearised shallow water equations — good enough for open-ocean tsunami propagation, though the full non-linear terms become important as the wave shoals and steepens near the coast.

3. Wave speed and shoaling

Linearising and combining the three equations gives a classic wave equation with speed:

c = √(g · H)

H is the undisturbed water depth. In 4 km deep ocean, c = √(9.81 × 4000) ≈ 198 m/s — over 700 km/h, as fast as a jet airliner, which is why a tsunami generated by an earthquake can reach a coastline thousands of kilometres away within hours. Because c depends on depth, as the wave approaches shore and H decreases, the wave slows down.

Conservation of energy flux (roughly, energy flux ∝ amplitude² × group velocity must stay constant) means that as speed drops, amplitude must rise to compensate — a phenomenon called shoaling. Green's law gives a common approximation:

A₂ / A₁ = (H₁ / H₂)^(1/4)

A tsunami that's a barely-noticeable 0.5 m swell in 4 km deep ocean can grow to several metres by the time H drops to a few tens of metres near shore — exactly what the shallow water equations predict, and exactly why an earthquake felt at sea produces a devastating wave on land.

4. Finite-difference discretisation

To simulate this on a computer, we discretise h, u and v onto a regular 2D grid with spacing dx, and replace spatial derivatives with central differences:

∂h/∂x ≈ (h[i+1,j] − h[i-1,j]) / (2·dx)

and advance the grid forward in time with a simple explicit (forward Euler) time-step: compute all spatial derivatives at the current time, use them to compute the rate of change of h, u, v, then step everything forward by dt.

5. The CFL stability condition

Explicit schemes are only stable if information can't cross more than one grid cell per time-step. Since the fastest signal in this system travels at the wave speed c = √(gH), the Courant-Friedrichs-Lewy (CFL) condition requires:

dt ≤ dx / √(g · H_max)

Pick dt any larger and the simulation will diverge into NaN within a few frames — this is the single most common bug when first implementing a shallow-water solver.

6. Implementation in JavaScript

class ShallowWaterSim {
  constructor(nx, ny, dx) {
    this.nx = nx; this.ny = ny; this.dx = dx;
    this.g = 9.81;
    this.H  = new Float32Array(nx * ny).fill(50);  // still-water depth (bathymetry)
    this.h  = new Float32Array(nx * ny);              // surface elevation (perturbation)
    this.u  = new Float32Array(nx * ny);
    this.v  = new Float32Array(nx * ny);
  }

  idx(i, j) { return j * this.nx + i; }

  // Drop a Gaussian bump — simulates an earthquake-generated initial displacement
  addDisturbance(cx, cy, amplitude, radius) {
    for (let j = 0; j < this.ny; j++)
      for (let i = 0; i < this.nx; i++) {
        const d = Math.hypot(i - cx, j - cy);
        this.h[this.idx(i, j)] += amplitude * Math.exp(-(d * d) / (2 * radius * radius));
      }
  }

  step(dt) {
    const { nx, ny, dx, g, H, h, u, v } = this;
    const hNew = new Float32Array(h.length);
    const uNew = new Float32Array(u.length);
    const vNew = new Float32Array(v.length);

    for (let j = 1; j < ny - 1; j++)
      for (let i = 1; i < nx - 1; i++) {
        const k = this.idx(i, j);
        const total = H[k] + h[k]; // total column height

        // Central differences for gradients
        const dhdx = (h[this.idx(i+1,j)] - h[this.idx(i-1,j)]) / (2 * dx);
        const dhdy = (h[this.idx(i,j+1)] - h[this.idx(i,j-1)]) / (2 * dx);

        // Momentum: du/dt = -g * dh/dx  (linearised, no advection)
        uNew[k] = u[k] - g * dhdx * dt;
        vNew[k] = v[k] - g * dhdy * dt;

        // Continuity: dh/dt = -div(total * u)
        const fluxRight = total * u[this.idx(i+1,j)];
        const fluxLeft  = total * u[this.idx(i-1,j)];
        const fluxUp    = total * v[this.idx(i,j+1)];
        const fluxDown  = total * v[this.idx(i,j-1)];
        const div = (fluxRight - fluxLeft) / (2 * dx) + (fluxUp - fluxDown) / (2 * dx);
        hNew[k] = h[k] - div * dt;
      }

    this.h = hNew; this.u = uNew; this.v = vNew;
    this.applyBoundaries();
  }
}
Choosing dt automatically: compute dt = 0.4 * dx / Math.sqrt(g * maxDepth) once at startup (the 0.4 safety factor keeps you comfortably under the CFL limit) rather than hard-coding a number — it keeps the simulation stable if you change grid resolution or depth.

7. Reflecting boundaries and a coastline

A domain edge needs a boundary condition or the finite-difference stencil reads out of bounds. The simplest physically reasonable choice is a reflecting (solid wall) boundary: zero the normal velocity component and mirror the height gradient, so waves bounce back instead of vanishing or wrapping around.

applyBoundaries() {
  const { nx, ny, u, v, h } = this;
  for (let j = 0; j < ny; j++) {
    u[this.idx(0, j)]      = 0; u[this.idx(nx - 1, j)] = 0;
    h[this.idx(0, j)]      = h[this.idx(1, j)];
    h[this.idx(nx - 1, j)] = h[this.idx(nx - 2, j)];
  }
  // (repeat symmetrically for the top/bottom edges using v)
}

To simulate a beach and watch shoaling in action, make the still-water depth H shallower toward one edge of the grid instead of constant — a linear ramp from 50 m down to 0.5 m over the last 20% of the domain is enough to see the wave slow down and visibly grow in amplitude exactly as Green's law predicts, right before it "breaks" as the linear approximation breaks down at very shallow depth.

🌊 Try the live Tsunami simulation

See the exact equations from this tutorial running in real time — drop a disturbance and watch it propagate, slow down and amplify as it approaches the shore.

Open simulation →

Frequently Asked Questions

What will I learn in this tutorial?

Build a real-time shallow water simulation from scratch in JavaScript: the shallow water equations, wave speed, shoaling amplification, a finite-difference solver on a Canvas 2D heightmap, and reflecting boundaries.

What topics are covered in this tutorial?

This tutorial covers: The shallow water assumption, The shallow water equations, Wave speed and shoaling, Finite-difference discretisation, The CFL stability condition, Implementation in JavaScript, Reflecting boundaries and a coastline, Try the live simulation.

How long does this tutorial take?

This tutorial takes approximately 40 minutes to complete.

What prerequisites do I need before starting?

This is a Intermediate-level tutorial — no special preparation beyond basic JavaScript is assumed.