Vanish UI

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.

35°
2.5s
0.22
0.35

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.

use-wave-field.ts
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;
}