Wake slider
- Source
- After bencho.dev ↗
- Author
- Interaction after bencho.dev
- Licence
- Original code — interaction after bencho.dev
Loading…
Drag the slider quickly and the bars ripple in its wake.
Code
"use client";
import { useEffect, useRef, useState } from "react";
import { prefersReducedMotion, Surface } from "../ui";
const BARS = 36;
export default function WakeSlider() {
const track = useRef<HTMLDivElement>(null);
const bars = useRef<(HTMLSpanElement | null)[]>([]);
const thumb = useRef<HTMLSpanElement>(null);
const sim = useRef({ t: 0.4, target: 0.4, drag: false, h: new Float32Array(BARS), v: new Float32Array(BARS) });
const [value, setValue] = useState(40);
useEffect(() => {
const still = prefersReducedMotion();
let raf = 0;
let shown = -1;
const loop = () => {
const s = sim.current;
const prev = s.t;
s.t += (s.target - s.t) * 0.25;
const speed = (s.t - prev) * BARS;
const at = s.t * (BARS - 1);
// the moving thumb pushes the water around it
for (let i = 0; i < BARS; i++) {
const d = i - at;
if (Math.abs(d) < 2.5) s.v[i] += Math.abs(speed) * 0.9 * (1 - Math.abs(d) / 2.5);
}
// a simple 1D wave: neighbours pull on each other, everything settles
const next = new Float32Array(BARS);
for (let i = 0; i < BARS; i++) {
const l = s.h[Math.max(0, i - 1)];
const r = s.h[Math.min(BARS - 1, i + 1)];
s.v[i] += (l + r - 2 * s.h[i]) * 0.25 - s.h[i] * 0.04;
s.v[i] *= 0.94;
next[i] = s.h[i] + s.v[i];
}
s.h = next;
for (let i = 0; i < BARS; i++) {
const el = bars.current[i];
if (!el) continue;
const on = i <= at;
el.style.transform = `scaleY(${1 + Math.max(-0.5, Math.min(2.4, s.h[i] * 2.2))})`;
el.style.backgroundColor = on ? "#151515" : "#cfcec8";
}
if (thumb.current) thumb.current.style.left = `${s.t * 100}%`;
const v = Math.round(s.t * 100);
if (v !== shown) {
shown = v;
setValue(v);
}
if (!still) raf = requestAnimationFrame(loop);
};
loop();
return () => cancelAnimationFrame(raf);
}, []);
const set = (x: number) => {
const r = track.current!.getBoundingClientRect();
sim.current.target = Math.min(1, Math.max(0, (x - r.left) / r.width));
};
return (
<Surface>
<div className="flex w-[22em] flex-col gap-[1.2em]">
<div className="flex items-baseline justify-between">
<span className="text-[0.8em] text-[#8a8984]">Volume</span>
<span className="font-mono text-[1.4em] tabular-nums">{value}</span>
</div>
<div
ref={track}
className="relative flex h-[4.4em] cursor-pointer touch-none items-center justify-between"
onPointerDown={(e) => {
e.currentTarget.setPointerCapture(e.pointerId);
sim.current.drag = true;
set(e.clientX);
}}
onPointerMove={(e) => sim.current.drag && set(e.clientX)}
onPointerUp={() => (sim.current.drag = false)}
onPointerCancel={() => (sim.current.drag = false)}
>
{Array.from({ length: BARS }, (_, i) => (
<span key={i} ref={(n) => void (bars.current[i] = n)} className="block h-[1.2em] w-[0.22em] rounded-full bg-[#cfcec8] will-change-transform" />
))}
<span ref={thumb} className="pointer-events-none absolute top-1/2 h-[2.4em] w-[0.3em] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#2B46B8] shadow-[0_0_0_0.25em_rgba(43,70,184,0.15)]" />
</div>
<p className="text-center font-mono text-[0.7em] text-[#8a8984]">drag fast to make a wake</p>
</div>
</Surface>
);
}
Prompt
Build "Wake slider", a input animation. What it does: Drag the slider quickly and the bars ripple in its wake. 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).