














Learn to code organic, blob-like animations from scratch
Click to add metaballs
Metaballs are organic-looking shapes that merge and blend together smoothly when they come close to each other. They're created using mathematical field equations and are commonly seen in lava lamps, liquid simulations, and game effects.
Each metaball creates an invisible "field" of influence around it. When multiple fields overlap, they add together. Where the combined field strength exceeds a threshold, we draw a pixel. This creates the smooth blending effect.
1 Create the HTML structure
Download the fullscreen HTML+JS file here: metaballs.htm or follow the steps below.
First, we need a canvas element to draw on:
<canvas id="canvas"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Make canvas fill the viewport
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
</script>
2 Define metaball properties and behavior
Each metaball needs position, velocity, and size:
class Metaball {
constructor(x, y, vx, vy, radius) {
this.x = x; // X position
this.y = y; // Y position
this.vx = vx; // X velocity
this.vy = vy; // Y velocity
this.radius = radius; // Size
}
update() {
// Move the ball
this.x += this.vx;
this.y += this.vy;
// Bounce off walls
if (this.x - this.radius < 0 || this.x + this.radius > canvas.width) {
this.vx *= -1;
}
if (this.y - this.radius < 0 || this.y + this.radius > canvas.height) {
this.vy *= -1;
}
}
}
3 Calculate the field strength at any point
This is the mathematical heart of metaballs. For each pixel, we calculate how much each ball influences it:
field_strength = (radius²) / (distance²)
The closer a point is to a ball's center, the stronger the influence. We sum up all influences:
function getMetaballValue(x, y) {
let sum = 0;
// Add influence from each ball
for (const ball of balls) {
// Calculate distance squared (faster than sqrt)
const dx = x - ball.x;
const dy = y - ball.y;
const distSq = dx * dx + dy * dy;
// Add this ball's influence
sum += (ball.radius * ball.radius) / distSq;
}
return sum;
}
We use distance squared instead of actual distance because it's faster to calculate (no square root needed) and still gives us the smooth falloff we want. The inverse square relationship creates a natural-looking field.
4 Convert field values to pixels
We check every pixel and color it based on the field strength:
function render() {
const imageData = ctx.createImageData(canvas.width, canvas.height);
const data = imageData.data;
const step = 3; // Check every 3 pixels for performance
for (let y = 0; y < canvas.height; y += step) {
for (let x = 0; x < canvas.width; x += step) {
const value = getMetaballValue(x, y);
// If field is strong enough, draw a pixel
if (value > 1) {
// Color based on intensity
const intensity = Math.min(value / 3, 1);
const r = Math.floor(100 + intensity * 155);
const g = Math.floor(50 + intensity * 100);
const b = Math.floor(200 + intensity * 55);
// Fill the step x step block
for (let dy = 0; dy < step; dy++) {
for (let dx = 0; dx < step; dx++) {
const idx = ((y + dy) * canvas.width + (x + dx)) * 4;
data[idx] = r;
data[idx + 1] = g;
data[idx + 2] = b;
data[idx + 3] = 255; // Alpha
}
}
}
}
}
ctx.putImageData(imageData, 0, 0);
}
Performance Tip: We use a step variable to skip pixels. Checking every pixel (step=1) looks smoother but is slower. A step of 2-4 balances quality and performance.
5 Track mouse/touch position
Create an invisible metaball that follows the cursor:
const mouseInfluence = {
x: -1000,
y: -1000,
radius: 80,
active: false
};
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouseInfluence.x = e.clientX - rect.left;
mouseInfluence.y = e.clientY - rect.top;
mouseInfluence.active = true;
});
canvas.addEventListener('mouseleave', () => {
mouseInfluence.active = false;
});
Then include it in the field calculation:
function getMetaballValue(x, y) {
let sum = 0;
for (const ball of balls) {
const dx = x - ball.x;
const dy = y - ball.y;
const distSq = dx * dx + dy * dy;
sum += (ball.radius * ball.radius) / distSq;
}
// Add mouse influence
if (mouseInfluence.active) {
const dx = x - mouseInfluence.x;
const dy = y - mouseInfluence.y;
const distSq = dx * dx + dy * dy;
sum += (mouseInfluence.radius * mouseInfluence.radius) / distSq;
}
return sum;
}
6 Bring it all together
function animate() {
// Clear canvas
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Update all balls
for (const ball of balls) {
ball.update();
}
// Render metaballs
render();
// Loop
requestAnimationFrame(animate);
}
animate();
Add touch event handlers for mobile devices:
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
mouseInfluence.x = touch.clientX - rect.left;
mouseInfluence.y = touch.clientY - rect.top;
mouseInfluence.active = true;
});
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
balls.push(new Metaball(
e.clientX - rect.left,
e.clientY - rect.top,
(Math.random() - 0.5) * 3,
(Math.random() - 0.5) * 3,
30 + Math.random() * 40
));
});
Now that you understand the basics, try these variations:
Spinning squares - visual effect (25 lines)
Oldschool fire effect (20 lines)
Physics engine - interactive sandbox
Physics engine - silly contraption
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。