The full-screen menu needed texture that responds to you without competing with the links. A sparse dot grid with a trailing ripple does that cheaply on Canvas 2D.
A 2D canvas draws one circle per grid cell. Each frame, every dot sums three influences: a linear falloff from the pointer, expanding ring ripples spawned on pointer down, and short-lived trail rings spawned as the pointer moves. The sum scales dot radius and opacity, so the grid swells around the pointer and settles back with no per-dot state.
const getTrailInfluence = (x, y, now) => {
let influence = 0;
trailRipplesRef.current.forEach((ripple) => {
const age = now - ripple.time;
const maxAge = 690;
if (age >= maxAge) return;
const distance = Math.hypot(x - ripple.x, y - ripple.y);
const radius = 4 + (age / maxAge) * 55;
const width = 32;
if (Math.abs(distance - radius) < width) {
influence += (1 - age / maxAge) * 0.86 * (1 - Math.abs(distance - radius) / width);
}
});
return Math.min(influence, 1.15);
};
const influence = (getPointerInfluence(dot.originalX, dot.originalY)
+ getRippleInfluence(dot.originalX, dot.originalY, now)
+ getTrailInfluence(dot.originalX, dot.originalY, now)) * tunables.interactionStrength;
const size = 1.7 + influence * 5.2 + Math.sin(timeRef.current + dot.phase) * 0.35;