I'm currently making an HTML5 game, and I'm trying to draw various things onto the canvas. My game is basically just where you move around in an infinite area, but I can't figure out how to optimize my code to draw bushes onto the screen. It works properly, but It lags a lot and I know there's ways to optimize it. Here's my current code:
for(var x=offset[0];x<(offset[0]+canvas.width)+300;x++) {
for(var y=offset[1];y<(offset[1]+canvas.height)+300;y++) {
if(x % 85 == 0 && y % 85 == 0 && noise.simplex2(x, y) == 0) {
ctx.drawImage(treeimage, ((x-offset[0])*-1)+canvas.width, ((y-offset[1])*-1)+canvas.height);
}
}
}
treeimage is defined as so:
var treeimage = new Image(); treeimage.src = 'images/mapobjects/tree2.png';
offset[] is an array with the values being the offset of the objects relative to the player (So when the player moves left, it goes up) horizontally and vertically respectively. I use simplex noise to generate the bushes because I like them to be in small clumps. The problem that makes the FPS so low is that at the resolution of my screen, I'm running 2 modulo functions 2137104 per frame, and that gets even worse at higher resolutions. I tried to make it faster by looping through every tile of my game instead of every pixel(each tile is 85x85, so incrementing y and x by 85 instead of 1) and then adding the player offset % 85, but I had issues with that jumping around because the offset % 85 didn't go to 0 right when it jumped to the next tile, and I tried and tried to get that working in many different ways, but this is the only way I could get it to work. This is how it looks, and everything works fine besides the code being super slow.
Is there something I was missing when I was trying to optimize it, or is there a completely different way that would fix it as well. I've never really had to optimize code, so this is a new thing for me. I can tell all the lag is coming from this code because without it and just incrementing by 85 it works perfectly fine. Thank you!
7225 pointless operations per image
Conditions slow code down. When ever possible you should try to avoid them.
eg the line...
if(x % 85 == 0 && y % 85 == 0 && noise.simplex2(x, y) == 0) {
... means that you are evaluating the if statement 85 * 85 (7225) times for every less than one tree this is a massive amount of unneeded overhead.
Remove those 7224 useless iterations.
Avoid indexing arrays when possible by storing repeated array lookups in a variable.
Simplify your math. eg ((x-offset[0])*-1)+canvas.width can be simplified to canvas.width - x + offset[0].
Offload as much as you can to the GPU. By default all position calculations are via the transform done on the GPU so that above math can be done once before the loop.
General rule for performance, reduce the amount of code inside a loop by moving what you can to outside the loop.
The snippet below implements the above points.
As you have not provided details as to the ranges of offset and canvas size the code below could be further optimized
var x, y;
const STEP = 85;
const offsetX = offset[0];
const offsetY = offset[1];
const startX = Math.floor(offsetX / STEP) * STEP;
const startY = Math.floor(offsetY / STEP) * STEP;
const endX = startX + canvas.width;
const endY = startY + canvas.height;
ctx.setTransform(1, 0, 0, 1, canvas.width - offsetX, canvas.height - offsetY);
for (x = startX; x < endX; x += STEP) {
for (y = startY; y < endY; y += STEP) {
if (noise.simplex2(x, y) == 0) {
ctx.drawImage(treeimage, x, y);
}
}
}
// reset transform
ctx.setTransform(1, 0, 0, 1, 0, 0);
Consider a quad tree
The call to simplex2 I suspect will be very slow. All the implementations I have seen are done very poorly. As the result of simplex is constant for any coordinate it should only be done once per coordinate before the game starts (outside the code in production).
As you want an infinite (like) playfield (infinite is impossible) the RAM requirement way too large. There is not much I can suggest, well apart from... Drop the infinite and set a practical limit to the playfield size which will allow you to create a quad tree map that will make it fly.
Many many years ago, as computers weren't as fast as today and you had to do some hefty mathematical operations like computing the sine or cosine - or even the modulo - there was just one option:
instead of calculating it everytime you need it, calculate it once and store it in a huge look-up table. Looking up a value is of course way faster than computation.
So in your case I'd recommend generating two arrays for the modulo of x and y
let xModulo = [];
let yModulo = [];
for (let a = 0; a < canvas.width; a++) {
xModulo.push(a % 85);
}
for (let a = 0; a < canvas.height; a++) {
yModulo.push(a % 85);
}
and in your render loop look up the values from the arrays like:
if (xModulo[x] == 0 && yModulo[y] == 0 && noise.simplex2(x, y) == 0) {
ctx.drawImage(treeimage, ((x - offset[0]) * -1) + canvas.width, ((y - offset[1]) * -1) + canvas.height);
}
That should give a noticeable performance boost. Depending on your needs you might need to change canvas.width / canvas.height to some higher value.
You might even consider generating a look-up table for the simplex noise.
Related
I struggle with both anything math, and describing my problems, so I'm going to keep include lots of images to help visualize it. I've been plotting prime numbers on a polar graph, to demonstrate the patterns that emerge.
For anyone unfamiliar, it looks like this.
Visualization
The problem that I've been having is that eventually the circles get too small to be seen. The code I'm using looks like this:
let radius;
let theta;
let toCalc = 50000
let primes;
let size = 1;
function setup() {
createCanvas(800,800);
primes = getPrimes(toCalc);
//put origin point in center
r = height * 0.45;
theta = 0;
noStroke();
color(255,255,255)
scale(0.01)
}
function draw() {
translate(width / 2, height / 2);
background(10);
scale(size);
//the for loop takes forever to fully create all of the circles, so it takes a really long time //to fully generate so i might leave it overnight sometime
for(var i = 0; i < toCalc; i++)
{
current = primes[i];
let x = current * cos(current);
let y = current * sin(current);
//the horrific (5/exp(size)*2) is something i threw together to get exponential scaling
//with the zoom at roughly the right level
circle(x, y, (5/exp(size))*3)
}
console.log(size)
}
function getPrimes(max) {
//i timed this code and apparently its actually really really performant,
//even up to a million primes. its the for loop that really kills performance.
var sieve = [], i, j, primes = [];
for (i = 2; i <= max; ++i) {
if (!sieve[i]) {
// i has not been marked -- it is prime
primes.push(i);
for (j = i << 1; j <= max; j += i) {
sieve[j] = true;
}
}
}
return primes;
}
//measuring mousewheel for zoom
function mouseWheel(event) {
//ternary to check if it was a scrolldown or scrollup
//have to make zoom out by size/10 because size is represented by a value from 0 to 1
event.delta >= 0 ? size-=size/10 : size+=size/10
//returning false to block page scrolling
return false;
}
Currently, totally arbitrarily, I have been using this to scale the circles, and it works relatively well up to a scale of 0.005 on a scale of 0 to 1, but then the circles get too small and disappear.
5/exp(size))*3
What would be the best way to scale circles like this as the user zoomed out?
I'm making a 2D game in JavaScript. For it, I need to be able to "perfectly" check collision between my players(the game has two players, open the picture please) and the walls! I mean, I have a function that actually works, but when I make them jump against the walls they pass through the walls and keep moving until they reach another area or even leave the canvas!
Also, if they are falling down and I make them collide with a wall, they just stop there wich is also pretty bad!
I really need help with that!! It's a university project and I have to finnish it really soon!
My game looks like this
The collision detection function I have is here:
function blockRectangle (objA, objB) {
var distX = (objA.x + objA.width / 2) - (objB.x + objB.width / 2);
var distY = (objA.y + objA.height / 2) - (objB.y + objB.height / 2);
var sumWidth = (objA.width + objB.width) / 2;
var sumHeight = (objA.height + objB.height) / 2;
if (Math.abs(distX) < sumWidth && Math.abs(distY) < sumHeight) {
var overlapX = sumWidth - Math.abs(distX);
var overlapY = sumHeight - Math.abs(distY);
if (overlapX > overlapY) {
objA.y = distY > 0 ? objA.y + overlapY : objA.y - overlapY;
}
else {
objA.x = distX > 0 ? objA.x + overlapX : objA.x - overlapX;
}
}
}
I did the walls with a maze and I'm using a for cycle to check the collisions with all of the walls I have saved in an array!
As you can see here:
for (var i in walls) {
var wall = walls[i];
if ((player.x < (wall.x + wall.width)) && ((player.x + player.width) > wall.x) && (player.y < (wall.y + wall.height)) && ((player.height + player.y) > wall.y)) {
player.falling = false;
}
blockRectangle(player, wall);
}
Please help me!!! Thank you all!
In your case I doubt a pixel perfect collision is required.
You can maintain a boolean matrix to store the position of solid objects. Solid objects like walls or players. Then in every frame you can check if your player is trying to move to a position where there is a solid object, if it is then stop it. You don't have to create grid of width x height in pixels, but rather choose a largest block (single element in the grid) in which each solid object reasonably occupies most of the block.
For example you can choose block size to be player_width / 2 x player_height /2.
See following image with grid
Another simple way could be to just check the background pixel color. Since your game is simple, background and object colors are different. So you just have to check if the player is trying to move somewhere where pixel color is not of background, thus there is a solid object and player should stop. You don't have to test for a lot of pixels, just 1 pixel in the direction the player is trying to move. (1 for horizontal and 1 for vertical). This however can not be used if you don't have a clear background color. Background color here is kind of the boolean grid for us in the previous suggestion.
I've made a script where there are supposed to be little balls that attract eachother in real time. The problem it is EXTREMELY slow. I used animation frame, so I think it should be updating every frame, but it isn't. Here is the code:
$(function() {
var mouseDown
var c = document.getElementById('myCanvas');
var ctx = c.getContext("2d");
var objects = []
c.addEventListener("mousedown", onMouseDown);
c.addEventListener("mouseup", onMouseUp);
function createSquare(x, y, size, direction, xVel, yVel) {
this.x = x;
this.y = y;
this.size = size;
this.drawStylus = drawStylus;
};
function drawStylus() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
ctx.fill();
};
function getDistance(x1, y1, x2, y2) {
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
}
function draw() {
ctx.clearRect(0, 0, 5000, 5000);
for (i = 0; i < objects.length; i++) {
var x = objects[i][0]
var y = objects[i][1]
var size = objects[i][2]
var dir = Math.random() * Math.PI * 2
var force = 0
var xVel = 0
var yVel = 0
for (n = 0; n < objects.length; n++) {
if (n != i) {
force = 100 * objects[n][2] / getDistance(x, y, objects[n][0], objects[n][1])
angle = Math.atan2(y - objects[n][1], x - objects[n][0])
xVel += force * -Math.cos(angle)
yVel += force * -Math.sin(angle)
window.requestAnimationFrame(draw)
};
};
ctx.beginPath();
ctx.arc(x + xVel, y + yVel, size, 0, 2 * Math.PI);
ctx.fill();
};
};
function onMouseDown() {
mouseDown = true
x = event.clientX
y = event.clientY
size = 100
animation = function() {
size = size + 20
var cursorSquare = new createSquare(x, y, size);
cursorSquare.drawStylus();
anim = window.requestAnimationFrame(animation)
};
window.requestAnimationFrame(animation)
};
function onMouseUp() {
if (mouseDown) {
window.cancelAnimationFrame(anim)
var newSquare = new createSquare(x, y, size);
objects.push([x, y, size])
mouseDown = false
};
};
function loop() {
draw();
window.requestAnimationFrame(loop);
};
function init() {
loop();
};
init()
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
<canvas id='myCanvas' width="5000" height="5000" style="border:1px solid #000000;"></canvas>
You are calling requestAnimationFrame for each object, this is the wrong way to use requestAnimationFrame (RAF).
You should only call it once per frame not once per object.
function mainLoop(time){ // main loop RAF will add the time in milliseconds to the arguments.
ctx.clearRect(0,0,canvas.width,canvas.height); // clear
draw(); // call the draw loop
requestAnimationFrame(loop); // request next frame
}
requestAnimationFrame(loop); // request next frame
Using the draw functions like ctx.arc is very slow. You will get much better performance if you render images instead ctx.drawImage. You can create a canvas, draw the arc on that canvas and then draw that canvas with ctx.drawImage(canvasImage,... to get a much faster update.
The other answer advised you to use forEach, don't use forEach or any of the array functions that involve callbacks as they are MUCH slower than using standard loops (for, while, do)
UPDATE
As things change rapidly in the browser world I have tested the use of forEach in this case and in this case the news is not good. forEach still adds a significant additional overhead on each iteration when compared to for, while , and do while
The important thing to note (and why I striked out the last paragraph) is that the overhead is per iteration, if you have a small number of iterations and a large amount of code per iteration then the overhead is insignificant and not worth the bother, personal coding style should make the choice of what style to use in those cases.
If on the other hand you have a large number of iterations and a small amount of processing per iteration then using forEach will significantly impact the performance of the loop.
This holds true for Chrome, Edge, and Firefox with all showing the standard iteration (for loops) with inline code (not calling a function) to be the quickest, next and 10% slower than standard iteration is standard iteration with a function call (like forEach), and then forEach with an additional overhead per iteration of over 2X. (each test used a 15-20 to 1 code balance, that is the code inside the iteration is 15-20 times longer than the minimum code required to iterate. So one line for the for, forEach loop and 10-15 lines of code inside the loop.)
If you are handling an array of a few thousand to tens of thousands the difference is not worth bothering with, If you are handling 100s of thousands to millions plus you should avoid forEach.
Note: I did not test forEach on typed arrays as that is not applicable in this case.
Tested on
Chrome Version 50.0.2661.37 beta-m
Firefox 46.0b2
Edge 25.10586
A couple of things that might help.
Take objects.length out of the for loop and assign it to a var before you start the loop. Currently your counting the length of objects on every interaction of your loop.
Better yet use objects.forEach to iterate over the arrays.
Lastly why does draw() call itself at the bottom of the two for loops? This is going to fill up the event loop very quickly and suspect the main reason for the slow down.
I am trying to design a traveling sine wave in JavaScript, but the design appears quite slow. The main bottleneck is the clearRect() for canvas clearing.
How can I solve this?
Also I am drawing the pixel by ctx.fillRect(x, y,1,1), but when I clear using clearRect(x, y,1,1), it leaves some footprints. Instead I have to do clearRect(x, y,5,5) to get proper clearing. What can be the work around?
/******************************/
var x = 0;
var sineval = [];
var offset = 0;
var animFlag;
function init() {
for(var i=0; i<=1000; ++i){
sineval[i] = Math.sin(i*Math.PI/180);
}
// Call the sineWave() function repeatedly every 1 microseconds
animFlag = setInterval(sineWave, 1);
//sineWave();
}
function sineWave()
{ //console.log('Drawing Sine');
var canvas = document.getElementById("canvas");
if (canvas.getContext) {
var ctx = canvas.getContext("2d");
}
for(x=0 ; x<1000 ;++x){
// Find the sine of the angle
//var i = x % 361;
var y = sineval[x+offset];
// If the sine value is positive, map it above y = 100 and change the colour to blue
if(y >= 0)
{
y = 100 - (y-0) * 70;
ctx.fillStyle = "green";
}
// If the sine value is negative, map it below y = 100 and change the colour to red
if( y < 0 )
{
y = 100 + (0-y) * 70;
ctx.fillStyle = "green";
}
// We will use the fillRect method to draw the actual wave. The length and breath of the
if(x == 0) ctx.clearRect(0,y-1,5,5);
else ctx.clearRect(x,y,5,5);
ctx.fillRect(x, y,1,1 /*Math.sin(x * Math.PI/180) * 5, Math.sin(x * Math.PI/180 * 5)*/);
}
offset = (offset > 360) ? 0 : ++offset ;
}
You need to refactor the code a bit:
Move all global variables such as canvas and context outside of the loop function
Inside the loop, clear full canvas at beginning, redraw sine
Use requestAnimationFrame instead of setInterval
Replace fillRect() with rect() and do a single fill() outside the inner for-loop
Using a timeout value of 1 ms will potentially result in blocking the browser, or at least slow it down noticeably. Considering that a monitor update only happens every 16.7ms this will of course be wasted cycles. If you want to reduce/increase the speed of the sine you can reduce/increase the incremental step instead.
In essence:
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var sineval = [];
var offset = 0;
init();
function init() {
for (var i = 0; i <= 1000; ++i) {
sineval.push(Math.sin(i * Math.PI / 180));
}
// Call the sineWave() function
sineWave();
}
function sineWave() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.beginPath();
ctx.fillStyle = "green";
// draw positive part of sine wave here
for (var x = 0; x < 1000; x++) {
var y = sineval[x + offset];
if (y >= 0) {
y = 100 - (y - 0) * 70;
ctx.rect(x, y, 2, 2);
}
}
ctx.fill();
ctx.beginPath();
ctx.fillStyle = "red";
// draw negative part of sine wave here
for (var x = 0; x < 1000; x++) {
var y = sineval[x + offset];
if (y < 0) {
y = 100 - (y - 0) * 70;
ctx.rect(x, y, 2, 2);
}
}
ctx.fill();
offset = (offset > 360) ? 0 : ++offset;
requestAnimationFrame(sineWave);
}
<canvas id="canvas" width=800 height=500></canvas>
And of course, if you load the script in <head> you need to wrap it in a window.onload block so canvas element is available. Or simply place the script at the bottom of the page if you haven't already.
A few speedups and odd ends:
In init, set up the sine wave pixel values one time.
Use typed arrays for these since sticking with integers is faster than using floats if possible.
We will manipulate the pixel data directly instead of using fill and clear. To start this, in init we call ctx.getImageData one time. We also just one time max the alpha value of all the pixels since the default 0 value is transparent and we want full opacity at 255.
Use setInterval like before. We want to update the pixels at a steady rate.
Use 'adj' as knob to adjust how fast the sine wave moves on the screen. The actual value (a decimal) will depend on the drawing frame rate. We use Date.now() calls to keep track of milliseconds consumed across frames. So the adjustment on the millisecond is mod 360 to set the 'offset' variable. Thus offset value is not inc by 1 every frame but instead is decided based on the consumption of time. The adj value could later be connected to gui if want.
At end of work (in sineWave function), we call requestAnimationFrame simply to do the ctx.putImageData to the canvas,screen in sync to avoid tearing. Notice 'paintit' function is fast and simple. Notice also that we still require setInterval to keep steady pace.
In between setting the offset and calling requestAnimationFrame, we do two loops. The first efficiently blackens out the exact pixels we drew from the prior frame (sets to 0). The second loop draws the new sine wave. Top half of wave is green (set the G in pixel rgba to 255). Bottom half is red (set the R pixel rgba to 255).
Use the .data array to paint a pixel, and index it to the pixel using 4x + 4y*canvas.width. Add 1 more if want the green value instead of the red one. No need to touch the blue value (byte offset 2) nor the already set alpha (byte offset 3).
The >>>0 used in some places turns the affected value into an unsigned integer if it wasn't already. It can also be used instead of Math.ceil. .data is typed Array already I think.
This answer is rather late but it addresses some issues brought up in comments or otherwise not yet addressed. The question showed up during googling.
Code hasn't been profiled. It's possible some of the speedups didn't speed anything up; however, the cpu consumption of firefox was pretty light by the end of the adjustments. It's set to run at 40 fps. Make 'delay' smaller to speed it up and tax cpu more.
var sineval;
var offset = 0;
var animFlag;
var canvas;
var ctx;
var obj;
var milli;
var delay=25;
var adj=1/delay; // .04 or so for 25 delay
function init() {
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
obj=ctx.getImageData(0,0,canvas.width,canvas.height);
for (let i=0; i<obj.data.length; i+=4) {
obj.data[i+3]=255; //set all alpha to full one time only needed.
}
sineval=new Uint8Array(1400); //set up byte based table of final pixel sine values.. 1400 degrees total
for (let i=0; i<=1400; ++i) { //1400
sineval[i] = (100-70*Math.sin(i*Math.PI/180))>>>0;
}
animFlag = setInterval(sineWave, delay); //do processing once every 25 milli
milli=Date.now()>>>0; //start time in milli
}
function sineWave() {
let m=((Date.now()-milli)*adj)>>>0;
let oldoff = offset;
offset=(m % 360)>>>0; //offset,frequency tuned with adj param.
for(x=0 ; x<1000 ;++x) { //draw sine wave across canvas length of 1000
let y=sineval[x+oldoff];
obj.data [0+x*4+y*4*canvas.width]=0; //black the reds
obj.data [1+x*4+y*4*canvas.width]=0; //black the greens
}
for(x=0 ; x<1000 ;++x) { //draw sine wave across canvas length of 1000
let y=sineval[x+offset];
if (y<100) {
obj.data [1+x*4+y*4*canvas.width]=255; //rGba //green for top half
} else {
obj.data [0+x*4+y*4*canvas.width]=255; //Rgba //red for bottom half
}
}
requestAnimationFrame(paintit); //at end of processing try to paint next frame boundary
}
function paintit() {
ctx.putImageData(obj,0,0);
}
init();
<canvas id="canvas" height=300 width=1000></canvas>
I'm trying to use the following effect on a HTML5 game: http://somethinghitme.com/projects/metaballs/
But since its a game (as opposed to graphical demo) I have tighter FPS requirements, I need time to calculate the physics and the some other things and my biggest bottleneck is the code for the metaballs.
The following code is what I got after stripping the original code for performance, its not as pretty but it's enough for my purposes:
ParticleSpawner.prototype.metabilize = function(ctx) {
var imageData = this._tempCtx.getImageData(0,0,900,675),
pix = imageData.data;
this._tempCtx.putImageData(imageData,0,0);
for (var i = 0, n = pix.length; i <n; i += 4) {
if(pix[i+3]<210){
pix[i+3] = 0;
}
}
//ctx.clearRect(0,0,900,675);
//ctx.drawImage(this._tempCanvas,0,0);
ctx.putImageData(imageData, 0, 0);
}
I had another loop on my code and I managed to increase its performance by using the technique described on the following link http://www.fatagnus.com/unrolling-your-loop-for-better-performance-in-javascript/ but using the same on this actually decreases the performance (maybe I did it wrong?)
I also researched web workers to see if I could split the load (since the code runs for each pixel individually) but the example I found on this link http://blogs.msdn.com/b/eternalcoding/archive/2012/09/20/using-web-workers-to-improve-performance-of-image-manipulation.aspx actually runs slower when using web workers.
What else can I do? Is there a way to remove the branching from the loop? Another way to unroll it? Or is this the best I can do?
Edit:
This is some of the surrounding code:
ParticleSpawner.prototype.drawParticles = function(ctx) {
this._tempCtx.clearRect(0,0,900,675);
var iterations = Math.floor(this._particles.getNumChildren() / 8);
var leftover = this._particles.getNumChildren() % 8;
var i = 0;
if(leftover > 0) {
do {
this.process(i++);
} while(--leftover > 0);
}
do {
this.process(i++);
this.process(i++);
this.process(i++);
this.process(i++);
this.process(i++);
this.process(i++);
this.process(i++);
this.process(i++);
} while(--iterations > 0);
this.metabilize(ctx);
}
and the process method:
ParticleSpawner.prototype.process = function(i) {
if(!this._particles.getChildAt(i)) return;
var bx = this._particles.getChildAt(i).x;
var by = this._particles.getChildAt(i).y;
if(bx > 910 || bx < -10 || by > 685) {
this._particles.getChildAt(i).destroy();
return;
}
//this._tempCtx.drawImage(this._level._queue.getResult("particleGradient"),bx-20,by-20);
var grad = this._tempCtx.createRadialGradient(bx,by,1,bx,by,20);
this._tempCtx.beginPath();
var color = this._particles.getChildAt(i).color;
var c = "rgba("+color.r+","+color.g+","+color.b+",";
grad.addColorStop(0, c+'1.0)');
grad.addColorStop(0.6, c+'0.5)');
grad.addColorStop(1, c+'0)');
this._tempCtx.fillStyle = grad;
this._tempCtx.arc(bx, by, 20, 0, Math.PI*2);
this._tempCtx.fill();
};
As can be seen, I tried using images instead of gradient shapes, but the performance was worse, I also tried to use ctx.drawImage instead of putImageData, but it loses the alpha and is not faster. I can't think of an alternative to achieve the desired effect. The current code runs perfectly on Google Chrome, but Safari and Firefox are really slow. Is there anything else I can try? Should I just give up on those browsers?
Updated
Some techniques that can be applied
Here are some optimization techniques that can be applied to make this work more fluent in FF and Safari as well.
That being said: Chrome's canvas implementation is very good and much faster (at the moment) than the bone provided by Firefox and Safari. The new Opera uses the same engine as Chrome and is (about?) equally as fast as Chrome's.
For this to work fine cross-browser some compromises needs to be made and as always quality will suffer.
The techniques I try to demonstrate are:
Cache a single gradient that is used as meta ball basis
Cache everything if possible
Render in half resolution
Use drawImage() to update main canvas
Disable image smoothing
Use integer coordinates and sizes
Use requestAnimationFrame()
Use while loops as often as you can
Bottlenecks
There is a high cost in generating a gradient for each metaball. So when we cache this once and for all we will just by doing that notice a huge improvement in performance.
The other point is getImageData and putImageData and the fact that we need to use a high-level language to iterate over a low-level byte array. Fortunately the array is typed array so that helps a little but we won't be able to get much more out of it unless we sacrifice more quality.
When you need to squeeze everything you can the so-called micro-optimizations becomes vital (these has an undeserved bad reputation IMO).
From the impression of your post: You seem to be very close to have this working but from the provided code I cannot see what went wrong so-to-speak.
In any case - Here is an actual implementation of this (based on the code you refer to):
Fiddle demo
Pre-calculate variables in the initial steps - everything we can pre-calculate helps us later as we can use the value directly:
var ...,
// multiplicator for resolution (see comment below)
factor = 2,
width = 500,
height = 500,
// some dimension pre-calculations
widthF = width / factor,
heightF = height / factor,
// for the pixel alpha
threshold = 210,
thresholdQ = threshold * 0.25,
// for gradient (more for simply setting the resolution)
grad,
dia = 500 / factor,
radius = dia * 0.5,
...
We use a factor here to reduce the actual size and to scale the final render to on-screen canvas. For each 2 factor you save 4x pixels exponentially. I preset this to 2 in the demo and this works great with Chrome and good with Firefox. You might even be able to run factor of 1 (1:1 ratio) in both browsers on a better spec'ed machine than mine (Atom CPU).
Init the sizes of the various canvases:
// set sizes on canvases
canvas.width = width;
canvas.height = height;
// off-screen canvas
tmpCanvas.width = widthF;
tmpCanvas.height = heightF;
// gradient canvas
gCanvas.width = gCanvas.height = dia
Then generate a single instance of a gradient that will be cached for the other balls later. Worth to notice: I initially used only this to draw all the balls but later decided to cache each ball as an image (canvas) instead of drawing and scaling.
This has a memory penalty but increases the performance. If memory is of importance you can skip the caching of rendered balls in the loop that generates them and just drawImage the gradient canvas instead when you need to draw the balls.
Generate gradient:
var grad = gCtx.createRadialGradient(radius, radius, 1, radius, radius, radius);
grad.addColorStop(0, 'rgba(0,0,255,1)');
grad.addColorStop(1, 'rgba(0,0,255,0)');
gCtx.fillStyle = grad;
gCtx.arc(radius, radius, radius, 0, Math.PI * 2);
gCtx.fill();
Then in the loop that generates the various metaballs.
Cache the calculated and rendered metaball:
for (var i = 0; i < 50; i++) {
// all values are rounded to integer values
var x = Math.random() * width | 0,
y = Math.random() * height | 0,
vx = Math.round((Math.random() * 8) - 4),
vy = Math.round((Math.random() * 8) - 4),
size = Math.round((Math.floor(Math.random() * 200) + 200) / factor),
// cache this variant as canvas
c = document.createElement('canvas'),
cc = c.getContext('2d');
// scale and draw the metaball
c.width = c.height = size;
cc.drawImage(gCanvas, 0, 0, size, size);
points.push({
x: x,
y: y,
vx: vx,
vy: vy,
size: size,
maxX: widthF + size,
maxY: heightF + size,
ball: c // here we add the cached ball
});
}
Then we turn off interpolating for images that are being scaled - this gains even more speed.
Note that you can also use CSS in some browsers to do the same as here.
Disable image smoothing:
// disable image smoothing for sake of speed
ctx.webkitImageSmoothingEnabled = false;
ctx.mozImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
ctx.oImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false; // future...
Now the non-critical parts are done. The rest of the code utilizes these tweaks to perform better.
The main loop now looks like this:
function animate() {
var len = points.length,
point;
// clear the frame of off-sceen canvas
tmpCtx.clearRect(0, 0, width, height);
while(len--) {
point = points[len];
point.x += point.vx;
point.y += point.vy;
// the checks are now exclusive so only one of them is processed
if (point.x > point.maxX) {
point.x = -point.size;
} else if (point.x < -point.size) {
point.x = point.maxX;
}
if (point.y > point.maxY) {
point.y = -point.size;
} else if (point.y < -point.size) {
point.y = point.maxY;
}
// draw cached ball onto off-screen canvas
tmpCtx.drawImage(point.ball, point.x, point.y, point.size, point.size);
}
// trigger levels
metabalize();
// low-level loop
requestAnimationFrame(animate);
}
Using requestAnimationFrame squeezes a little more of the browser as it is intended to be more low-level and more efficient than just using a setTimeout.
The original code checked for both edges - this is not necessary as a ball can only cross one edge at the time (per axis).
The metabolize function is modified like this:
function metabalize(){
// cache what can be cached
var imageData = tmpCtx.getImageData(0 , 0, widthF, heightF),
pix = imageData.data,
i = pix.length - 1,
p;
// using a while loop here instead of for is beneficial
while(i > 0) {
p = pix[i];
if(p < threshold) {
pix[i] = p * 0.1667; // multiply is faster than div
if(p > thresholdQ){
pix[i] = 0;
}
}
i -= 4;
}
// put back data, clear frame and update scaled
tmpCtx.putImageData(imageData, 0, 0);
ctx.clearRect(0, 0, width, height);
ctx.drawImage(tmpCanvas, 0, 0, width, height);
}
Some micro-optimizations that actually helps in this context.
We cache the pixel value for alpha channel as we use it more than two times. Instead of diving on 6 we multiply with 0.1667 as multiplication is a tad faster.
We have already cached tresholdQ value (25% of threshold). Putting the cached value inside the function would give a little more speed.
Unfortunately as this method is based on the alpha channel we need to clear also the main canvas. This has a (relatively) huge penalty in this context. The optimal would be to be able to use solid colors which you could "blit" directly but I didn't look Into that aspect here.
You could also had put the point data in an array instead of as objects. However, since there are so few this will probably not be worth it in this case.
In conclusion
I have probably missed one or two (or more) places which can be optimized further but you get the idea.
And as you can see the modified code runs several times faster than the original code mainly due to the compromise we make here with quality and some optimizations particularly with the gradient.
There is scope of improvement in programming, in drawing particle section.
instead of using
if(leftover > 0) {
do {
this.process(i++);
} while(--leftover > 0);
}
you can just use this
while(leftover > 0) {
this.process(i++);
leftover --;
}
This will reduce one step of condition checking of if and also the (--)operator that decrements one value and checks. this will reduce the complexity
with all do while you have (--) that can be removed, with simple statement this will reduce the Cyclomatic Complexity of this particular code and make this code faster.
ultimately this will give the performance improvement with the faster processing of your code and less use of CPU and resources. although Ken's answer is also working one, I have created one more fiddle that is similar to your sample site with more speed.
fiddle
If any problem please leave a comment, and update fiddle with game code for performance check.
This loop is already pretty simple, uses stable types that JIT likes, so I don't think you can get significant improvement.
I've eliminated +3 and unrolled it a bit (assuming width*height is divisible by 4). I've added |0 "cast" to integer that makes it sliiightly faster in V8.
Overall it gave 10% improvement:
var i = (3 - 4)|0;
var n = (pix.length - 16)|0;
while(i < n) {
if (pix[i+=4] < 210){
pix[i] = 0;
}
if (pix[i+=4] < 210){
pix[i] = 0;
}
if (pix[i+=4] < 210){
pix[i] = 0;
}
if (pix[i+=4] < 210){
pix[i] = 0;
}
}
If you need it to be massively faster, then maybe use lower-resolution canvas for the effect?