library / particles & systems / silk-flow-field
live - 60fps 1920 x 1080
silk-flow-field.html - self-contained
<!DOCTYPE html>
<html>
<head>
<title>Silk Flow Field</title>
<style>
    :root {
        --bg-color: #05060c;
        --stream-1: #22d3ee;
        --stream-2: #a855f7;
    }
    * { 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');
    const ctx = cv.getContext('2d');
    const css = getComputedStyle(document.documentElement);
    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 bg = hex(css.getPropertyValue('--bg-color') || '#05060c');
    const s1 = hex(css.getPropertyValue('--stream-1') || '#22d3ee');
    const s2 = hex(css.getPropertyValue('--stream-2') || '#a855f7');
    let w, h, parts;
    const N = 1300;
    const mouse = { x: -9999, y: -9999, active: false };
    const rnd = (a, b) => a + Math.random() * (b - a);
    function build() {
        w = cv.width = innerWidth;
        h = cv.height = innerHeight;
        parts = [];
        for (let i = 0; i < N; i++) parts.push({ x: Math.random() * w, y: Math.random() * h, life: rnd(0, 240) });
        ctx.fillStyle = `rgb(${bg[0]},${bg[1]},${bg[2]})`;
        ctx.fillRect(0, 0, w, h);
    }
    // Smooth curl-like flow field from layered trig — cheap, no noise tables
    function angle(px, py, t) {
        const s = 0.0015;
        const a = Math.sin(px * s + t) * Math.cos(py * s * 1.3 - t * 0.6)
                + Math.sin((px + py) * s * 0.5 + t * 0.4);
        return a * Math.PI * 1.4;
    }
    addEventListener('mousemove', e => { mouse.x = e.clientX; mouse.y = e.clientY; mouse.active = true; });
    addEventListener('mouseleave', () => { mouse.active = false; });
    addEventListener('resize', build);
    build();
    let t = 0;
    function step() {
        t += 0.002;
        ctx.globalCompositeOperation = 'source-over';
        ctx.fillStyle = `rgba(${bg[0]},${bg[1]},${bg[2]},0.045)`;
        ctx.fillRect(0, 0, w, h);
        ctx.globalCompositeOperation = 'lighter';
        ctx.lineWidth = 1.1;
        for (const p of parts) {
            const a = angle(p.x, p.y, t);
            let vx = Math.cos(a), vy = Math.sin(a);
            if (mouse.active) {
                const dx = p.x - mouse.x, dy = p.y - mouse.y, d = Math.hypot(dx, dy) || 1;
                if (d < 190) { const f = (1 - d / 190); vx += (-dy / d) * f * 2.6; vy += (dx / d) * f * 2.6; }
            }
            const ox = p.x, oy = p.y;
            p.x += vx * 1.5; p.y += vy * 1.5; p.life--;
            const mix = Math.sin(a) * 0.5 + 0.5;
            const r = s1[0] + (s2[0] - s1[0]) * mix | 0;
            const g = s1[1] + (s2[1] - s1[1]) * mix | 0;
            const b = s1[2] + (s2[2] - s1[2]) * mix | 0;
            ctx.strokeStyle = `rgba(${r},${g},${b},0.5)`;
            ctx.beginPath(); ctx.moveTo(ox, oy); ctx.lineTo(p.x, p.y); ctx.stroke();
            if (p.x < 0 || p.x > w || p.y < 0 || p.y > h || p.life < 0) {
                p.x = Math.random() * w; p.y = Math.random() * h; p.life = rnd(60, 260);
            }
        }
    }
    if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
        for (let i = 0; i < 320; i++) step(); // paint a static silk field, then hold
    } else {
        (function loop() { step(); requestAnimationFrame(loop); })();
    }
</script>
</body>
</html>

About this animation

Thousands of particles stream along a flowing vector field, weaving silky ribbons of light that swirl into a vortex around your cursor.

Source Code
Copied to clipboard