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.
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.
I found a lovely code snippet on canvas spritesheet animations. this is ts:
http://jsfiddle.net/m1erickson/h85Gq/
I tried to beautify this code, by writing an animate function that accepts Image objects, so that I can animate multiple images in my canvas simultaneously. This is my attempt at it:
$(function(){
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var spritePosition=0;
var spriteWidth=100;
var spriteHeight=100;
var spriteCount=40;
var spritePlayCount=0;
var maxSpritePlays=2;
var objectS=new Image();
objectS.src="sprites/first.png";
var fps = 50;
function animate(sprite) {
setTimeout(function() {
if(spritePlayCount<maxSpritePlays){
requestAnimationFrame(animate);
}
// Drawing code goes here
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.drawImage(sprite,spritePosition*spriteWidth, 0,spriteWidth, spriteHeight, 0,0,spriteWidth, spriteHeight);
spritePosition++;
if(spritePosition>spriteCount-1){
spritePosition=0;
spritePlayCount++;
}
}, 1000 / fps);
}
objectS.onload=function(){
animate(objectS);
}
}); // end $(function(){});
I am getting a much observed error, but I cant seem to find the fix for it:
index3.html:59 Uncaught TypeError: Failed to execute 'drawImage' on
'CanvasRenderingContext2D': The provided value is not of type
'(CSSImageValue or HTMLImageElement or HTMLVideoElement or
HTMLCanvasElement or ImageBitmap or OffscreenCanvas)'
Can you help me out finding my bug?
OMDG!
quote OP "Imagine having 50 spritesheets you want to animate."
50!
Looking at the code "accepted answer version"
function animate(sprite) {
// create a timer event to fire in 1/50th second
setTimeout(function() {
if (spritePlayCount < maxSpritePlays) {
// create a animation frame event that may fire at some time
// between 0 and 16ms or 32ms from now
requestAnimationFrame(function() {
animate(sprite);
});
}
// Drawing code etc... Make canvas dirty
// exiting with dirty canvas outside the requestAnimationFrame
// forces DOM to swap canvas backbuffer immediately on exit.
}, 1000 / 50);
}
This is the worst possible way to animate one, let alone more than one sprite.
The timing is out of sync with the display refresh.
Using requestAnimationFrame's callback to create a timed event that renders, completely negates the reason for using requestAnimationFrame. requestAnimationFrame tells the DOM that what you draw in the callback is part of an animation. Using a timer to draw means you don't draw anything in the requested frame making the request redundant.
requestAnimationFrame does its best to get all the callbacks in before the next display refresh (1/60th) but it will delay the callback if there is no time to update the DOM (swap all dirty buffers) before the next refresh. You have no control over the timing of your animation, they may fire anywhere from 20ms to 36ms or more with no consistency over time.
By using timers to draw to the canvas you are forcing a backbuffer swap for each sprite you draw. This is an expensive process and will severely limit the speed that you can draw sprites at, and cause sprites to flicker and shear randomly during animation.
The best practice way.
Use a single animation loop triggered by requestAnimationFrame.
Use an array to store all sprites and update them in the main loop if/as needed.
Only render from within the main loop. Do not render or do anything (at a regular interval) to the DOM outside the main loop's execution.
Don't render inside events like timers, IO, or any other rapidly firing event.
You'll also need to pass the sprite parameter when calling the animate function using requestAnimationFrame.
$(function() {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var spritePosition = 0;
var spriteWidth = 100;
var spriteHeight = 100;
var spriteCount = 40;
var spritePlayCount = 0;
var maxSpritePlays = 2;
var objectS = new Image();
objectS.src = "sprites/first.png";
var fps = 50;
function animate(sprite) {
setTimeout(function() {
if (spritePlayCount < maxSpritePlays) {
requestAnimationFrame(function() {
animate(sprite);
});
}
// Drawing code goes here
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(sprite, spritePosition * spriteWidth, 0, spriteWidth, spriteHeight, 0, 0, spriteWidth, spriteHeight);
spritePosition++;
if (spritePosition > spriteCount - 1) {
spritePosition = 0;
spritePlayCount++;
}
}, 1000 / fps);
}
objectS.onload = function() {
animate(objectS);
};
});
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
I'm trying to adapt the Html JS library Char.js to QML QtQuick 2.4.
The library have a function to animate the scene. Everything works great if I don't animate it (eg animate with only 1 step of animation).
When the animation is used, the canvas freeze until the animation is finished, and then the scene is redraw. Here is the animationLoop function:
animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){
var currentStep = 0,
easingFunction = easingEffects[easingString] || easingEffects.linear;
var animationFrame = function(){
currentStep++;
var stepDecimal = currentStep/totalSteps;
var easeDecimal = easingFunction(stepDecimal);
callback.call(chartInstance,easeDecimal,stepDecimal, currentStep);
onProgress.call(chartInstance,easeDecimal,stepDecimal);
if (currentStep < totalSteps){
chartInstance.animationFrame = chartInstance.chart.canvas.requestAnimationFrame(animationFrame);
} else{
onComplete.apply(chartInstance);
}
};
chartInstance.chart.canvas.requestAnimationFrame(animationFrame);
},
Each time the onAnimationProgress callback is called, I call the Canvas requestPaint function to redraw the scene. Unfortunately, the onPaint function will not be called until every step of the animation have finished.
The Canvas object is accessible with : chartInstance.chart.canvas. I tried to call directely chartInstance.chart.canvas.requestPaint() at each iteration too, which do not work. I still have to wait until the animation finished to see the scene redraw correctly.
onAnimationProgress: function(easeDecimal,stepDecimal) {
skipNextChartUpdate = true;
requestPaint();
}
onPaint: {
if (!chartInstance) {
initializeChart()
} else if(!skipNextChartUpdate) {
chartInstance.scale.update({width:width,height:height});
chartInstance.resize(chartInstance.update);
} else {
skipNextChartUpdate = false;
}
}
This is more or less as I would expect. requestPaint is not an immediate operation, it's a request to repaint at a later point. So if you call requestPaint multiple times in a single JS function, you will only receive one onPaint event per (vsync) frame.
So if you want to drive an animation, you should be driving them inside onPaint, by calling requestPaint (to ask for another onPaint event in the future) if there is still more animation to show for instance.
What I am trying to do a simple javascript animation on the HTML5 canvas. Right now my canvases are layered so that when I receive a mouse event the background layer doesn't change but the top layer with the avatars move around. If I use requestAnimationFrame and don't clear the screen, I see my nice little player moving across the screen in multiple frames with a long tail of the character. However, if I try and do the clearRect after each animation frame, then my character never appears and I'm not sure what is causing this.
I am using these links as a basis for my code:
http://www.html5canvastutorials.com/advanced/html5-canvas-start-and-stop-an-animation/
http://paulirish.com/2011/requestanimationframe-for-smart-animating/
http://www.nczonline.net/blog/2011/05/03/better-javascript-animations-with-requestanimationframe/
A lot of the examples are animating shapes that are drawn, whereas I'm using images, not sure if this matters and whether I should've just used a canvas transform function instead of clearRect, but didn't think this should've made a difference. Also, I deleted a bunch of code for readability, so the brackets may be off, but the code is functioning, I just did it for readability so you see animation in one direction. My code looks something like:
// what is this function for? See here - http://stackoverflow.com/questions/10237471/please-explain-this-requestanimationframe-idiom
window.requestAnimFrame = function(callback){
// add in this parentheses - http://stackoverflow.com/questions/5605588/how-to-use- requestanimationframe
return ( window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback){
window.setTimeout(callback, 1000 / 60);
}
);
}();
function stopAnimatingPlayer(currentAvatarAnimating, destinationCellX, destinationCellY) {
gIsAnimating = false;
//Did this final draw because I wasn't sure if the avatar would end on the exact pixel position, so this should snap him back into place
drawAvatar(currentAvatarAnimating, destinationCellX, destinationCellY, false,0,0);
}
function movePlayer(lastTime, playerPixelX, playerPixelY, destinationCellX, destinationCellY) {
if (gIsAnimating) {
// the canvas is already globally held as gAvatarCanvasElement & gAvatarDrawingContext;
// update
var date = new Date();
var time = date.getTime();
var timeDiff = time - lastTime;
var linearSpeed = 100;
// pixels / second
var linearDistEachFrame = linearSpeed * timeDiff / 1000;
var horizontal = false;
var newX, newY;
// gets the new coordinate of the player
if (gTowerCurrentPlayer == 1) {
//fill in later - just trying to get one horizontal animation working
} else if (destinationCellY == gPlayer1Cell.y) { // we're moving horizontally
var currentX = playerPixelX;
var diffX = destinationCellX - gPlayer1Cell.x;
horizontal = true;
if (diffX > 0) { // player is moving right - just get one direction going for now
if (currentX < getPixelFromRow(destinationCellX)) {
newX = currentX + linearDistEachFrame;
} else {
stopAnimatingPlayer(gTowerCurrentPlayer, destinationCellX, destinationCellY);
}
} //fill in rest later - get one direction working
lastTime = time;
// clear - this is where the problem is
gAvatarDrawingContext.clearRect(playerPixelX, playerPixelY, kPieceWidth, kPieceHeight);
//gAvatarDrawingContext.clearRect(0,0, gAvatarCanvasElement.width, gAvatarCanvasElement.height);
if (horizontal) {
drawAvatar(gTowerCurrentPlayer, 0, 0, true, newX, playerPixelY);
// request new frame
requestAnimFrame(function(){
movePlayer(lastTime, newX, playerPixelY, destinationCellX, destinationCellY);
});
}
}
}
function animatePlayer(playerPixelX, playerPixelY, destinationCellX, destinationCellY) {
gIsAnimating = true; // global var here
var date = new Date();
var time = date.getTime();
movePlayer(time, playerPixelX, playerPixelY, destinationCellX, destinationCellY);
}
If anyone could provide any help I would really appreciate it, I'm just not getting why this is not working. I don't need super flashy animations which is why i didn't go with kineticjs or any of the other libraries out there.
Thanks.
When you clear the canvas, it erases everything on it, so if you call it after you've drawn you get a blank canvas, which is what you are describing. Instead, you should wait until the next frame, and then clear the canvas before drawing, rather than after, so that the drawn image shows up for a while. To fix your problem, just move your clearing command up so that it happens immediately before your drawing commands for each frame.