Making the waves move
How the ocean background on this site is generated and animated.
The waves on this site used to be three SVG files sliding sideways. That worked, but it always felt a bit too much like a looping background asset. Fine from a distance, kind of dead if you watched it for more than a few seconds.
I ended up replacing that with generated SVG paths. Still simple, still just a decorative background, but now the water has a bit more of its own movement.
The layers
There are three wave layers: foreground, midground, and background. Each one has its own baseline, colour, opacity, blur, speed, and wave shape. The foreground wave is taller and sharper. The background wave is slower, darker, and slightly blurred.
That matters more than I expected. If every layer moves the same way, it immediately looks fake. If they each drift at a slightly different pace, your eye accepts it as depth.
The shape
Each wave line is built from a few sine waves added together. There is a main wave for the big motion, a secondary wave to stop it looking too regular, and a small detail wave for the little uneven bits.
Roughly, each sampled point does this:
const y =
baseline +
primaryWave +
secondaryWave +
detailWave;
The code samples points across the width of the screen, then joins them with quadratic curves so the path stays smooth. The fill path is just that line plus the bottom corners of the SVG.
The motion
The animation is not moving a finished SVG sideways.
Anime.js drives an angle value from 0 to 2π, and on each update the wave path is rebuilt from the new phase values.
So the wave is actually changing shape over time:
- the main phase shifts
- the smaller detail phase shifts faster
- the opacity breathes slightly
- each layer rises and falls by a small amount
None of those movements are big on their own. Together they make the background feel less like wallpaper.
Keeping it cheap
The wave only samples a limited number of points based on the container width. Enough to look smooth, not so many that it wastes work.
Math.max(56, Math.min(132, Math.round(width / 22)));
It also respects prefers-reduced-motion.
If someone has reduced motion turned on, the paths still render, but the animation does not run.
Was this necessary?
Absolutely not.
But I like small details like this. The page is still mostly text and links, and the waves sit quietly behind it. They just make the site feel a little less static without turning the whole thing into a screensaver.