Why does this script lag delay in Javascript? - javascript

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.

Related

Optimizing HTML5 canvas game loop

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.

Make clearRect() of canvas work faster

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>

Why is this canvas code so slow?

I have short code, that draws circles (orbits) dots (satellites) on these orbits. Satellites are moving around orbits. In fact code isn't mine, but I was asked to solve the problem.
According to profiler in chrome and firefox, function drawSatellite eats 50%-100% cpu and I would like to know why.
Canvas is same big as your window (1920x1080). There are around 160 orbits (increasing with time page is online).
This is drawSatellite:
OrbitBackground.prototype.drawSatellite = function(ctx, satellite) {
ctx.fillStyle = satellite.satellite.fill;
ctx.beginPath();
if (++satellite.satellite.angularPosition == 360)
satellite.satellite.angularPosition = 0;
// 1 FPS = 60 calls => 180 / 6 (6-times faster # 60 FPS) = 30
var radians = satellite.satellite.angularPosition * Math.PI / 30 / satellite.rps;
if (satellite.backward)
radians = -radians;
ctx.arc(
satellite.satellite.x + satellite.orbit.radius * Math.cos(radians),
satellite.satellite.y + satellite.orbit.radius * Math.sin(radians),
satellite.satellite.radius,
0,
Math.PI*2,
true
);
ctx.closePath();
ctx.fill();
};
Function that calls it:
OrbitBackground.prototype.drawFrame = function() {
if (this.running)
requestAnimationFrame(this.drawFrame.bind(this));
this.dynamicStageCtx.clearRect(0, 0, this.pageWidth, this.pageHeight);
for (var i=0; i < this.orbits.length; i++) {
this.drawSatellite(this.dynamicStageCtx, this.orbits[i]);
}
};
You're doing this:
Loop:
set fill style
begin path
make path
end path
fill
You would be much faster doing this:
set fill style (just once, before loop)
begin path (just one path, with loop-number of subpaths)
Loop:
moveTo (start of subpath)
make path
close path
fill (just once, after loop)
But that requires the fill style to be the same on every satellite. If there are only a few colors, you could try to bunch them by their colors.
Note also that calcluating cosine and sine are slow (all trig functions and square root calls are slow), and if you can avoid their use you will be better off.
The size (pixel count) of your canvas also matters. Consider making the canvas half-size or quarter-size (960x540 or 480x270) and scaling it up with CSS.
Might be a problem:
I don't see a ctx.beginPath before your ctx.arc command.
Without ctx.beginPath all your previous arcs are being redrawn along with your current arc.
A small optimization
Assign Math.PI*2 to a variable since it's used very often
var PI2=Math.PI*2;
How to eliminate the slowest part of your code ( Math.cos and Math.sin ).
Since your nodes are going in repeating orbits, you can precalculate all the untranslated [x,y] for a complete orbit.
var statellite.orbitTrig=[];
for(var i=0;i<360;i++){
var radians=PI2/360*i;
var x=satellite.orbit.radius * Math.cos(radians)
var y=satellite.orbit.radius * Math.sin(radians)
satellite.orbitTrig.push({x:x,y:y});
}
Then you can refer to the precalculated values in your animation loop.
var trig=satellite.orbitTrig[satellite.satellite.angularPosition];
var x=satellite.satellite.x + trig.x;
var y=satellite.satellite.y + trig.y;
ctx.beginPath();
ctx.arc(x,y,satellite.satellite.radius,0,PI2);
ctx.closePath();
ctx.fill();

This is an infinite loop, but I cannot for the life of me figure out why

So pretty much I'm making a little web app to get better with using the canvas, but I'm stuck. I would like a rotating n-sided polygon (the drawing of lines already works). The game loop loops through a grid array (each point on the grid holds a subclass of a Point() object), and calls the tick() method on each. Everything works fine until it hits a ShapePoint() object (middle mouse to place on canvas). The ShapePoint's tick() method is somehow an infinite loop. If you put a console.log("hi") inside it, it will give you about 2000 "hi" messages, so it's working (in theory). The funny bit is, even though it is looping through stoke()'es, nothing is happening!
//################################################################
// THIS IS THE PROBLEM CLASS.
// So pretty much, when the game loop calls the tick() funciton
// of ANY ShapePoint object, everything hangs. The game is still
// looping through the ENTIRE tick() function (put console.log()
// functions in and you'll see what I mean) continually, but the
// effects it is supposed to display aren't shown.
//
//################################################################
function ShapePoint(x, y, sides) {
//position variable
this.positionOnCanvas = [x, y];
//number of sides
this.N = sides;
//current step
this.step = 0;
//the array to store all the angle data
this.thetas = new Array(this.N);
//the array to store all the vertex data
this.vertList = new Array(this.N);
//function to increase all the angels by an even amount
this.stepPoints = function(s) {
//for every side
for (i=0; i<this.N; i++) {
//multiply the current 'i' value by ((360/number of sides) + current step). This serves to create points at even intervals all the way around a circle, and have it increase by s every loop
this.thetas[i] = i*((360/this.N) + s);
//get the x value with 40*cos(angle for this point). Same for y, only with sin. Round both to 2 decimal places
this.vertList[i] = [Math.round((40*(Math.cos(this.thetas[i])))*100)/100, Math.round((40*(Math.sin(this.thetas[i])))*100)/100];
//if the current angle is between 90 and 180...
if (this.thetas[i]>=90 && this.thetas[i]<=180) {
//invert the x value
this.vertList[i][0] *= -1;
//else if the angle is between 180 and 270...
} else if (this.thetas[i]>=180 && this.thetas[i]<=270) {
//invert both the x and the y values
this.vertList[i][0] *= -1;
this.vertList[i][1] *= -1;
//else if the angle is between 270 and 360...
} else if (this.thetas[i]>=270 && this.thetas[i]<=360) {
//invert the y value
this.vertList[i][1] *= -1;
}
//nothing needed for 0-90 because both are positive
}
}
this.tick = function() { //<<<<<<<<THIS IS THE PROBLEM FUNCTION!
//setup all the points forward
this.stepPoints(this.step);
//for every side in this polyogn...
for (i=0; i<this.N; i++) {
//shorten the location of the positions
var posX = this.vertList[i][0] + this.positionOnCanvas[0];
var posY = this.vertList[i][1] + this.positionOnCanvas[1];
//begin drawing
ctx.beginPath();
//move to the x and y location of the current point
ctx.moveTo(posX, posY);
//if point is not the last in the array...
if (i < this.N-1) {
//draw a line to the next point in the array
ctx.lineTo(this.vertList[i+1][0] + this.positionOnCanvas[0], this.vertList[i+1][1] + this.positionOnCanvas[1]);
//else...
} else {
//draw a line to the first point in the array
ctx.lineTo(this.vertList[0][0] + this.positionOnCanvas[0], this.vertList[0][1] + this.positionOnCanvas[1]);
}
//draw a line
ctx.strokeStyle = "#000000";
ctx.lineWidth = 0.5;
//end
ctx.stroke();
//draw the vertex
ctx.fillStyle = "orange";
ctx.fillRect(posX-2, posY-2, 4, 4);
}
//draw the origin of the polygon
ctx.fillStyle = "lightPurple";
ctx.fillRect(this.positionOnCanvas[0]-2, this.positionOnCanvas[1]-2, 4, 4);
//if the step is greater than 360, set it to 0
this.step = this.step % 36; //(thanks Neikos!)
}
}
ShapePoint.prototype = new Point();
So I've spent hours tweaking different things, and I cannot for the life of me see what the problem is! If anybody can figure it out, it would be fantastic. If you need more context as to how exactly this is implemented, I've created a JSFiddle for you. Thanks in advance, this place is always so helpfull!
EDIT :: I do realize my code is a bit clunky, but I typing out what everything does really helps me learn for the next time
user2310289 is correct in his/her comment above: you're using a single global i variable in both stepPoints and tick, so these methods are interfering with each other.
There are some languages where a variable used in a method is implicitly local unless declared otherwise, but JavaScript is not one of those languages. In JavaScript you need to use the var keyword to declare your local variables, otherwise they are implicitly global.

Increasing real-time performance on canvas' effects

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?

Categories

Resources