Amber crescents
- Source
- arlan.me/vault ↗
- Author
- Arlan
- Licence
- MIT
Loading…
Eighty thin amber circles slide open on their own rhythms to reveal crescents; the pointer tilts the crescents away like a lamp and eases nearby circles open.
Code
"use client";
import { CanvasStudy, hash, smooth, clamp, type Frame, type Painter } from "./stage";
export const BG = "#121110";
const AMBER = "#f6b21b";
const COLS = 10;
const ROWS = 8;
export function create(): Painter {
const open = new Float32Array(COLS * ROWS); // smoothed pointer opening
const dirX = new Float32Array(COLS * ROWS).fill(-Math.SQRT1_2);
const dirY = new Float32Array(COLS * ROWS).fill(-Math.SQRT1_2);
return ({ ctx, w, h, t, dt, p }: Frame) => {
ctx.fillStyle = BG;
ctx.fillRect(0, 0, w, h);
const cell = Math.min((w * 0.9) / COLS, (h * 0.88) / ROWS);
const r = cell * 0.36;
const x0 = (w - cell * COLS) / 2 + cell / 2;
const y0 = (h - cell * ROWS) / 2 + cell / 2;
const ease = Math.min(1, dt * 5);
ctx.lineWidth = Math.max(1, cell * 0.035);
for (let j = 0; j < ROWS; j++) {
for (let i = 0; i < COLS; i++) {
const id = j * COLS + i;
const cx = x0 + i * cell;
const cy = y0 + j * cell;
// own rhythm: a period of 3–8 s and a random phase, square-ish so circles rest open or shut
const period = 3 + hash(id, 1) * 5;
const wave = Math.sin((t / period + hash(id, 2)) * Math.PI * 2) + (hash(id, 3) - 0.5) * 0.4;
let o = smooth(clamp((wave + 0.25) / 0.5));
// pointer: lamp-like tilt and a partial opening nearby
let tx = -Math.SQRT1_2;
let ty = -Math.SQRT1_2;
let near = 0;
if (p.inside) {
const dx = p.x - cx;
const dy = p.y - cy;
const d = Math.hypot(dx, dy) || 1;
tx = dx / d;
ty = dy / d;
near = Math.exp(-(d * d) / (cell * cell * 4));
}
dirX[id] += (tx - dirX[id]) * ease;
dirY[id] += (ty - dirY[id]) * ease;
open[id] += (near - open[id]) * ease;
o = Math.max(o, open[id] * 0.6);
const dl = Math.hypot(dirX[id], dirY[id]) || 1;
const off = o * r * 0.42;
const ox = (dirX[id] / dl) * off;
const oy = (dirY[id] / dl) * off;
if (o > 0.002) {
ctx.fillStyle = AMBER;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.fill();
}
// the outline circle slides toward the lamp and covers the disc behind it
ctx.fillStyle = BG;
ctx.strokeStyle = AMBER;
ctx.beginPath();
ctx.arc(cx + ox, cy + oy, r, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
}
};
}
export default function AmberCrescents() {
return (
<CanvasStudy
create={create}
background={BG}
still={2}
label="Eighty thin amber circles slide open on their own rhythms to show crescents; the pointer tilts the crescents away like a lamp."
/>
);
}
Prompt
Build "Amber crescents", a cursor animation. What it does: Eighty thin amber circles slide open on their own rhythms to reveal crescents; the pointer tilts the crescents away like a lamp and eases nearby circles open. 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 dark 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://www.arlan.me/vault (Arlan, MIT).