Dot loader
- Source
- After bencho.dev ↗
- Author
- Interaction after bencho.dev
- Licence
- Original code — interaction after bencho.dev
Loading…
A wave runs through a grid of dots from wherever your pointer is; click to change it.
Code
"use client";
import { useEffect, useRef, useState } from "react";
import { prefersReducedMotion, Surface } from "../ui";
const N = 9;
const MODES = ["ripple", "sweep", "spiral"] as const;
export default function DotLoader() {
const dots = useRef<(HTMLSpanElement | null)[]>([]);
const grid = useRef<HTMLDivElement>(null);
const origin = useRef({ x: (N - 1) / 2, y: (N - 1) / 2, tx: (N - 1) / 2, ty: (N - 1) / 2 });
const [mode, setMode] = useState(0);
const modeRef = useRef(0);
useEffect(() => {
modeRef.current = mode;
}, [mode]);
useEffect(() => {
const still = prefersReducedMotion();
let raf = 0;
const t0 = performance.now();
const loop = (now: number) => {
const t = (now - t0) / 1000;
const o = origin.current;
o.x += (o.tx - o.x) * 0.12;
o.y += (o.ty - o.y) * 0.12;
const m = MODES[modeRef.current];
for (let i = 0; i < N * N; i++) {
const el = dots.current[i];
if (!el) continue;
const x = i % N;
const y = Math.floor(i / N);
const dx = x - o.x;
const dy = y - o.y;
let phase: number;
if (m === "ripple") phase = Math.hypot(dx, dy) * 0.55 - t * 3.2;
else if (m === "sweep") phase = (dx + dy) * 0.4 - t * 3.6;
else phase = Math.atan2(dy, dx) * 2 + Math.hypot(dx, dy) * 0.4 - t * 3.4;
const v = (Math.sin(phase) + 1) / 2;
const k = v * v;
el.style.transform = `scale(${0.45 + k * 0.75})`;
el.style.opacity = String(0.28 + k * 0.72);
}
if (!still) raf = requestAnimationFrame(loop);
};
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, []);
return (
<Surface>
<div className="flex flex-col items-center gap-[1.4em]">
<div
ref={grid}
className="grid cursor-pointer touch-none gap-[0.55em] p-[0.6em]"
style={{ gridTemplateColumns: `repeat(${N}, 1fr)` }}
onPointerMove={(e) => {
const r = grid.current!.getBoundingClientRect();
origin.current.tx = ((e.clientX - r.left) / r.width) * N - 0.5;
origin.current.ty = ((e.clientY - r.top) / r.height) * N - 0.5;
}}
onPointerLeave={() => {
origin.current.tx = (N - 1) / 2;
origin.current.ty = (N - 1) / 2;
}}
onClick={() => setMode((m) => (m + 1) % MODES.length)}
>
{Array.from({ length: N * N }, (_, i) => (
<span key={i} ref={(n) => void (dots.current[i] = n)} className="block h-[0.85em] w-[0.85em] rounded-full bg-[#151515]" />
))}
</div>
<p className="font-mono text-[0.7em] text-[#8a8984]">
{MODES[mode]} · move to steer · click to change
</p>
</div>
</Surface>
);
}
Prompt
Build "Dot loader", a loader animation. What it does: A wave runs through a grid of dots from wherever your pointer is; click to change it. Write it as a single self-contained React component in TypeScript that fills its parent (w-full h-full), styled with Tailwind CSS. Technique: an HTML canvas, a requestAnimationFrame loop, pointer tracking. It sits on a light background, in a 16:10 frame. Respect prefers-reduced-motion with a still state, pause work when the element is off screen, and keep it accessible. Original: https://bencho.dev (Interaction after bencho.dev, Original code — interaction after bencho.dev).