I'm working on a looping animation using JavaScript and HTML5 <canvas>. The animation consists of using canvas' lineTo() method to connect between 10 and 200 coordinates to create an animated shape. The animation is looping and has a duration of 10-20 seconds. Using requestAnimationFrame() I'm looking at around 60fps, each iteration of which the position of each coordinate is updated.
What is the most efficient way of approaching this type of animation?
Approach 1 - on-the-fly calculations
Currently I'm just calculating and updating the position of each point in each iteration of requestAnimationFrame(). The performance is OK, but on older devices I've noticed the CPU usage spiking and occasional rendering lags. The calculations are pretty basic geometrical manipulations.
Approach 2 - pre-calculated positions
I have been toying with the idea of pre-calculating each possible position that a pointcould have, and storing that in memory. Then during the requestAnimationFrame() loop, I can simply access the coordinates for each point and update its position. But I'm unsure of the performance implications and whether this would really make an impact.
I plan on testing approach 2 to see if there is a noticeable difference. But I'm curious if there is another approach I should consider? Is canvas even the right tool for the job here, or should I be looking at something like WebGL?
*** Edit: here are the calculation & canvas drawing functions so you have an idea of the computation run on each frame:
Animation.prototype = {
... the rest of the prototype setup ...
// This function runs once per `requestAnmiationFrame()` call:
updateNodes: function() {
let progress = this.getProgressRadians(); // simple time offset
// This loops over between 10-200 node objects, updating their positions
this.nodes.forEach(node => {
let offset =
((Math.sin(progress - node.numberOffset) + 1) / 2) *
this.pixelWaveHeight;
if (this.waveGrows) {
offset =
offset * (node.index / (this.nodes.length - 1)) * this.waveGrows;
}
if (this.waveAngleRadians) {
offset +=
Math.tan(this.waveAngleRadians) * this.spaceBetweenNodes * node.index;
}
let yValue = this.canvas.height - offset - this.pixelBaseHeight;
node.y = yValue;
});
},
// This function runs at the end of each `requestAnimationFrame()` call
draw: function() {
let ctx = this.canvas.ctx;
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
ctx.beginPath();
ctx.moveTo(0, this.canvas.height);
this.nodes.forEach(node => {
ctx.lineTo(node.x, node.y);
});
ctx.lineTo(this.canvas.width, this.canvas.height);
ctx.lineTo(0, this.canvas.height);
ctx.closePath();
ctx.fill();
}
}
Most of those properties are static, e.g. calculated once at the beginning and then just accessed:
pixelWaveHeight - Number
waveGrows - Number
Related
I'm coding a game in the style of agar.io where I want to move the player to the direction relative to the mouse position.
This is how I check the mouse position, returning a Vector object;
let mouse_vector = new Vector();
canvas.onmousemove = function() {
mouse_vector = mouse(event);
}
function mouse(evt) {
mouse_vector = new Vector(evt.clientX - canvas.width/2, evt.clientY - canvas.height/2);
return mouse_vector;
}
The player is an object with an x and y coordinate to which I add the vector pointing towards the mouse. I have 1 canvas that represents the world and is hidden. The other canvas is my viewport, on which I draw the cropped world relative to the player position.
I'm using requestAnimationFrame here, but I tried using Interval as well.
function draw() {
player.x += mouse_vector.x * 0.005;
player.y += mouse_vector.y * 0.005;
canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
canvasCtx.drawImage(game, player.x-canvas.width/2, player.y-canvas.height/2, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
canvasCtx.arc(canvas.width/2, canvas.height/2, player.r, 0, 2*Math.PI);
canvasCtx.fill();
requestAnimationFrame(draw);
}
My issue is that the game starts stuttering over time. I would like it to be as smooth as at the start. I'm suspecting the issue is related to some kind of caching. I've noticed the game uses a lot of CPU power. Any ideas?
The issue I was having was that I was not using the beginPath() method when filling arcs. Seems that not reseting the previous draw builds up and causes performance loss over time.
canvasCtx.beginPath(); // addition
canvasCtx.arc(canvas.width/2, canvas.height/2, player.r, 0, 2*Math.PI);
canvasCtx.fill();
So at the end of the day, you'll be re-drawing the whole map every 16ms to get 60fps which is the target of requestAnimationFrame - so you'll not be escaping the CPU hogging problem, unless you do some optimisation about the drawing.
About the logic you are doing: it doesn't seem from this piece of code that there can be anything that could be building up or leaking memory, so I suggest pasting your code to a code-sharing site like codepen.io and sharing it with the community, we'd have more chance of debugging it.
Edit: Ran the code on a more powerful computer and the grid rendered correctly. Possible hardware limitation? Computer where the problem occurred was a Samsung series 3 Chromebook. I'm thinking it has to do with trying to draw too many lines at the same time. Will test later.
I'm trying to draw a grid onto a canvas using the lineTo() method. The lines draw properly in the beginning, but any line that is drawn completely past 2048 pixels either down or to the right doesn't show up. Line going from inside this point to past it still show up on the other side, just lines that only are only drawn past the point don't show up.
Here's my JavaScript:
function drawGrid() {
//data.tiles is the map stored as an array of arrays
//tilesize = 60
var bw = data.tiles[0].length * tilesize;
var bh = data.tiles.length * tilesize;
ctx.beginPath();
for (i = bw; i >= 0; i -= tilesize) {
ctx.moveTo(i, 0);
ctx.lineTo(i, bh);
}
for (i = bh; i >= 0; i -= tilesize) {
ctx.moveTo(0, i);
ctx.lineTo(bw,i);
}
ctx.strokeStyle = "black";
ctx.stroke();
}
I've checked the data.tiles variable, and it's reading the right number. Really have no idea what I messed up here.
HTML Canvas has a maximum render area depending on your browser & hardware.
once you exceed these limits well your done pretty much.
try pre-rendering or use multiple canvas' positioned with CSS.
If you can see images drawn beyond 2048 then there's no reason a lineTo wouldn't be drawn also.
In the code you calculate bw and bh in different ways. You might check if this is a problem. If not, we'll need to see more code.
// bw uses data.tiles[0]
var bw = data.tiles[0].length * tilesize;
// bh uses data.tiles with no subscript
var bh = data.tiles.length * tilesize;
So I have a basic canvas setting, where sprites are added a little above the canvas and fall down the page, before being removed if their Y position is greater than the height of the canvas. It's not an impressive creation.
It all works fine, but what I'd really like is for each unique sprite to also fade out as it moves down the page. From what I've seen, there's no simple way to go about this.
Modifying the global alpha of the canvas context isn't good enough, because this affects the whole canvas at once (as far as I've seen). I want to affect each sprite individually - so it'll start with an opacity of 255 and gradually decrease to 0 as it also moves down the page.
Altering the image data seems like a pretty hefty task, especially considering the position of the images are always changing (well, vertically, at least) and there can be up to 60 of these on the page at one time.
I know I could (if I really wanted to) create and remove HTML image tags and modify each images opacity via CSS, but this also doesn't seem very practical, again considering I can have up to 60 on the page at any one time.
Is there any way I can achieve this, even if it's one of the aforementioned techniques made a little more efficient?
a) If you are only drawing those objects, you can just set the globalAlpha prior to any draw, like :
function drawSprite(x,y) {
ctx.globalAlpha = 1 - (y/canvasHeight) ;
ctx.drawImage(mySprite, x, y);
}
this way all draws are made with the right alpha.
(you have to define var canvasHeight=canvas.height earlier)
b) if you perform some other operations and you're not sure next operation will set the globalAlpha, just restore it to one after the draw (all other operations are supposed to use an alpha of 1 here ):
function drawSprite(x,y) {
ctx.globalAlpha = 1 - (y/canvasHeight) ;
ctx.drawImage(mySprite, x, y);
ctx.globalAlpha = 1 ;
}
c) another flavor might be to save/restore the globalAlpha by yourself :
function drawSprite(x,y) {
var lastGlobalAlpha = ctx.globalAlpha ;
ctx.globalAlpha = 1 - (y/canvasHeight) ;
ctx.drawImage(mySprite, x, y);
ctx.globalAlpha = lastGlobalAlpha ;
}
this way you're sure drawSprite won't affect current globalAlpha, whatever its value.
d) Lastly you'll have to throw an eye at ctx.save() and ctx.restore() which allow you to perform local changes that won't affect other draws. Since, here, you only change globalAlpha, you'd better use a), b) or best : c), but i'll just write the code for the record :
function drawSprite(x,y) {
ctx.save();
ctx.globalAlpha = 1 - (y/canvasHeight) ;
ctx.drawImage(mySprite, x, y);
ctx.restore();
}
I'm working on a game for a university assignment. The idea is that you defend the centre circle from the incoming asteroids (lines) by drawing a line (click, drag & release to draw a line) which blocks them. An asteroid hitting a line should destroy both the asteroid the line.
The problem I'm currently having is that the collision isn't being detected.
I have arrays of objects of both lines & asteroids. The lines consist of simply start & end x & y, the asteroids consist of a random speed & a random angle (their incoming angle) - the context is rotated, the asteroid drawn, & then it reset for the next line.
To detect collision, I use getImageData & check in front of the asteroids however many pixels the line will progress in that iteration (basically, their speed) & if the colour is red, it will destroy the asteroid - I haven't got round to destroying the line yet, will tackle that hurdle when I come to it (suggestions are welcome though).
function asteroids_draw() {
for (var i = 0; i < asteroids.length; i++) {
// Drawing setup
context.save();
context.translate(width / 2, height / 2);
context.rotate(asteroids[i].angle);
// Detecting close asteroids
if ((asteroids[i].distance - asteroids[i].speed) < planet.size) {
asteroids.splice(i, 1);
game_life_lost();
context.restore();
return;
} else if ((asteroids[i].distance - asteroids[i].speed) < 150){
asteroids[i].colour = '#FF0000';
}
// Scanning ahead for lines
for (var j = 0; j < asteroids[i].speed; j++) {
if (context.getImageData(asteroids[i].distance - j, 0, 1, 1).data[0] == 255) {
asteroids.splice(i, 1);
context.restore();
return;
}
}
// Drawing asteroid
context.beginPath();
context.moveTo(asteroids[i].distance -= asteroids[i].speed, 0);
context.lineTo(trig, 0);
context.strokeStyle = asteroids[i].colour;
context.stroke();
context.closePath();
context.restore();
}
}
The problem is, the asteroids never collide with the lines & I can't for the life of me see why, or see another simple way of doing it. Any advice would be much appreciated, thanks in advance.
I think your problem is that when you rotate the context, previously drawn items(lines) don't get rotated, only objects drawn after the rotation are rotated. See this page for more info.
You could try performing your asteroid/line intersection test before you translate and rotate the canvas, and use cosine and sine to find the x and y coordinates of the pixels you want to get image data from.
var pixelLocation =
[Math.cos(asteroids[i].angle) * j, Math.sin(asteroids[i].angle) * j];
if (context.getImageData(pixelLocation[0], pixelLocation[1], 1, 1).data[0] == 255) {
Just make sure your angle is in radians before passing to cos and sin.
I thought about the problem some more, & realised this method of doing things definitely isn't the best way. It should be doable without a view - a la Model View Controller design pattern. The best way to solve it would be to use maths!
There's simple maths for the intersection of two lines, but this needs intersection of two lines in a range. I found an algorithm which simplifies this further, using eight coordinates - the start x & y & the end x & y of the two lines.
I've posted the results. Thanks for the help.
I do know about the case of float/integer values for drawImage's x and y. But what I need is a smooth animation with an ability to cache my shapes.
Article on caching complex paths with backup canvas
Article on drawImage with float parameters
For example, I want to draw some complex shape (i.e. SVG-tiger, converted to canvas-commands) to canvas just once and then move it smoothly with ctx.translate and ctx.drawImage. I need the float values then, because instead I get a step-by-step moving:
Here's the examples at JSFiddle:
One: Fast speed, with Math.floor applied to translate parameters (x and y are equal to time in seconds multiplied by 10): Animation is weird (sequential, not smooth).
Two: Slow speed, with Math.floor applied to translate parameters (x and y are equal to time in seconds): Animation is weird (sequential, not smooth).
Three: Fast speed, no rounding, float values (x and y are equal to time in seconds multiplied by 10). Speed is fast, so animation looks good.
Four: Slow speed, no rounding, float values (x and y are equal to time in seconds). Speed is slow, so animation looks pulsating. Why?
The last case is the one that confuses me. Am I wrong in my tryings and there is a possibility to make this caching trick work nice?
In Firefox, there is a property of canvas named mozImageSmoothingEnabled (see), but there is no help from that in other browsers. And it also removes paths smoothing.
Code extract:
var shapeCanvas = null;
var w = 320, h = 240;
var startTime = 0;
function start() {
startTime = Date.now();
var docCanvas = document.getElementById('canvas');
. . .
shapeCanvas = document.createElement('canvas');
. . .
drawShape(shapeCanvas.getContext('2d'));
drawNext(docCanvas.getContext('2d'));
}
function drawNext(ctx) {
var msec = (Date.now() - startTime);
var time = msec / 1000; // seconds passed from start
ctx.clearRect(0, 0, w, h);
ctx.save();
// the lines to change: time | (time * 10) | Math.floor(time * 10)
ctx.translate((time < 500) ? Math.floor(time * 10) : 500,
(time < 500) ? Math.floor(time * 10) : 500);
ctx.drawImage(shapeCanvas, 0, 0);
ctx.restore();
__nextFrame(function() {
drawNext(ctx);
});
}
function drawShape(ctx) {
. . .
}
I wrote the tutorial in your first link.
Just to clear the air:
shapeCanvas.style.width = w + 'px';
shapeCanvas.style.height = h + 'px';
is not really worth doing. No point setting the style if its just a in-memory canvas, and you shouldn't really ever want to set the width and height style of a canvas anyway, it just confounds things.
What ellisbben said in the comment is pretty much what's happening.
It's possible to get around it in a few hackish ways I bet. One way might be to make sure its never drawn on an integer pixel. Another might be to use ctx.scale(.99,.99) before drawing anything so it is always anti-aliased. It's tough to get a consistent solution here because different browswer's implementations of anti-aliasing are different.
Here are a few experiments from myself:
http://jsfiddle.net/KYZYT/29/
The first two are the shape drawn from a canvas and also drawn from a PNG
The second two are the same pair but scaled by .99,.99
The last one is the real thing. It still blurs a bit but looks a lot sharper than using the images.
None of my experiments lead to an end of your pulsating, at least not on a microscopic level. I think this is just something you're going to have to live with if you want to animate pixel-perfect images onto half-pixel spaces.
If you really feel you can't just draw on perfect pixels then your (second) best bet for consistency is probably to find a way to force anti-aliasing at all times. Making sure you are always translated to a non-integer or scaling it ever so slightly are decent candidates, but there may be others.
To be honest, you best bet is to not cache these animated paths until you absolutely need the performance from them. Cache the stylized buttons and other static things you make, but if you've got animated paths that need to move slowly and precisely and look very good, you might as well stick with the true thing over my caching optimization unless you really need it for those too.
Bit shameless plug but: I've implement smooth animation in HTML5 slot game with bit hacky way. The generated cached Image is drawn on small canvas once and then I used translate3d() with -moz-transform / -webkit-transform styles for the canvas to move, mirror and scale the image around.
Pregeneration
Create Image
Draw image content
Create canvas object in DOM
Animation phase
Clear canvas
Draw the cached image to the canvas
Use CSS3 transforms (scale3d and translate3d) to move canvas around.