Edit: I've reported this as a Chromium bug: https://bugs.chromium.org/p/chromium/issues/detail?id=668257
I'm creating a little canvas game in JS with enemies that can shoot. For testing, I created a flag, declared globally as let fancy = true;, to determine whether or not to use a "fancy" targeting algorithm. I made it so that pressing P will toggle this flag. My main function, frame, calls another function, autoShoot, five times per second. autoShoot uses the fancy flag.
Today, something strange started happening; I don't remember what change introduced it. Sometimes, when I press P, autoShoot acts like fancy didn't get toggled. I did some debugging and discovered that the new, toggled value is reflected inside frame, but in autoShoot, the value isn't updated. It happens intermittently, and sometimes the value in autoShoot will fix itself (without me having done anything).
I've reduced the code to the following, which still exhibits the problem for me. Try pressing P a bunch of times. For me, the two values get "out of sync" and display differently after pressing P just once or twice:
(I'm running Chrome "Version 54.0.2840.99 m" on Windows 10.)
const canvas = document.getElementById("c");
const width = 0;
const height = 0;
const ctx = canvas.getContext("2d");
const ratio =1;// (window.devicePixelyRatio||1)/(ctxFOOOOOOOOFOOOOOOOOOFOOOOO||1);
canvas.width = width*ratio;
canvas.height = height*ratio;
canvas.style.width = width+"px";
canvas.style.height = height+"px";
ctx.scale(ratio, ratio);
function testSet(id, val) {
console.log(id+": "+val);
document.getElementById(id).innerText = val;
}
let fancy = true;
document.body.addEventListener("keydown", function(e) {
if (e.keyCode == 80) {
fancy = !fancy;
console.log("Set fancy to: "+fancy);
}
});
let bullets = Array(2000);
let lastTime = 0, shotTimer = 0;
function frame(time) {
const dt = (time - lastTime)/1000;
lastTime = time;
if ((shotTimer -= dt) <= 0) {
testSet("frame", fancy);
autoShoot();
shotTimer = 0.2;
}
for (let b of bullets) {}
requestAnimationFrame(frame);
}
function autoShoot() {
testSet("autoShoot", fancy);
}
requestAnimationFrame(frame);
<code>
fancy (frame) = <span id="frame"></span><br>
fancy (autoShoot) = <span id="autoShoot"></span>
</code>
<canvas id="c"></canvas>
Playing around, here are some observations:
removing any of the following causes the issue to go away:
any line in the code at the top dealing with the canvas, even just the comment after const ratio
the empty for...of loop: for (let b of bullets) {}
changing let fancy = to var fancy = or just fancy =
putting the whole thing out of the global scope (by using IIFE, onload handler, or block scope)
Increasing the size of the bullets array increases the frequency that the issue occurs. I think it's because it makes frame take longer to execute; originally, bullets.length was only 20, but each loop iteration did some stuff to update the bullet, etc.
Does this happen on your computers? Is there any logical explanation for this? I've tried restarting my browser, no change.
As everybody commented out it seems to be a Chrome issue.
I've tried to reproduce the same issue on Chrome version 45.0.2454.85 m (64-bit) and 44.0.2403.107 m (32-bit) (enabling strict mode of course) but I've not succeded. But on version 54.0.2840.99 m (64-bit) it is there.
And I noticed that changing requestAnimationFrame to something like setInterval also makes the problem totally go away.
So, I assume that this strange behaviour has something to do with Chrome's requestAnimationFrame on newer versions, and maybe block scoping nature of let, and function hoisting.
I can't say from which version of Chrome we can see this kind of "bug", but I can assume that it can be the version 52, because in this version many changes occured, like new method of Garbage collection, native support for es6 and es7 etc. For more information you can watch this video from I/O 2016.
Maybe, new Garbage collection method is causing this issue, because as they told in the above mentioned video it is connected with browser frames, something like v8 does GC when the browser is idle, in order not to touch the drawing of frames etc. And as we know that requestAnimationFrame is a method which calls a callback on the next frame drawing, maybe in this process these weird thing's happening. But this is just an assumption and I've no competence to say something serious about this:)
I'm on Chrome 54.0.2840.98 on Mac and it happens too. I think it's a scoping issue, because if I wrap the declaration following the let statement into a {…} block, then the snippet works fine and both values change immediately after key press.
const canvas = document.getElementById("c");
const width = 0;
const height = 0;
const ctx = canvas.getContext("2d");
const ratio =1;// (window.devicePixelyRatio||1)/(ctxFOOOOOOOOFOOOOOOOOOFOOOOO||1);
canvas.width = width*ratio;
canvas.height = height*ratio;
canvas.style.width = width+"px";
canvas.style.height = height+"px";
ctx.scale(ratio, ratio);
function testSet(id, val) {
console.log(id+": "+val);
document.getElementById(id).innerText = val;
}
let fancy = true;
{
document.body.addEventListener("keydown", function(e) {
if (e.keyCode == 80) {
fancy = !fancy;
console.log("Set fancy to: "+fancy);
}
});
let bullets = Array(2000);
let lastTime = 0, shotTimer = 0;
function frame(time) {
const dt = (time - lastTime)/1000;
lastTime = time;
if ((shotTimer -= dt) <= 0) {
testSet("frame", fancy);
autoShoot();
shotTimer = 0.2;
}
for (let b of bullets) {}
requestAnimationFrame(frame);
}
function autoShoot() {
testSet("autoShoot", fancy);
}
requestAnimationFrame(frame);
}
<code>
fancy (frame) = <span id="frame"></span><br>
fancy (autoShoot) = <span id="autoShoot"></span>
</code>
<canvas id="c"></canvas>
Related
This question already has an answer here:
HTML5 Canvas performance very poor using rect()
(1 answer)
Closed 2 years ago.
I start the code, watch in dev window, get no errors. The image moves very quickly at first but, after a few seconds, it comes to a craw.
I checked on here but I can't figure it out. I'm a rookie so that could be the problem.
I've tried breaking it out into basic functional steps rather than any class, put "===" and "==" back and forth (cause I do not get the real difference between them), and changed from a "setInterval" to a "setTimeout" just in case I was calling the interval too soon.
I am very much a noob to Javascript and this is my first real work with canvas.
The HTML code simply adds the script with nothing else. The window load at the end of the script runs "startgame".
Thanks for anything you can help me with.
var winX=0;
var winY=0;
var scaleX=0;
var scaleY=0;
var bkcolor="#777777";
var ctx;
var objs=[];
var wallimg = new Image();
wallimg.src = 'wall.png';
var willy=new Image();
willy.src='willy.gif';
var player;
var gameActive=0;
var keyboard=[];
function startGame()
{
var i;
setWindow();
theBoard.start();
gameActive=1;
someting=new Obj(0,10,600,20,"PATTERN",wallimg);
someting.setimage(wallimg);
Obj.Wall(40,100,100,16,wallimg);
Obj.Wall(0,420,620,16,wallimg);
Obj.Wall(0,0,16,440,wallimg);Obj.Wall(584,0,16,440,wallimg);
player=new Obj(24,400,16,16,"PLAYER",willy);
player.setimage(willy);
player.gravity=1;
}
function setWindow()
{
winX = window.innerWidth|| document.documentElement.clientWidth|| document.body.clientWidth;
winY = window.innerHeight|| document.documentElement.clientHeight|| document.body.clientHeight;
winX=winX-4;
winY=winY-4;
scaleX=640/winX;
scaleY=480/winY;
if (gameActive==1) {
theBoard.canvas.width = 600/scaleX;
theBoard.canvas.height = 440/scaleY;
theBoard.canvas.style.left=""+20/scaleX+"px";
theBoard.canvas.style.top=""+20/scaleY+"px";
}
}
function setBackdrop(img)
{
var str="<img src='"+img+"' onclick='showCoords(event);' style='";
str=str+"width:"+winX+"px;height:"+winY+"px;'>";
document.getElementById('page').innerHTML=str;
document.getElementById('page').innerHTML=str;
currimage=img;
}
var theBoard = {
canvas : theCanvas=document.createElement("canvas"),
start : function() {
this.canvas.width = 600/scaleX;
this.canvas.height = 440/scaleY;
this.canvas.style.left=""+20/scaleX+"px";
this.canvas.style.top=""+20/scaleY+"px";
this.canvas.style.position="absolute";
this.canvas.tabIndex=1;
this.context = this.canvas.getContext("2d");
ctx=this.context;
document.body.insertBefore(this.canvas, document.body.childNodes[0]);
this.canvas.style.backgroundColor=bkcolor;
setTimeout(updateGameArea, 40);
window.addEventListener('keydown', function (e) {
e.preventDefault();
keyboard=(keyboard||[]);
keyboard[e.keyCode]=(e.type=="keydown");
})
window.addEventListener('keyup', function (e) {
keyboard[e.keyCode]=(e.type=="keydown");
})
},
stop : function() {
},
restart:function() { this.interval = setTimeout(updateGameArea, 40);},
clear : function() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
function updateGameArea()
{
var i;
theBoard.clear();
if (keyboard && keyboard[37])
{
player.speed-=2; if (player.speed<-8) player.speed=-8;
}
else if (player.speed<0)
{
player.speed+=1;
}
if (keyboard && keyboard[39])
{
player.speed+=2; if (player.speed>8) player.speed=8;
}
else if (player.speed>0)
{
player.speed-=1;
}
if (player.gravity<1) player.gravity++;
if (keyboard && keyboard[38] && player.gravity>-1 && player.canjump==1){
player.gravity=-16;
player.dir=-6;
player.canjump=0;
}
if (player.gravity<4) {player.gravity=player.gravity+player.dir; player.dir+=4;if (player.dir>16) player.dir=16;}
if (player.gravity!=0)
{
player.y+=player.gravity;
if (checkWalls(player)==true)
{ player.y-=player.gravity;
if (player.gravity>0) player.canjump=1;
}
}
if (player.speed!=0)
{
player.x+=player.speed;
if (checkWalls(player)===true)
player.x-=player.speed;
}
for (i=0;i<objs.length;i++)
objs[i].draw();
setTimeout(updateGameArea, 10);
}
function checkWalls(obj)
{
var i;
for (i=0;i<objs.length;i++)
{
if (objs[i].type=="WALL")
if (obj.collision(objs[i])) {return true;}
}
return false;
}
class Obj {
constructor (x,y,w,h,t,img="") {
this.width=w;
this.height=h;
this.x=x;
this.y=y;
this.type=t;
this.imagemap=img;
this.speed=0;
this.gravity=0;
this.dir=0;
this.canjump=1;
this.pattern=0;
objs[objs.length]=this;
}
static Wall(x,y,w,h,img) {
var id=new Obj(x,y,w,h,"WALL",img);
return id;
}
draw()
{
if ((this.x/scaleX)<0 || (this.x/scaleX)>theBoard.canvas.width ||
(this.y/scaleY)<0 || (this.y/scaleY)>theBoard.canvas.height)
return;
switch (this.type){
case 'PATTERN':
case 'WALL':
{
if (this.pattern===0)
{ this.pattern=ctx.createPattern(this.imagemap,"repeat");}
ctx.rect(this.x/scaleX,this.y/scaleY,this.width/scaleX,this.height/scaleY);
ctx.fillStyle=this.pattern;
ctx.fill();
break;
}
case 'PLAYER':
ctx.drawImage(this.imagemap,0,0,this.width,this.height,this.x/scaleX,this.y/scaleY,this.width/scaleX,this.height/scaleY);
break;
}
}
setimage(img)
{
this.imagemap=img;
}
collision(wth) {
if (((this.x+this.width)>wth.x) && (this.x<(wth.x+wth.width))
&& ((this.y+this.height)>wth.y) && (this.y<(wth.y+wth.height)))
{return true;}
else return false;
}
}
window.onload=startGame();
As pointed out by #Kaiido, solution to your problem is here: HTML5 Canvas performance very poor using rect().
In short, just put your main loop code between beginPath and closePath without changing your theBoard.clear() method.
function updateGameArea()
{
var i;
theBoard.clear();
theBoard.context.beginPath();
...
theBoard.context.closePath();
requestAnimationFrame(updateGameArea);
}
Answer I originally wrote:
Resetting the dimensions to clear the canvas works better in your case, but it would induce performance issues.
clear : function() {
this.context.canvas.width = 600 / scaleX;
this.context.canvas.height = 440 / scaleY;
}
Also, use requestAnimationFrame as it eliminates any flicker that can happen when using setTimeout.
requestAnimationFrame(updateGameArea);
The following is a guess. I think you're running out of cycles and your frames are piling up on top of each other. At a glance, I don't see anything in your code that would cause a memory leak. Unless you look at the console memory graph and find out that you do, because you're adding listeners over and over or something like that. But simply clearing a canvas does not slow things down. It's basically the same as setting a bunch of values in an array.
However: Running heavy canvas operations within a setTimeout() can have a big toll on your CPU, if the CPU can't finish one operation before the next one enters the queue. Remember that timeouts are asynchronous. If your CPU throttles down and if the refresh rate you are specifying (40 milliseconds) is too short, then you will be left with a whole stack of redraws and clears that are waiting to go right after the last one, without giving the CPU any time to breathe.
Most Canvas animation packages have ways of dealing with this, by not just setting a timeout but waiting to make sure the last redraw is finished before triggering the next one in the call stack, and dropping a frame if necessary. At a bare minimum, you want to set a global variable like _redrawing=true before you do your redraw, and then set it to false when the redraw is finished, and ignore any call to setTimeout while it's still true. That will let you count how many frames you might be dropping. If you see this number going up over time, your CPU may be throttling as well. But do also check the memory graph and see if anything else is leaking.
Edit as correctly noted by #N3R4ZZuRR0 using requestAnimationFrame() will also avoid the timer problem. But you then need to measure the time between animation frames to figure out where things should actually be at that point in time. My suggestion of dropping frames here and there is primitive and most packages use requestAnimationFrame(), but it would help you identify whether your problem is with some other part of your code or with your frames building up in the timer.
This question already has answers here:
get a smooth animation for a canvas game
(3 answers)
Closed 6 years ago.
So I was creating a game on the canvas in HTML and Javascript. I wanted to make some kind of flappy bird-ish game but when I press a key, the animation of the player looks really stuttering. Take a look:
body {
overflow: hidden;
}
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="Style.css"/>
</head>
<body onload="startgame()">
<canvas id="canvas"></canvas>
<script>
canvas.height=window.innerHeight;
canvas.width=window.innerWidth;
function startgame() {
var c = document.getElementById("canvas");
var ctx = c.getContext("2d");
var x = 900;
var y = 300;
var w = 25;
var h = 500;
var yperson = 20;
var xperson = 200;
document.addEventListener("keydown", function() {
yperson -= 150;
});
function updateperson() {
yperson = yperson;
}
setInterval(createobject, 10);
function createobject() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
x -= 1;
yperson += 0.5;
yperson *= 1.003;
ctx.fillStyle = "#009999";
ctx.fillRect(x,y,w,h);
ctx.fillStyle = "black";
ctx.fillRect(xperson,yperson,30,30);
if (x <= 50) {
if (yperson < 280 && xperson === x-30) {
x -= 1;
} else if (yperson > 280){
x += 1;
}
}
}
}
</script>
</body>
</html>
I want it to have a smooth animation up. I have seen some people saying it should be done with requestanimationframe but I don't know how to use it.
Thanks in advance.
requestAnimationFrame
For full details see MDN window.requestAnimationFrame
As the previous answer is lacking some information, here is annotated example of the basic usage.
// A flag to indicate that the animation is over
var stop = false; // when true the animation will stop
// define main loop update
// the callback that is the main loop
// the browser treats this function as special in terms of display items including
// the canvas, and all DOM items.
// It will ensure that any changes you make to the page are synced to the display
function update(time){ // time is the time since load in millisecond 1/1000th
// time is high precision and gives the time down to
// microseconds (1/1,000,000) as fraction 0.001 is one microsecond
// you can stop the animation by simply not calling the request
// so if the flag stop is true stop the animation
if(!stop){
requestAnimationFrame(update); // request the next frame
}
}
requestAnimationFrame(update); // request the very first frame
// or you can start it with a direct call. But you will have to add the time
update(0);
The update function will be called up to 60 times a second. If the code can not keep up (ie it take more than 1/60th of a second to render) then the update function will wait for the next frame effectively reducing the frame rate to 1/30. It will continue skipping frames if the render is slow.
Because you can not control the frame rate you can do the following to slow the animation down to a required frame rate.
const FRAMES_PER_SECOND = 30; // Valid values are 60,30,20,15,10
// set the mim time to render the next frame
const FRAME_MIN_TIME = (1000/60) * (60 / FRAMES_PER_SECOND) - (1000/60) * 0.5;
var lastFrameTime = 0; // the last frame time
function update(time){
if(time-lastFrameTime < FRAME_MIN_TIME){ //skip the frame if the call is to early
requestAnimationFrame(update);
return; // return as there is nothing to do
}
lastFrameTime = time; // remember the time of the rendered frame
// render the frame
requestAnimationFrame(update);
}
If you change the focus to a another tab the browser will no longer call the request until focus is returned to the tab.
Like other timer events the call requestAnimationFrame returns a ID that can be used to cancel the callback event
var id = requestAnimationFrame(update);
// to cancel
cancelAnimationFrame(id);
You can actually call requestAnimationFrame more than once per frame. As long as all the requests can render within the 1/60th of a second they all will by synced and presented to the display at the same time. But you must be careful because they can come out of sync if the rendering time is too long.
RequestAnimationFrame prevents flickering (displaying the canvas when the rendering is not complete) by double buffering changes. Syncs to the display hardware and prevents shearing (caused when the display is updated midway through a frame and the top half of the display shows the old frame and bottom the new frame). There are more benefits depending on the browser.
This is how I set up my games:
// DEFINE OBJECTS UP HERE
var update = function(modifier) {
// update all the object properties
// multiply values that depend on time (like speeds) by modifier
};
var render = function() {
// draw everything
};
var main = function() {
var now = Date.now();
var change = now - then;
update(change/1000); // update based on frame rate, change in milliseconds/second
render();
then = now;
requestAnimationFrame(main);
};
// ADD EVENT LISTENERS HERE
requestAnimationFrame = window.requestAnimationFrame
|| window.webkitRequestAnimationFrame
|| window.msRequestAnimationFrame
|| window.mozRequestAnimationFrame;
// ABOVE CODE GIVES CROSS-BROWSER COMPATIBILITY
var then = Date.now();
main();
requestAnimationFrame tells the browser to execute the loop based on the frame rate. Personally, I don't understand how it works, although if someone here did I'd be very interested in knowing more. setInterval allows you to set how quickly you want the loop to run, but the optimal rate will depend on the browser.
The "then" and "now" variables are for determining how long has passed since the last execution of the loop. This value can be passed to the update function and used for calculations that depend on the frame rate, although sometimes you don't need it and can just use:
var update = function() {
//STUFF
};
// if using that variation just ignore then and now and call:
update();
//in your main
Although using then and now is better.
I have written code that takes two arrays, both of which contain co-ordinates for a four-cornered shape (effectively a start frame and an end frame), a canvas ID and a time value. The function then calculates dX and dY of each corner and uses window.performance.now() to create a timestamp. Then, on every requestAnimationFrame(), it calculates what the co-ordinates should be by using dX, dY, the old timestamp, a new timestamp and the time value from the function call. It looks like this:
function doAnim(cv, startFrame, endFrame, animTime)
{
this.canvas = document.getElementById(cv);
this.ctx = this.canvas.getContext('2d');
if(startFrame.length != endFrame.length)
{
return('Error: Keyframe arrays do not match in length');
};
this.animChange = new Array();
for(i=1;i<=startFrame.length;i++)
{
var a = startFrame[i];
var b = endFrame[i]
var c = b - a;
this.animChange[i] = c;
}
this.timerStart = window.performance.now();
function draw()
{
this.requestAnimationFrame(draw, cv);
this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height);
this.currentFrame = new Array();
for(i=1;i<=startFrame.length;i++)
{
this.currentFrame[i] = startFrame[i]+(this.animChange[i]*((window.performance.now()-this.timerStart)/animTime));
}
if((window.performance.now()-this.timerStart)>=animTime)
{
this.ctx.beginPath()
this.ctx.moveTo(endFrame[1], endFrame[2]);
this.ctx.lineTo(endFrame[3], endFrame[4]);
this.ctx.lineTo(endFrame[5], endFrame[6]);
this.ctx.lineTo(endFrame[7], endFrame[8]);
this.ctx.fill();
return;
}
else
{
this.ctx.beginPath()
this.ctx.moveTo(this.currentFrame[1], this.currentFrame[2]);
this.ctx.lineTo(this.currentFrame[3], this.currentFrame[4]);
this.ctx.lineTo(this.currentFrame[5], this.currentFrame[6]);
this.ctx.lineTo(this.currentFrame[7], this.currentFrame[8]);
this.ctx.fill();
}
}
draw();
}
The goal is to have multiple animations of objects happening at once. I took the whole co-ordinate approach because I want the objects to appear as if they are coming from the horizon, creating a fake 3D perspective effect (all objects' starting frames would be a single point at the center of the canvas), and I do not want to warp the objects' textures.
Well, it works great for a single animation, but if I try to start a new animation on a completely different canvas while the first one is running, then the first animation stops dead in its tracks.
As you can see from my JS, I've tried getting around this with gratuitous use of this (I do not fully understand how this works yet, and every explanation I've read has left me even more confused), but it has not worked. I also tried a horrific approach which stored all the functions' own variables in one global array (the first time the function runs, all the variables are put in entries 1-30, the second time they're put in 31-60, etc). Unsurprisingly, that did not work either.
Here is a JSFiddle so you can see this scenario for yourself and play with my code. I am officially out of ideas. Any help will be greatly appreciated.
Like markE linked too, trying to call requestAnimationFrame multiple times won't work.
Instead you make multiple objects and then call some sort of function on them each frame.
I have created an example using your code:
https://jsfiddle.net/samcarlin/2bxn1r79/7/
var anim0frame1 = new Array();
anim0frame1[1] = 0;
anim0frame1[2] = 0;
anim0frame1[3] = 50;
anim0frame1[4] = 0;
anim0frame1[5] = 50;
anim0frame1[6] = 150;
anim0frame1[7] = 0;
anim0frame1[8] = 150;
var anim0frame2 = new Array();
anim0frame2[1] = 200;
anim0frame2[2] = 200;
anim0frame2[3] = 300;
anim0frame2[4] = 250;
anim0frame2[5] = 300;
anim0frame2[6] = 300;
anim0frame2[7] = 200;
anim0frame2[8] = 250;
//Call global
animations = [];
requestAnimationFrame( GlobalStep );
function GlobalStep(delta){
//Functions called by request animation frame have the new time as an argument
//so delta should be approximately the same as window.performance.now()
//especially in realtime applications, which this is
//Check if we have any animation objects
if(animations.length > 0){
//Iterate through and call draw on all animations
for(var i=0; i<animations.length; i++){
if(animations[i].draw(delta)){
//Basically we have it so if the draw function returns true we stop animating the object
//And remove it from the array, so have the draw function return true when animation is complete
animations[i].splice(i, 0);
//We removed an object from the array, so we decrement i
i--;
}
}
}
//And of course call requestAnimationFrame
requestAnimationFrame( GlobalStep );
}
function AnimationObject(cv, startFrame, endFrame, animTime){
//Add this object to the objects arrays
animations.push(this);
//We need to store start and end frame
this.startFrame = startFrame;
this.endFrame = endFrame;
this.animTime = animTime;
//Your code
this.canvas = document.getElementById(cv);
this.ctx = this.canvas.getContext('2d');
if (startFrame.length != endFrame.length) {
return ('Error: Keyframe arrays do not match in length');
};
this.animChange = new Array();
for (i = 1; i <= startFrame.length; i++) {
var a = startFrame[i];
var b = endFrame[i]
var c = b - a;
this.animChange[i] = c;
}
this.timerStart = window.performance.now();
}
//This adds a function to an object, but in such a way that every object shares the same function
//Imagine a kitchen, each object is a person, and this function is a spoon
//by defining this function in this manner Object.prototype.function_name = function(arguments){}
//We make it so one function definition is needed, essentially allowing all the people to share one spoon,
//the 'this' variable still refers to whichever object we call this method, and we save memory etc.
AnimationObject.prototype.draw = function(newTime){
//I added this to start frame so we get what we stored earlier
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.currentFrame = new Array();
for (i = 1; i <= this.startFrame.length; i++) {
this.currentFrame[i] = this.startFrame[i] + (this.animChange[i] * ((newTime - this.timerStart) / this.animTime));
}
if ((newTime - this.timerStart) >= this.animTime) {
this.ctx.beginPath()
this.ctx.moveTo(this.endFrame[1], this.endFrame[2]);
this.ctx.lineTo(this.endFrame[3], this.endFrame[4]);
this.ctx.lineTo(this.endFrame[5], this.endFrame[6]);
this.ctx.lineTo(this.endFrame[7], this.endFrame[8]);
this.ctx.fill();
return;
} else {
this.ctx.beginPath()
this.ctx.moveTo(this.currentFrame[1], this.currentFrame[2]);
this.ctx.lineTo(this.currentFrame[3], this.currentFrame[4]);
this.ctx.lineTo(this.currentFrame[5], this.currentFrame[6]);
this.ctx.lineTo(this.currentFrame[7], this.currentFrame[8]);
this.ctx.fill();
}
}
Notes:
Everytime you press the button a new object is added and simply overwrites previous ones for each frame, you should implement your program so that it checks if a specific animation has already started, you could also use the builtin mechanism to stop animation when complete (read the comments in the code)
You also need to change the on button click code
<button onclick="new AnimationObject('canvas1', anim0frame1, anim0frame2, 3000);">
Lastly if you have further questions feel free to contact me
this problem was fixed in the new chrome version(Version 35.0.1916.114)
In chrome for mac osx, CanvasRenderingContext2D#getImageData function will make memory leaks, how can I avoid this problem, here is the test case and result, it's only happened in chrome browser, safari is OK
<!DOCTYPE html>
<html>
<head>
<title>CanvasRenderingContext2D#getImageData bug in chrome</title>
<script type="text/javascript">
var g;
function init(){
g = document.getElementById('canvas').getContext('2d');
g.fillStyle = "blue";
g.fillRect(10, 10, 100, 100);
g.fillStyle = "green";
g.fillRect(60, 60, 100, 100);
}
function getImageData(){
var i = 0;
while(i++ < 100){
var c = g.getImageData(0,0,1000, 1000);
delete c;
}
}
function toDataURL(){
var i = 0;
while(i++ < 100){
var c = g.canvas.toDataURL();
delete c;
}
}
</script>
</head>
<body onload="init()">
<button onclick="getImageData()">call getImageData 100 times - then memory will grow, can't GC</button>
<button onclick="toDataURL()">call toDataURL 100 times - it is OK</button><br>
<canvas id='canvas' width='600px' height='500px'/>
</body>
</html>
Your problem is not with the getImageData function. It's the way the variable that holds the getImageData is assigned that create the leaks.
The problem is that delete c will fail (delete doesn't affect variable names) and the browser silently returns false.
MDN delete reference
Try with c = null instead. Try to declare the c variable outside the for loop, to avoid recreate the variable in each step of the loop.
Here is the modified code:
function getImageData(){
var i = 0;
var c;
while(i++ < 100){
c = g.getImageData(0,0,1000, 1000);
// c = null; // <= check UPDATE to see why this doesn't work as expected
}
}
function toDataURL(){
var i = 0;
var c;
while(i++ < 100){
c = g.canvas.toDataURL();
// c = null; // <= check UPDATE to see why this doesn't work as expected
}
}
I tried the code exactly in the same browser and using the memory profile in developer tools I could see the memory being perfectly cleared by garbage collector.
Check the memory timeline in developer tools (Ctrl+Shift+i).
To enable the memory profile you need to start Chrome with the flag --enable-memory-info.
UPDATE:
As I’ve already said in comments, garbage collection works by reclaiming blocks of memory (objects) which are no longer reachable.
When a function returns, the object which c points to is automatically available for garbage collection, because there is nothing left that has a reference to it.
There are also misconceptions about how null works. Setting an object reference to null doesn’t “null” the object. It sets the object reference to null.
So, in this case, the memory allocated to store each getImageData information remains there until the function returns. Since the image data is a very large object, and it's larger as larger are the canvas dimensions, in huge loops (let's say 500 loops or above, that depends on the machine) will cause overflow in memory before the function returns and the garbage collector be triggered.
I recommend the following article: Writing Fast, Memory-Efficient JavaScript. It's well explained and easy to read.
SOLUTION !!!
Now we know that the garbage collector is triggered only after a function return, one solution that came into my mind is defer the function that call the getImageData by a fraction of millisecond. That way we guarantee that the function returns after each getImageData call.
I tried the code below and it work even for 10000 iterations! Spends a lot of time to finish, but it finishes and with no memory leaks!)
Try it by yourself:
<!DOCTYPE html>
<html>
<head>
<title>CanvasRenderingContext2D#getImageData bug fixed</title>
<script type="text/javascript">
var g;
function init(){
g = document.getElementById('canvas').getContext('2d');
g.fillStyle = "blue";
g.fillRect(10, 10, 100, 100);
g.fillStyle = "green";
g.fillRect(60, 60, 100, 100);
}
function getImageData(){
var c = g.getImageData(0,0,1000, 1000);
}
var total = 0;
var iterations = 100;
function test(){
var i = 0;
while(i++ < iterations){
setTimeout(function(){
getImageData();
total++;
//console.log(total);
if(total == iterations){
alert("" + total+" getImageData functions were completed!!!")
}
}, 0.01); // defer
}
alert("" + (i-1) + " iterations completed. Wait for the return of all getImageData");
}
</script>
</head>
<body onload="init()">
<button onclick="test()">call getImageData several times</button><br>
<canvas id='canvas' width='600px' height='500px'/>
</body>
</html>
I have found an API that'll make working with CANVAS a lot easier. It allows selection and modification of individual elements on the canvas very easily. It's EaselJS. The API doc is here. http://easeljs.com/docs/
I am Ok with the API so far. What confuses me is actually some javascript in there. The part that's in bold or within * *(couldn't get the formatting to work)
What kind of javascript structure is this?
(function(target){...content...})(bitmap)
and in the content, it references bitmap, which is something outside.
HERE IS THE CODE
for(var i = 0; i < 100; i++){
bitmap = new Bitmap(image);
container.addChild(bitmap);
bitmap.x = canvas.width * Math.random()|0;
bitmap.y = canvas.height * Math.random()|0;
bitmap.rotation = 360 * Math.random()|0;
bitmap.regX = bitmap.image.width/2|0;
bitmap.regY = bitmap.image.height/2|0;
bitmap.scaleX = bitmap.scaleY = bitmap.scale = Math.random()*0.4+0.6;
bitmap.mouseEnabled = true;
bitmap.name = "bmp_"+i;
(function(target) {
*bitmap.onPress = function(evt) *
{if (window.console && console.log) { console.log("press!"); }
target.scaleX = target.scaleY = target.scale*1.2;
container.addChild(target);
// update the stage while we drag this target
//Ticker provides a central heartbeat for stage to listen to
//At each beat, stage.tick is called and the display list re-rendered
Ticker.addListener(stage);
var offset = {x:target.x-evt.stageX, y:target.y-evt.stageY};
evt.onMouseMove = function(ev) {
target.x = ev.stageX+offset.x;
target.y = ev.stageY+offset.y;
if (window.console && console.log) { console.log("move!"); }
}
evt.onMouseUp = function() {
target.scaleX = target.scaleY = target.scale;
// update the stage one last time to render the scale change, then stop updating:
stage.update();
Ticker.removeListener(stage);
if (window.console && console.log) { console.log("up!"); }
}
** }})(bitmap); **
bitmap.onClick = function() { if (window.console && console.log) { console.log("click!"); } }
}
(function(target){...content...})(bitmap)
is creating a lexical scope for content so that any var or function declarations in content do not leak into the global scope. Inside content, target is just another name for
bitmap.
You can think of this as similar to
function init(target) { ...content... }
and then an immediate call to it passing bitmap as the actual value of the target parameter
but the first version interferes with the global scope even less -- it doesn't define init as a name in the global scope.
EDIT:
I think the purpose is not lexical scoping, but to make sure that the event handlers point to the right bitmap, instead of the last bitmap the for loop deals with.
init(bitmap);
See Event handlers inside a Javascript loop - need a closure? for more detail.