JavaScript metaballs optimization I created this basic metaball visualization in JavaScript. While it does look nice, it is very slow if you increase number of balls and/or make drawing area larger. I think that the performance is so bad because it iterates through every pixel every time the frame is updated but I cannot think anyway how I could update only those pixels where something has actually happened.

Here's the code:



// canvas

var c = document.getElementById("canvas");
width = c.width = /*window.innerWidth;*/1280;
height = c.height = /*window.innerHeight;*/720;
var ctx = c.getContext("2d");

// imagedata

var imageData = ctx.createImageData(width,height);
var data = imageData.data;

// array for balls

var balls = [];
var num = 20;

// random number generation

function random(min, max){
 var num = Math.floor(Math.random()*(max-min+1)+min);
 return num; 
}

// metaball

function ball() {
 this.posX = random(0, width);
 this.posY = random(0, height);
 this.dx = random(-4,4);
 this.dy = random(-4,4);
 this.radius = random(10, 100);
}

// ball movement

ball.prototype.move = function() {

 if (this.posX &gt; width || this.posX &lt; 0) { // change direction if edge is encountered, x-axis
  this.dx = -(this.dx);
 }

 if (this.posY &gt; height || this.posY &lt; 0 ) { // change direction if edge is encountered, y-axis
  this.dy = -(this.dy);
 }

 this.posX += this.dx; // move balls, x-axis
 this.posY += this.dy; // move balls, y-axis 

}

// create balls and put them into an array

for (i=0; i&lt;num;i++) {
 var metaball = new ball();
 balls.push(metaball);
}

// draw and move metaballs

function Loop(){

 for (var y=0; y&lt;height; y++) {  // loop through draw area pixels, height
  for (var x=0; x&lt;width; x++) { // loop through draw area pixels, width
   var index = (x+y*width)*4; // pixel index
   var color = 0;    // pixel color

   for (i=0; i&lt;balls.length; i++) {
    color += balls[i].radius * 10000 / (Math.pow((x - balls[i].posX),2) + Math.pow((y - balls[i].posY),2)); // metaball calculation formula
   }
   
    // assign color to pixel based on previous formula
    data[index+0] = color*1.05; // r 
    data[index+1] = color;  // g 
    data[index+2] = color*1.4; // b 
    data[index+3] = 255;  // a
   }
 }

 // move balls

 for (i=0; i&lt;balls.length; i++) {
  balls[i].move();
 }

 // put imagedata on canvas

 ctx.putImageData(imageData, 0,0);

 // animate

 requestAnimationFrame(Loop);
}

// call loop function

Loop();
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
 &lt;title&gt;metaballs&lt;/title&gt;

&lt;style type="text/css"&gt;

 body {
  padding: 0px;
  margin: 0px;
 }



&lt;/style&gt;

&lt;/head&gt;
&lt;body&gt;

&lt;div&gt;
 &lt;canvas id="canvas"&gt;
 &lt;/canvas&gt;
&lt;/div&gt;

&lt;script type="text/javascript" src="metaballs.js"&gt;
&lt;/script&gt;

&lt;/body&gt;
&lt;/html&gt;




Is there anything I can do to optimize this?

Since I'm a total beginner in JavaScript and programming in general, I would be very happy to hear if there are better practices than what I've used here :)
JavaScript metaballs optimization


I read that as "JavaScript meatballs optimization" :D

First optimization I would do is... code organization. Slow-looking code will be slow code, since it takes you an extraordinary amount of time working with it. So keep your code tidy. Use proper indentation, name variables properly, organize code in a coherent fashion.

Nested loops become a problem, fast. Avoid nested loops as much as possible. In the case of looping through canvas pixels, instead of using a nested loop to traverse horizontally and vertically, you can simply use one loop and skip by fours. That will give you the same pixel-traversing effect.

But isn't it the same amount of iterations? Yes, but here's the thing. Loops are a dead end in terms of optimization. If you get your x and y that way, that's it. However, if you do the 1d loop and do math to get your x and y, you still have a chance at optimization since math can be optimized (bitwise operations) and values can be cached easily.

Another perf killer is property access. It's negligible alone, but if you're operating at 3M pixels per frame, it becomes very visible. Cache values as much as possible, and avoid property accesses.

One optimization you could do is heuristics. I you know a pixel is far enough from any ball, you can just color it black without calculation. Same goes for pixels within a ball. If it's within a ball, it will be white regardless of overlapping balls. Also, if a pixel is already white, you can just skip calculation of the other balls since there's no way it could darken.

Another trick you could do is just use a smaller canvas and upscale it with CSS scale transform. To the script, the number of pixels on the canvas is lesser which means lesser iterations. But from the viewing perspective, the animation is about the same as the full-scale version.

Here's my take on it. Slightly faster due to skipping and upscaling.



const c = document.getElementById("canvas");
const width = c.width = 640;
const height = c.height = 360;
const ctx = c.getContext("2d");
const imageData = ctx.createImageData(width, height);
const data = imageData.data;
const dataLength = data.length;
const num = 20;
const balls = Array(num).fill().map(_ =&gt; new Ball());

function random(min, max) {
  var num = Math.floor(Math.random() * (max - min + 1) + min);
  return num;
}

function Ball() {
  this.posX = random(0, width);
  this.posY = random(0, height);
  this.dx = random(-4, 4);
  this.dy = random(-4, 4);
  this.radius = random(2, 25);
}

Ball.prototype.move = function() {
  if (this.posX &gt; width || this.posX &lt; 0) this.dx = -(this.dx);
  if (this.posY &gt; height || this.posY &lt; 0) this.dy = -(this.dy);
  this.posX += this.dx;
  this.posY += this.dy; 
}

function updateBalls() {
  for (var i = balls.length; i--;) balls[i].move();
}

function renderBalls() {

  for (var sp = 0; sp &lt; dataLength; sp += 4) {
    const index = sp / 4;
    const y = index / width | 0;
    const x = index % width | 0;
    var color = 0;

    for (var i = balls.length; i--;) {
      const ball = balls[i];
      const radius = ball.radius;
      const posX = ball.posX;
      const posY = ball.posY;

      color += radius * 2500 / (Math.pow((x - posX), 2) + Math.pow((y - posY), 2));
      
      if(color &gt;= 255) break;
    }

    data[sp + 0] = color * 1.05;
    data[sp + 1] = color;
    data[sp + 2] = color * 1.4;
    data[sp + 3] = 255;
  }

  ctx.putImageData(imageData, 0, 0);
}


;(function loop() {
  requestAnimationFrame(loop);
  updateBalls();
  renderBalls();
})();
body {
  padding: 0px;
  margin: 0px;
}

canvas{
  transform: scale(4);
}
&lt;canvas id="canvas"&gt;&lt;/canvas&gt;