Wave Field
A deterministic wave sweeps across a grid in any direction you choose, turning each cell's position into a phase in the wave. No per-cell state, no randomness — just position, time, and a direction vector.
How it works
1. Project the cell onto the wave's direction. angle gives a unit vector. Dotting each cell's (x, y) against it collapses the 2D grid into a single number, travel — how far along the wave's direction that cell sits, regardless of the grid's actual shape.
2. Turn time + position into a phase. The wave's phase at a cell is time / duration (how far through one cycle the clock has gotten) plus travel × frequency (how far ahead or behind that cell is in space). Taken mod 1, this is what makes the wave visibly move: cells further along the direction vector hit the same phase later.
3. Shape the phase, don't just use it. A raw phase is a sawtooth — snaps back to 0 every cycle. bounce folds it into a triangle wave (0→1→0) instead. Either way, the result still passes through an easing curve (linear, smooth = smoothstep, inOut = cubic, elastic = overshoot) before it's remapped to [min, max].
The function itself doesn't know about DOM nodes, canvases, or React — it just maps (x, y, time) → number. useWaveField is the thin React layer that calls it every animation frame for a grid of cells; nothing stops you from feeding the same function into a canvas render loop or a WebGL uniform instead.
function waveField(x, y, timeMs, options) {
const { angle, duration, frequency, easing, bounce, min, max } = options;
const direction = (angle * Math.PI) / 180;
const travel = x * Math.cos(direction) + y * Math.sin(direction);
const rawPhase = ((timeMs / 1000 / duration + travel * frequency) % 1) + 1;
const phase = rawPhase % 1;
const t = bounce ? 1 - Math.abs(phase * 2 - 1) : phase;
const eased = easings[easing](t);
return min + (max - min) * eased;
}