JavaScript metaballs optimization
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;