live - 60fps 1920 x 1080
<!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.