Wave Laboratory
Side project · simulation & graphics
The lab needs a browser with WebGL 2. It runs on desktop and on phones.
At a glance
4 FFT cascades, from 1 km swells down to centimetre ripples
280 water columns, at most, sampled under a hull to integrate pressure
328 + 25 unit and long-running validation tests, counted by grep
The problem
I have always liked how much of the sea can be written as a short recipe. A real ocean is not one wave. It is thousands of simple waves of different lengths and directions added together, and oceanographers describe how much energy each one carries with a curve called a spectrum. Ships respond to that sum, not to any single wave.
I wanted a place to build a sea from those published recipes, drop ships into it, and watch and measure what happens, with the physics honest enough that the numbers mean something and the page still running smoothly in a browser tab.
Approach
The sea is built from named, published models. The energy curve comes from JONSWAP, Pierson–Moskowitz or Bretschneider spectra, with a depth correction (TMA) for shallow water and Hasselmann’s fetch-limited growth for seas built by wind. Waves are spread over directions, and the speed of each wave follows the dispersion relation, including water depth and surface tension. The surface is then synthesised with an inverse FFT on four grids of different sizes, with a “choppy” sideways displacement that sharpens crests.
Ships float by pressure integration: for each moment, the simulation sums the water pressure over the part of the hull that is underwater (buoyancy plus the Froude–Krylov wave force), then adds added mass, damping, propulsion, rudder and wind. A freak wave can be placed on purpose using NewWave theory, which lines up many components so their crests meet at one point and time. The Draupner crest, 18.5 m in a sea with a significant height of 11.9 m, is the reference case.
What I built
- Deterministic simulation. The same experiment file always produces the same sea. Physics runs at a fixed time step in a Web Worker, and the main thread keeps at most one request in flight, so a slow machine runs slower than real time rather than building a queue.
- What you see is what the ships feel. The GPU draws the sea from the same amplitudes the CPU physics uses. On each new sea it compares one texel with the CPU result and logs an error if they disagree. The browser tests fail if that error appears.
- Instruments. Wave gauges, ship motion recorders, power spectra by Welch’s method, wave statistics, motion-sickness incidence and CSV export.
- Underwater view. Light fades per colour with depth for the chosen Jerlov water type, and the surface seen from below shows Snell’s window.
- Weather, damage and moorings. Gusts, squalls, rain and fog; flooding compartments; anchor chains modelled as elastic catenaries.
- Phones and accessibility. A phone layout with panel switching and touch targets of at least 44 px, except the camera controls drawn on top of the 3D view, which stay shorter so they do not cover the sea (the stylesheet sets them to 32 px). It also has light and dark themes, keyboard operation and WCAG 2.2 AA contrast. Accessibility is checked with axe in the browser tests on desktop and phone-sized viewports.
Results






Heads up. This is a teaching and exploration tool, not a certified seakeeping program. The reference notes list what is not modelled, such as current shear with depth, wave refraction by currents, and floodwater sloshing. The technical report covers these limits.
What I learned
- Putting the sea’s recipe in one place, then reading it from both the CPU and the GPU, made disagreements easy to find and fix.
- A test that fails when the two sides disagree is worth more than careful reading.
- Writing down what a model does not cover, and warning the user when they leave its range, is part of the physics.
- Accessibility checks in the browser tests caught issues I would not have found by eye.
Stack
TypeScript React Three.js WebGL 2 Web Workers Zod Zustand Vite Vitest Playwright axe-core