HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptMinor

JavaScript metaballs optimization

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
javascriptoptimizationmetaballs

Problem

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 > width || this.posX height || this.posY


metaballs

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




`



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 :)

Solution

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(_ => 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 > width || this.posX height || this.posY = 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);
}

``

Context

StackExchange Code Review Q#160012, answer score: 3

Revisions (0)

No revisions yet.