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