live - 60fps 1920 x 1080
<!DOCTYPE html>
<html>
<head>
<title>Mercury Metaballs</title>
<style>
:root {
--bg-color: #0a0a14;
--metal-1: #6366f1;
--metal-2: #ec4899;
--metal-3: #22d3ee;
}
* { margin: 0; padding: 0; }
body { height: 100vh; background: var(--bg-color); overflow: hidden; }
.field {
position: absolute;
inset: 0;
filter: url(#goo);
}
.blob {
position: absolute;
border-radius: 50%;
will-change: transform;
}
svg { position: absolute; width: 0; height: 0; }
</style>
</head>
<body>
<svg>
<filter id="goo">
<feGaussianBlur in="SourceGraphic" stdDeviation="15" result="blur" />
<feColorMatrix in="blur" mode="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -9" result="goo" />
<feBlend in="SourceGraphic" in2="goo" />
</filter>
</svg>
<div class="field" id="field"></div>
<script>
const css = getComputedStyle(document.documentElement);
const colors = [
(css.getPropertyValue('--metal-1') || '#6366f1').trim(),
(css.getPropertyValue('--metal-2') || '#ec4899').trim(),
(css.getPropertyValue('--metal-3') || '#22d3ee').trim()
];
const field = document.getElementById('field');
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
const blobs = [];
const COUNT = 9;
for (let i = 0; i < COUNT; i++) {
const el = document.createElement('div');
el.className = 'blob';
const size = 120 + Math.random() * 220;
const col = colors[i % colors.length];
el.style.width = el.style.height = size + 'px';
el.style.background = `radial-gradient(circle at 35% 35%, ${col}, ${col} 55%, transparent 72%)`;
field.appendChild(el);
blobs.push({
el, size,
x: Math.random() * innerWidth, y: Math.random() * innerHeight,
ax: Math.random() * Math.PI * 2, ay: Math.random() * Math.PI * 2,
sx: 0.4 + Math.random() * 0.5, sy: 0.4 + Math.random() * 0.5,
rx: innerWidth * (0.2 + Math.random() * 0.3),
ry: innerHeight * (0.2 + Math.random() * 0.3),
follow: i === 0
});
}
const mouse = { x: innerWidth / 2, y: innerHeight / 2, active: false };
addEventListener('mousemove', e => { mouse.x = e.clientX; mouse.y = e.clientY; mouse.active = true; });
addEventListener('mouseleave', () => { mouse.active = false; });
let t = 0;
function place(b) { b.el.style.transform = `translate(${b.x - b.size / 2}px, ${b.y - b.size / 2}px)`; }
function step() {
t += 0.006;
const cx = innerWidth / 2, cy = innerHeight / 2;
for (const b of blobs) {
if (b.follow && mouse.active) {
b.x += (mouse.x - b.x) * 0.08;
b.y += (mouse.y - b.y) * 0.08;
} else {
b.x = cx + Math.cos(b.ax + t * b.sx) * b.rx;
b.y = cy + Math.sin(b.ay + t * b.sy) * b.ry;
}
place(b);
}
}
if (reduce) { t = 2; blobs.forEach(b => { b.x = innerWidth / 2 + Math.cos(b.ax) * b.rx; b.y = innerHeight / 2 + Math.sin(b.ay) * b.ry; place(b); }); }
else (function loop() { step(); requestAnimationFrame(loop); })();
</script>
</body>
</html> About this animation
Liquid-metal blobs that merge and separate with gooey surface tension as they drift, with one glob chasing your cursor around the screen.
Related