Baktiyar Ablaikhan

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