library / backgrounds / spirograph
live - 60fps 1920 x 1080
spirograph.html - self-contained
<!DOCTYPE html>
<html>
<head>
<title>Spirograph</title>
<style>
    :root { --bg-color: #06040f; --trace-1: #f472b6; --trace-2: #22d3ee; }
    * { margin: 0; padding: 0; }
    body { height: 100vh; background: var(--bg-color); overflow: hidden; }
    canvas { display: block; }
</style>
</head>
<body>
    <canvas id="c"></canvas>
<script>
    const cv = document.getElementById('c'), ctx = cv.getContext('2d');
    const css = getComputedStyle(document.documentElement);
    const bg = (css.getPropertyValue('--bg-color') || '#06040f').trim();
    function hex(h){h=(h||'').trim().replace('#','');if(h.length===3)h=h.split('').map(c=>c+c).join('');const n=parseInt(h,16);return[(n>>16)&255,(n>>8)&255,n&255];}
    const t1 = hex(css.getPropertyValue('--trace-1') || '#f472b6');
    const t2 = hex(css.getPropertyValue('--trace-2') || '#22d3ee');
    let w, h, cx, cy, S;
    function build() { w = cv.width = innerWidth; h = cv.height = innerHeight; cx = w / 2; cy = h / 2; S = Math.min(w, h) * 0.42; }
    addEventListener('resize', build); build();
    function step(t) {
        ctx.fillStyle = bg; ctx.fillRect(0, 0, w, h);
        const R = 1, r = 0.28 + 0.16 * Math.sin(t * 0.3), d = 0.7 + 0.25 * Math.sin(t * 0.21);
        const k = (R - r) / r, rot = t * 0.15;
        const steps = 2600, span = Math.PI * 2 * 14;
        ctx.lineWidth = 1.2; ctx.globalCompositeOperation = 'lighter';
        ctx.beginPath();
        for (let i = 0; i <= steps; i++) {
            const th = i / steps * span;
            let x = (R - r) * Math.cos(th) + d * Math.cos(k * th);
            let y = (R - r) * Math.sin(th) - d * Math.sin(k * th);
            const xr = x * Math.cos(rot) - y * Math.sin(rot), yr = x * Math.sin(rot) + y * Math.cos(rot);
            const px = cx + xr * S, py = cy + yr * S;
            if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
        }
        const mix = 0.5 + 0.5 * Math.sin(t * 0.4);
        ctx.strokeStyle = `rgba(${t1[0]+(t2[0]-t1[0])*mix|0},${t1[1]+(t2[1]-t1[1])*mix|0},${t1[2]+(t2[2]-t1[2])*mix|0},0.85)`;
        ctx.stroke();
        ctx.globalCompositeOperation = 'source-over';
    }
    if (matchMedia('(prefers-reduced-motion: reduce)').matches) step(1.0);
    else { let t = 0; (function loop(){ t += 0.01; step(t); requestAnimationFrame(loop); })(); }
</script>
</body>
</html>

About this animation

A single luminous curve loops endlessly through a morphing spirograph, tracing hypnotic geometric flowers that never quite repeat.

Source Code
Copied to clipboard