Baktiyar Ablaikhan

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).