Increasing real-time performance on canvas' effects - javascript

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?

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.

Making a sine wave curve around a path in javascript

I am trying to figure out a nice way to make a sine wave flow naturally along a javascript path. I made something like this:
Which captures some of the intent but it's very forced and unnatural, especially around the change direction. I also would love to accommodate for higher slope, but not sure if that ives a more natural effect or not.
Any thoughts on how I might be able to accomplish this?
The intent was:
1) Take a set of points
2) Break into equal segments
3) Adjust the actual line's position by the difference of the sin coords and the actual line coords.
This gives a pretty weak display though, and I'd like to create something that was more natural and flowing as if to capture the flow of a sine wave travelling along a path.
var c = document.getElementById("c");
var ctx = c.getContext("2d");
var cw = c.width = window.innerWidth;
var ch = c.height = window.innerHeight;
var cx = cw / 2,
cy = ch / 2;
var rad = Math.PI / 180;
var w = cw;
var h = ch * 0.3;
var amplitude = h;
var frequency = 0.01;
var phi = 0;
var frames = 0;
var stopped = true;
ctx.lineWidth = .4;
var offset = 100;
var points = interpolateLineRange( [ [0, 0], [ 95, 58], [84, 158], [350, 300], [540, 190] ], 20);
points = interpolateLineRange(points, 100);
ctx.moveTo(0, 0);
var distance_traveled = 0;
var current_slope = 0;
for (var ii in points) {
if (ii == 0) {
continue;
}
distance_traveled += dist(points[ii - 1], points[ii]);
current_slope = slope(points[ii - 1], points[ii]);
var newY = Math.sin(distance_traveled * .07) * 45 + points[ii][1];
var diff = newY - points[ii][1];
if (points[ii][1] > points[ii - 1][1]) {
ctx.lineTo(points[ii][0] - diff, newY);
} else {
ctx.lineTo(points[ii][0] + diff, newY);
}
}
ctx.stroke();
ctx.moveTo(0, 0);
for (var ii in points) {
ctx.lineTo(points[ii][0], points[ii][1]);
}
ctx.strokeStyle = 'red';
ctx.stroke();
The problem isn't really "drawing sine waves along a path": that part is actually trivial. Take your path section, express it in terms of a distance or time variable, and then draw the sines (or anything else) as an offset function:
for t=0; t<distance; t+=fraction of distance:
point = path.get(t)
normal = path.normal(t)
strength = sin(t)
if t=0:
ctx.moveTo(point + strength * normal)
else:
ctx.lineTo(point + strength * normal)
Easy enough, let's implement that: http://jsbin.com/nefemazovo/edit?js,output
Sure, it's a bit of code, but it's hardly complicated: just a class that models a polygonal path that tracks its length as we add points to it, and a draw function that draws the polygon, as well as some offset function, by sampling the polygon at regular intervals and computing the normal at each point.
The real question is: how are you going to deal with overlaps in your offset data? For instance, from the example above:
There's a pretty obvious area here where we're going to have to do ... something:
So what do we do? Turns out: no one knows, that's really up to you. For instance, you could draw "uneven" sines so that you always end up with a node at the end points of your polygonal sections. Might work, but you might also still have overlap if there's a small enough angle between consecutive segments. Plus your sines would be uneven, so would that look good? Ehh... up to you. Or, you could dampen the offset strength to zero at the polygon transition, and then ramp it back up to 100%, but will that look good? No idea, that is your call. You could also use interpolation so that the sine waves "blend" at the transition. Will that look good? Again, no idea, still up to you. You could even replace the offending section of polygon with something like a quadratic or cubic curve, so you always have smooth transitions along which sine offsets will "just work", but will that look good? ...you get the idea =)
The part of this question we can answer isn't super interesting, and the part that's interesting we unfortunately cannot answer for you...
We can give advice, though: I don't know what your polygon represents, but "curves" almost always work better as spines (almost, because curves can have discontinuities as well, which is the very thing you want to avoid), so if you can construct curves instead, probably worth it. However, that won't solve the problem of weird overlaps when your angles are too small:
You're still left with problems that can only be solved with "executive decisions" more than textbook "in this situation, do this: ..." solutions.

Why does this script lag delay in 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.

Drawing image in a loop onto canvas - how could I optimize this code?

I am drawing a background image on a canvas element. I create a loop with requestAnimationFrame. In this loop, I draw an image onto the canvas with the appropriate coordinates.
The animation seems to be smooth, in Chrome 60 fps, but I have few glitches every now and then. It's worse in Firefox than in Chrome. It's better when I view it with a clean profile, without open tabs - but it's still not perfect.
Here is the full source: http://jsbin.com/vopiw/1/edit?html,output
This function gets called in every frame:
function draw(delta) {
totalSeconds += delta;
var vx = 100; // the background scrolls with a speed of 100 pixels/sec
var numImages = Math.ceil(canvas.width / img.width) + 1;
var xpos = totalSeconds * vx % img.width;
context.save();
context.translate(-xpos, 0);
for (var i = 0; i < numImages; i++) {
context.drawImage(img, i * img.width, 0);
}
context.restore();
}
Can you spot anything, which could be a real performance drawback?
What I have found so far:
memory consumption is growing slightly but constantly
BUT there is no garbage collection happening, which could be blamed for the glitches
Do you maybe have any clues?
Use the image as a background image on the element itself and use background position to scroll it.
Instead of the img onload just go directly into the code:
(function imageLoaded() {
canvas.style.backgroundImage = 'url(...)';
canvas.style.backgroundRepeat = 'repeat-x';
draw(0);
...
Then just update the draw() method with something like this:
// cache these
var iw = 400,
cw = canvas.width;
function draw(delta) {
totalSeconds += delta;
var vx = 100; // if always 100 just insert the value directly below
var numImages = ((cw / iw)|0) + 1; // use logic OR to remove fractions
var xpos = totalSeconds * vx % iw;
// update background position
canvas.style.backgroundPosition = (-xpos + iw) + 'px 0';
}
The second issue is the way you calculate the time delta. Using the low-resolution timer can add to the jerky-ness.
Try to use the built-in high-resolution timer instead. Luckily the rAF provides a high-res time stamp which you can use instead:
function loop(now) { // use argument from rAF (hi-res timestamp)
if (!looping) {
return;
}
requestAnimationFrame(loop);
var deltaSeconds = (now - lastFrameTime) * 0.001; //mul is faster than div
lastFrameTime = now;
draw(deltaSeconds);
}
Modified jsbin
This hands the drawing action to the browser but have in mind that the gain is not all that. The reason is that the drawImage() method is pretty fast in itself but you are saving a few steps in the JavaScript which is the real bottle-neck (canvas is very fast in itself despite the myth) and the repetition of these draw operations are left to internal compiled code in the browser.
Other factors that influence the smoothness is the hardware clock and hardware capability in general as well as other things going on in the browser.
I would also put that canvas element on an absolute or fixed position as the browser will give the element a separate bitmap (not related to canvas bitmap) for it which could improve the CSS background performance (not shown in the modified jsbin).

Maximum size of a <canvas> element

I'm working with a canvas element with a height of 600 to 1000 pixels and a width of several tens or hundreds of thousands of pixels. However, after a certain number of pixels (obviously unknown), the canvas no longer display shapes I draw with JS.
Does anyone know if there's a limit?
Tested both in Chrome 12 and Firefox 4.
Updated 10/13/2014
All tested browsers have limits to the height/width of canvas elements, but many browsers also limit the total area of the canvas element. The limits are as follows for the browsers I'm able to test:
Chrome:
Maximum height/width: 32,767 pixels
Maximum area: 268,435,456 pixels (e.g., 16,384 x 16,384)
Firefox:
Maximum height/width: 32,767 pixels
Maximum area: 472,907,776 pixels (e.g., 22,528 x 20,992)
IE:
Maximum height/width: 8,192 pixels
Maximum area: N/A
IE Mobile:
Maximum height/width: 4,096 pixels
Maximum area: N/A
Other:
I'm not able to test other browsers at this time. Refer to the other answers on this page for additional limits.
Exceeding the maximum length/width/area on most browsers renders the canvas unusable. (It will ignore any draw commands, even in the usable area.) IE and IE Mobile will honor all draw commands within the usable space.
The accepted answer is outdated and incomplete.
Browsers impose different canvas size limitations, but these limitations often change based on the platform and hardware available. This makes it difficult to make statements like "the maximum canvas [area/height/width] of [browser] is [value]" because [value] can change based the operating system, available RAM, or GPU type.
There are two approaches to working with large HTML <canvas> elements:
Limit canvas dimensions to those known to work on all supported platforms.
Programmatically determine canvas limitations on the client before rendering.
Those looking to programmatically determine canvas limitations on the client should consider using canvas-size.
GitHub: https://github.com/jhildenbiddle/canvas-size
NPM: https://www.npmjs.com/package/canvas-size
From the docs:
The HTML canvas element is widely supported by modern and legacy
browsers, but each browser and platform combination imposes unique
size limitations that will render a canvas unusable when exceeded.
Unfortunately, browsers do not provide a way to determine what their
limitations are, nor do they provide any kind of feedback after an
unusable canvas has been created. This makes working with large canvas
elements a challenge, especially for applications that support a
variety of browsers and platforms.
This micro-library provides the maximum area, height, and width of an
HTML canvas element supported by the browser as well as the ability to
test custom canvas dimensions. By collecting this information before a
new canvas element is created, applications are able to reliably set
canvas dimensions within the size limitations of each
browser/platform.
Test results for a variety of platform and browser combinations are available here:
Test Results: https://github.com/jhildenbiddle/canvas-size#test-results
Full disclosure, I am the author of the library. I created it back in 2014 and recently revisited the code for a new canvas-related project. I was surprised to find the same lack of available tools for detecting canvas size limitations in 2018 so I updated code, released it, and hope it helps others running into similar issues.
I've ran into out of memory errors on Firefox with canvas heights greater than 8000, chrome seems to handle much higher, at least to 32000.
EDIT: After running some more tests, I've found some very strange errors with Firefox 16.0.2.
First, I seem to get different behavior from in memory (created in javascript) canvas as opposed to html declared canvas.
Second, if you don't have the proper html tag and meta charset, the canvas might be restricted to 8196, otherwise you can go up to 32767.
Third, if you get the 2d context of the canvas and then change the canvas size, you might be restricted to 8196 as well. Simply setting the canvas size before grabbing the 2d context allows you to have up to 32767 without getting memory errors.
I haven't been able to consistently get the memory errors, sometimes it's only on the first page load, and then subsequent height changes work. This is the html file I was testing with http://pastebin.com/zK8nZxdE.
iOS max canvas size (width x height):
iPod Touch 16GB = 1448x1448
iPad Mini = 2290x2289
iPhone 3 = 1448x1448
iPhone 5 = 2290x2289
tested on march 2014.
To expand a bit on #FredericCharette answer:
As per safari's content guide under section "Know iOS Resource Limits":
The maximum size for a canvas element is 3 megapixels for devices with less than 256 MB RAM and 5 megapixels for devices with greater or equal than 256 MB RAM
Therefore, any size variation of 5242880 (5 x 1024 x 1024) pixels will work on large memory devices, otherwise it's 3145728 pixels.
Example for 5 megapixel canvas (width x height):
Any total <= 5242880
--------------------
5 x 1048576 ~= 5MP (1048576 = 1024 x 1024)
50 x 104857 ~= 5MP
500 x 10485 ~= 5MP
and so on..
The largest SQUARE canvases are ("MiB" = 1024x1024 Bytes):
device < 256 MiB device >= 256 MiB iPhone 6 [not confirmed]
----------------- ----------------- ---------------------
<= 3145728 pixels <= 5242880 pixels <= 16 x 1024 x 1024 p
1773 x 1773 2289 x 2289 4096 x 4096
According to w3 specs, the width/height interface is an unsigned long - so 0 to 4,294,967,295 (if I remember that number right -- might be off a few).
EDIT: Strangely, it says unsigned long, but it testing shows just a normal long value as the max: 2147483647. Jsfiddle - 47 works but up to 48 and it reverts back to default.
Even though the canvas will allow you to put height=2147483647, when you start drawing, nothing will happen
Drawing happens only when I bring the height back to 32767
iOS has different limits.
Using the iOS 7 simulator I was able to demonstrate the limit is 5MB like this:
var canvas = document.createElement('canvas');
canvas.width = 1024 * 5;
canvas.height = 1024;
alert(canvas.toDataURL('image/jpeg').length);
// prints "110087" - the expected length of the dataURL
but if I nudge the canvas size up by a single row of pixels:
var canvas = document.createElement('canvas');
canvas.width = 1024 * 5;
canvas.height = 1025;
alert(canvas.toDataURL('image/jpeg'));
// prints "data:," - a broken dataURL
On PC-
I don't think there is a restriction but yes you can get out of memory exception.
On Mobile devices-
Here is the restrictions for the canvas for mobile devices:-
The maximum size for a canvas element is 3 megapixels for devices with less than 256 MB RAM and 5 megapixels for devices with greater or equal than 256 MB RAM.
So for example - if you want to support Apple’s older hardware, the size of your canvas cannot exceed 2048×1464.
Hope these resources will help you to pull you out.
When you are using WebGL canvases, the browsers (including the desktop ones) will impose extra limits on the size of the underlying buffer. Even if your canvas is big, e.g. 16,000x16,000, most browsers will render a smaller (let's say 4096x4096) picture, and scale it up. That might cause ugly pixelating, etc.
I have written some code to determine that maximum size using exponential search, if anyone ever needs it. determineMaxCanvasSize() is the function you are interested in.
function makeGLCanvas()
{
// Get A WebGL context
var canvas = document.createElement('canvas');
var contextNames = ["webgl", "experimental-webgl"];
var gl = null;
for (var i = 0; i < contextNames.length; ++i)
{
try
{
gl = canvas.getContext(contextNames[i], {
// Used so that the buffer contains valid information, and bytes can
// be retrieved from it. Otherwise, WebGL will switch to the back buffer
preserveDrawingBuffer: true
});
}
catch(e) {}
if (gl != null)
{
break;
}
}
if (gl == null)
{
alert("WebGL not supported.\nGlobus won't work\nTry using browsers such as Mozilla " +
"Firefox, Google Chrome or Opera");
// TODO: Expecting that the canvas will be collected. If that is not the case, it will
// need to be destroyed somehow.
return;
}
return [canvas, gl];
}
// From Wikipedia
function gcd(a,b) {
a = Math.abs(a);
b = Math.abs(b);
if (b > a) {var temp = a; a = b; b = temp;}
while (true) {
if (b == 0) return a;
a %= b;
if (a == 0) return b;
b %= a;
}
}
function isGlContextFillingTheCanvas(gl) {
return gl.canvas.width == gl.drawingBufferWidth && gl.canvas.height == gl.drawingBufferHeight;
}
// (See issue #2) All browsers reduce the size of the WebGL draw buffer for large canvases
// (usually over 4096px in width or height). This function uses a varian of binary search to
// find the maximum size for a canvas given the provided x to y size ratio.
//
// To produce exact results, this function expects an integer ratio. The ratio will be equal to:
// xRatio/yRatio.
function determineMaxCanvasSize(xRatio, yRatio) {
// This function works experimentally, by creating an actual canvas and finding the maximum
// value, the browser allows.
[canvas, gl] = makeGLCanvas();
// Reduce the ratio to minimum
gcdOfRatios = gcd(xRatio, yRatio);
[xRatio, yRatio] = [xRatio/gcdOfRatios, yRatio/gcdOfRatios];
// if the browser cannot handle the minimum ratio, there is not much we can do
canvas.width = xRatio;
canvas.height = yRatio;
if (!isGlContextFillingTheCanvas(gl)) {
throw "The browser is unable to use WebGL canvases with the specified ratio: " +
xRatio + ":" + yRatio;
}
// First find an upper bound
var ratioMultiple = 1; // to maintain the exact ratio, we will keep the multiplyer that
// resulted in the upper bound for the canvas size
while (isGlContextFillingTheCanvas(gl)) {
canvas.width *= 2;
canvas.height *= 2;
ratioMultiple *= 2;
}
// Search with minVal inclusive, maxVal exclusive
function binarySearch(minVal, maxVal) {
if (minVal == maxVal) {
return minVal;
}
middle = Math.floor((maxVal - minVal)/2) + minVal;
canvas.width = middle * xRatio;
canvas.height = middle * yRatio;
if (isGlContextFillingTheCanvas(gl)) {
return binarySearch(middle + 1, maxVal);
} else {
return binarySearch(minVal, middle);
}
}
ratioMultiple = binarySearch(1, ratioMultiple);
return [xRatio * ratioMultiple, yRatio * ratioMultiple];
}
Also in a jsfiddle https://jsfiddle.net/1sh47wfk/1/
The limitations for Safari (all platforms) are much lower.
Known iOS/Safari Limitations
For example, I had a 6400x6400px canvas buffer with data drawn onto it. By tracing/ exporting the content and by testing on other browsers, I was able to see that everything was fine. But on Safari, it would skip the drawing of this specific buffer onto my main context.
I tried to programmatically figure out the limit: setting canvas size starting from 35000, stepping down by 100 until valid size is found. In every step writing the right-bottom pixel and then reading it. It works - with caution.
The speed is acceptable if either width or height is set to some low value (eg. 10-200) this way: get_max_canvas_size('height', 20).
But if called without width or height like get_max_canvas_size(), the created canvas is so big that reading SINGLE pixel color is very slow, and in IE causes serious hang.
If this like test could be done someway without reading pixel value, the speed would be acceptable.
Of course the easiest way to detect maximum size would be some native way to query the max width and height. But Canvas is 'a living standard', so may be it is coming some day.
http://jsfiddle.net/timo2012/tcg6363r/2/ (Be aware! Your browser may hang!)
if (!Date.now)
{
Date.now = function now()
{
return new Date().getTime();
};
}
var t0 = Date.now();
//var size = get_max_canvas_size('width', 200);
var size = get_max_canvas_size('height', 20);
//var size = get_max_canvas_size();
var t1 = Date.now();
var c = size.canvas;
delete size.canvas;
$('body').append('time: ' + (t1 - t0) + '<br>max size:' + JSON.stringify(size) + '<br>');
//$('body').append(c);
function get_max_canvas_size(h_or_w, _size)
{
var c = document.createElement('canvas');
if (h_or_w == 'height') h = _size;
else if (h_or_w == 'width') w = _size;
else if (h_or_w && h_or_w !== 'width' && h_or_w !== 'height' || !window.CanvasRenderingContext2D)
return {
width: null,
height: null
};
var w, h;
var size = 35000;
var cnt = 0;
if (h_or_w == 'height') w = size;
else if (h_or_w == 'width') h = size;
else
{
w = size;
h = size;
}
if (!valid(w, h))
for (; size > 10; size -= 100)
{
cnt++;
if (h_or_w == 'height') w = size;
else if (h_or_w == 'width') h = size;
else
{
w = size;
h = size;
}
if (valid(w, h)) break;
}
return {
width: w,
height: h,
iterations: cnt,
canvas: c
};
function valid(w, h)
{
var t0 = Date.now();
var color, p, ctx;
c.width = w;
c.height = h;
if (c && c.getContext)
ctx = c.getContext("2d");
if (ctx)
{
ctx.fillStyle = "#ff0000";
try
{
ctx.fillRect(w - 1, h - 1, 1, 1);
p = ctx.getImageData(w - 1, h - 1, 1, 1).data;
}
catch (err)
{
console.log('err');
}
if (p)
color = p[0] + '' + p[1] + '' + p[2];
}
var t1 = Date.now();
if (color == '25500')
{
console.log(w, h, true, t1 - t0);
return true;
}
console.log(w, h, false, t1 - t0);
return false;
}
}
You could chunk it and in javascript auto add as many smaller canvases as needed and draw the elements on the appropriate canvas. You may still run out of memory eventually but would get you by the single canvas limit.
I don't know how to detect the max possible size without itteration, but you can detect if a given canvas size works by filling a pixel and then reading the colour back out. If the canvas has not rendered then the color you get back will not match. W
partial code:
function rgbToHex(r, g, b) {
if (r > 255 || g > 255 || b > 255)
throw "Invalid color component";
return ((r << 16) | (g << 8) | b).toString(16);
}
var test_colour = '8ed6ff';
working_context.fillStyle = '#' + test_colour;
working_context.fillRect(0,0,1,1);
var colour_data = working_context.getImageData(0, 0, 1, 1).data;
var colour_hex = ("000000" + rgbToHex(colour_data[0], colour_data[1], colour_data[2])).slice(-6);

Categories

Resources