Baktiyar Ablaikhan

Metal Lissajous

Source
arlan.me/vault ↗
Author
Arlan
Licence
MIT

Loading…

Twenty-four glowing wire Lissajous curves breathe together between loops and straight diagonals; moving the pointer sweeps the reflection across them.

Code

"use client";

import { CanvasStudy, type Frame, type Painter } from "./stage";

export const BG = "#0d0e12";
const ROWS = 4;
const COLS = 6;
const N = 240; // samples per curve
const BUCKETS = 10;

export function create(): Painter {
  let light = 0;

  return ({ ctx, w, h, t, dt, p }: Frame) => {
    ctx.fillStyle = BG;
    ctx.fillRect(0, 0, w, h);
    const cw = (w * 0.92) / COLS;
    const ch = (h * 0.9) / ROWS;
    const rad = Math.min(cw, ch) * 0.4;
    const x0 = (w - cw * COLS) / 2 + cw / 2;
    const y0 = (h - ch * ROWS) / 2 + ch / 2;
    const lw = Math.max(1.4, rad * 0.075);

    // light direction: slow sweep, or the pointer's position across the card
    const target = p.inside ? (p.x / w) * Math.PI * 2.2 - Math.PI * 0.6 : t * 0.45;
    let d = target - light;
    d = Math.atan2(Math.sin(d), Math.cos(d));
    light += d * Math.min(1, dt * (p.inside ? 8 : 2));
    const Lx = Math.cos(light);
    const Ly = Math.sin(light);

    // one shared breath: π/2 (circles on the diagonal) toward ~0 (straight diagonals)
    const delta = Math.PI / 2 - (0.5 - 0.5 * Math.cos((t / 7) * Math.PI * 2)) * (Math.PI / 2 - 0.06);

    const base: Path2D[] = Array.from({ length: BUCKETS }, () => new Path2D());
    const glint: Path2D[] = Array.from({ length: BUCKETS }, () => new Path2D());

    for (let r = 0; r < ROWS; r++) {
      for (let c = 0; c < COLS; c++) {
        const a = c + 1;
        const b = r + 1;
        const cx = x0 + c * cw;
        const cy = y0 + r * ch;
        let px = cx + Math.sin(delta) * rad;
        let py = cy;
        for (let i = 1; i <= N; i++) {
          const s = (i / N) * Math.PI * 2;
          const qx = cx + Math.sin(a * s + delta) * rad;
          const qy = cy + Math.sin(b * s) * rad;
          const tx = qx - px;
          const ty = qy - py;
          const len = Math.hypot(tx, ty) || 1;
          let nx = -ty / len;
          let ny = tx / len;
          let dot = nx * Lx + ny * Ly;
          if (dot < 0) {
            nx = -nx;
            ny = -ny;
            dot = -dot;
          }
          const k = Math.min(BUCKETS - 1, Math.floor(dot ** 1.5 * BUCKETS));
          base[k].moveTo(px, py);
          base[k].lineTo(qx, qy);
          const o = lw * 0.28;
          glint[k].moveTo(px + nx * o, py + ny * o);
          glint[k].lineTo(qx + nx * o, qy + ny * o);
          px = qx;
          py = qy;
        }
      }
    }

    ctx.lineCap = "round";
    for (let k = 0; k < BUCKETS; k++) {
      const v = k / (BUCKETS - 1);
      const lo = [34, 36, 44];
      const hi = [196, 202, 216];
      ctx.strokeStyle = `rgb(${lo.map((l, i) => Math.round(l + (hi[i] - l) * v * 0.85)).join(",")})`;
      ctx.lineWidth = lw;
      ctx.stroke(base[k]);
    }
    ctx.globalCompositeOperation = "lighter";
    for (let k = 3; k < BUCKETS; k++) {
      const v = (k - 3) / (BUCKETS - 4);
      ctx.strokeStyle = `rgba(150,175,255,${0.05 * v})`;
      ctx.lineWidth = lw * 3.5;
      ctx.stroke(glint[k]);
      ctx.strokeStyle = `rgba(255,255,255,${0.15 + 0.85 * v * v})`;
      ctx.lineWidth = lw * 0.38;
      ctx.stroke(glint[k]);
    }
    ctx.globalCompositeOperation = "source-over";
  };
}

export default function MetalWires() {
  return (
    <CanvasStudy
      create={create}
      background={BG}
      still={1}
      label="Twenty-four glowing metal Lissajous wires breathe together between loops and straight diagonals; the pointer sweeps the reflection across them."
    />
  );
}
Prompt

Build "Metal Lissajous", a canvas animation. What it does: Twenty-four glowing wire Lissajous curves breathe together between loops and straight diagonals; moving the pointer sweeps the reflection across them. 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).