惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
博客园 - 司徒正美
Last Week in AI
Last Week in AI
博客园 - 聂微东
Jina AI
Jina AI
月光博客
月光博客
爱范儿
爱范儿
美团技术团队
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Hugging Face - Blog
Hugging Face - Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 叶小钗
T
Tailwind CSS Blog
博客园 - 【当耐特】
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Apple Machine Learning Research
Apple Machine Learning Research
有赞技术团队
有赞技术团队
罗磊的独立博客
小众软件
小众软件
雷峰网
雷峰网
IT之家
IT之家
大猫的无限游戏
大猫的无限游戏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
Visual Studio Blog

Echo JS

GitHub - evoluteur/mandala-maker: Draw a mandala with mirrored symmetry: pick the number of folds, paint with a brush, and export your mandala as PNG or SVG. GitHub - aboviq/supapower: A sync engine for Supabase and a local PGlite instance - inspired by PowerSync. billboard.js 4.1.0: Live resizing, configurable subchart, React subpath & CSP-safe worker From 1,256ms to 96ms: Fixing INP in a Massive React Dropdown GitHub - evoluteur/cymatics: Play a frequency and watch the sand settle into its Chladni figure, computed from the wave equation. Memdeklaro - The Basics of Decentralized Identity (DID) and Self-Sovereign Identity (SSI) How Railmid Works GitHub - evoluteur/platonic-solids: Turn the five Platonic solids in 3D, show their duals, read their measurements, and print the nets to fold your own. Sharing Application State in a URL GitHub - evoluteur/sacred-geometry: Sacred Geometry Generator: draw, tune, and export Vesica Piscis, Seed of Life, Flower of Life, Metatron's Cube, and the Golden Spiral as SVG Best of Self-Sovereign Identity: Digitalcourage, World Passport and Memdeklaro Reads Are Subscriptions - Migrating from Zustand to Coaction GitHub - evoluteur/binaural-beats: Simple web page to play binaural beats for sleep, meditation, relaxation, and focus: Delta, Theta, Alpha, Beta, and Gamma brainwave frequencies, with an optional pink or brown noise bed. toast-queue — Accessible, customizable toast notifications Building a High-Performance Data Grid in React, Vue, and Svelte I built a flight recorder for AI sessions React Authentication With JWT, Zustand, and Axios | JavaScript Tools Blog My idempotency library had one job. A dropped connection made it run the payment twice. "half-open" twice is not the same state: the bug that shaped breakwater 1.0 GitHub - evoluteur/evolutility-server-node: Framework for building REST APIs for CRUD with models rather than code (using Node.js, Express, and PostgreSQL). React Router v8 in Action: Lazy Loading and Nested Routes One $ for every environment | Xec My test suite had 100% coverage. Mutation testing still found real bugs. The type-safe data layer for Kysely | Kysera What JavaScript Obfuscation in the AI Era | JavaScript Tools Blog Using Mongoose Studio with Apache Cassandra via Data API GitHub - trekhleb/yesbrainer: 🧠 A council of AI models for the decisions that aren't no-brainers — they answer in parallel, debate to consensus, or get judged to a verdict. Browser-only, open source, bring your own keys (BYOK), no backend. Node.js has plenty of circuit breakers. So why did I build another one? My Redis library said the write succeeded. Redis was down.
Interactive Metaballs Tutorial
2026-07-24 · via Echo JS

Learn to code organic, blob-like animations from scratch

Click to add metaballs

What Are 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.

🧠 Core Concept

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.

Step 1: Setting Up the Canvas

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>

Step 2: Creating the Metaball Class

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;
        }
    }
}

Step 3: The Metaball Formula

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;
}

💡 Why Distance Squared?

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.

Step 4: Rendering the Metaballs

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.

Step 5: Adding Interactivity

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;
}

Step 6: Animation Loop

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();

Touch Support

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
    ));
});

Experiment and Extend

Now that you understand the basics, try these variations:

  • Different colors: Change the RGB values based on ball properties
  • Gravity: Add a gravity force to ball velocities
  • Size variation: Make balls grow and shrink over time
  • Multiple thresholds: Draw different colors at different field strengths
  • Glow effects: Add a blur filter for a softer look

More JS tutorials

Spinning squares - visual effect (25 lines)

Oldschool fire effect (20 lines)

Fireworks (60 lines)

Animated fractal (32 lines)

Physics engine for beginners

Physics engine - interactive sandbox

Physics engine - silly contraption

Starfield (21 lines)

Yin Yang with a twist (4 circles and 20 lines)

Tile map editor (70 lines)

Sine scroller (30 lines)

Interactive animated sprites

Image transition effect (16 lines)