Animation
Use requestAnimationFrame() NOT setInterval() for animation loops
Section titled “Use requestAnimationFrame() NOT setInterval() for animation loops”requestAnimationFrame is similar to setInterval, it but has these important improvements:
- Animation automatically stops when the user switches to a different browser tab. This saves power on mobile devices because the device is not wasting power computing an animation that the user can’t currently see.
Device displays will refresh about 60 times per second so requestAnimationFrame can continuously redraw at about 60 “frames” per second. The eye sees motion at 20-30 frames per second so requestAnimationFrame can easily create the illusion of motion.
Notice that requestAnimationFrame is recalled at the end of each animateCircle. This is because each ‘requestAnimatonFrameonly requests a single execution of the animation function.
Example: simple `requestAnimationFrame
<!doctype html><html><head><style> body{ background-color:white; } #canvas{border:1px solid red; }</style><script>window.onload=(function(){
// canvas related variables var canvas=document.getElementById("canvas"); var ctx=canvas.getContext("2d"); var cw=canvas.width; var ch=canvas.height;
// start the animation requestAnimationFrame(animate);
function animate(currentTime){
// draw a full randomly circle var x=Math.random()*canvas.width; var y=Math.random()*canvas.height; var radius=10+Math.random()*15; ctx.beginPath(); ctx.arc(x,y,radius,0,Math.PI*2); ctx.fillStyle='#'+Math.floor(Math.random()*16777215).toString(16); ctx.fill();
// request another loop of animation requestAnimationFrame(animate); }
}); // end $(function(){});</script></head><body> <canvas id="canvas" width=512 height=512></canvas></body></html>To illustrate the advantages of requestAnimationFrame this stackoverflow question has a live demo
Animate an image across the Canvas
Section titled “Animate an image across the Canvas”This example loads and animates and image across the Canvas
Important Hint! Make sure you give your image time to fully load by using image.onload.
Annotated Code
<!doctype html><html><head><style> body{ background-color:white; } #canvas{border:1px solid red; }</style><script>window.onload=(function(){
// canvas related variables var canvas=document.getElementById("canvas"); var ctx=canvas.getContext("2d"); var cw=canvas.width; var ch=canvas.height;
// animation related variables var minX=20; // Keep the image animating var maxX=250; // between minX & maxX var x=minX; // The current X-coordinate var speedX=1; // The image will move at 1px per loop var direction=1; // The image direction: 1==righward, -1==leftward var y=20; // The Y-coordinate
// Load a new image // IMPORTANT!!! You must give the image time to load by using img.onload! var img=new Image(); img.onload=start; img.src="https://dl.dropboxusercontent.com/u/139992952/stackoverflow/sun.png"; function start(){ // the image is fully loaded sostart animating requestAnimationFrame(animate); }
function animate(time){
// clear the canvas ctx.clearRect(0,0,cw,ch);
// draw ctx.drawImage(img,x,y);
// update x += speedX * direction; // keep "x" inside min & max if(x<minX){ x=minX; direction*=-1; } if(x>maxX){ x=maxX; direction*=-1; }
// request another loop of animation requestAnimationFrame(animate); }
}); // end $(function(){});</script></head><body> <canvas id="canvas" width=512 height=512></canvas></body></html>Simple animation with 2D context and requestAnimationFrame
Section titled “Simple animation with 2D context and requestAnimationFrame”This example will show you how to create a simple animation using the canvas and the 2D context. It is assumed you know how to create and add a canvas to the DOM and obtain the context
// this example assumes ctx and canvas have been createdconst textToDisplay = "This is an example that uses the canvas to animate some text.";const textStyle = "white";const BGStyle = "black"; // background styleconst textSpeed = 0.2; // in pixels per millisecondconst textHorMargin = 8; // have the text a little outside the canvas
ctx.font = Math.floor(canvas.height * 0.8) + "px arial"; // size the font to 80% of canvas heightvar textWidth = ctx.measureText(textToDisplay).width; // get the text widthvar totalTextSize = (canvas.width + textHorMargin * 2 + textWidth);ctx.textBaseline = "middle"; // not put the text in the vertical centerctx.textAlign = "left"; // align to the leftvar textX = canvas.width + 8; // start with the text off screen to the rightvar textOffset = 0; // how far the text has moved
var startTime;// this function is call once a frame which is approx 16.66 ms (60fps)function update(time){ // time is passed by requestAnimationFrame if(startTime === undefined){ // get a reference for the start time if this is the first frame startTime = time; } ctx.fillStyle = BGStyle; ctx.fillRect(0, 0, canvas.width, canvas.height); // clear the canvas by drawing over it textOffset = ((time - startTime) * textSpeed) % (totalTextSize); // move the text left ctx.fillStyle = textStyle; // set the text style ctx.fillText(textToDisplay, textX - textOffset, canvas.height / 2); // render the text
requestAnimationFrame(update);// all done request the next frame}requestAnimationFrame(update);// to start request the first frameA demo of this example at jsfiddle
Animate at a specified interval (add a new rectangle every 1 second)
Section titled “Animate at a specified interval (add a new rectangle every 1 second)”This example adds a new rectangle to the canvas every 1 second (== a 1 second interval)
Annotated Code:
<!doctype html><html><head><style> body{ background-color:white; } #canvas{border:1px solid red; }</style><script>window.onload=(function(){
// canvas related variables var canvas=document.getElementById("canvas"); var ctx=canvas.getContext("2d"); var cw=canvas.width; var ch=canvas.height;
// animation interval variables var nextTime=0; // the next animation begins at "nextTime" var duration=1000; // run animation every 1000ms
var x=20; // the X where the next rect is drawn
// start the animation requestAnimationFrame(animate);
function animate(currentTime){
// wait for nextTime to occur if(currentTime<nextTime){ // request another loop of animation requestAnimationFrame(animate); // time hasn't elapsed so just return return; } // set nextTime nextTime=currentTime+duration;
// add another rectangle every 1000ms ctx.fillStyle='#'+Math.floor(Math.random()*16777215).toString(16); ctx.fillRect(x,30,30,30);
// update X position for next rectangle x+=30;
// request another loop of animation requestAnimationFrame(animate); }
}); // end $(function(){});</script></head><body> <canvas id="canvas" width=512 height=512></canvas></body></html>Animate at a specified time (an animated clock)
Section titled “Animate at a specified time (an animated clock)”This example animates a clock showing the seconds as a filled wedge
Annotated Code:
<!doctype html><html><head><style> body{ background-color:white; } #canvas{border:1px solid red; }</style><script>window.onload=(function(){
// canvas related variables var canvas=document.getElementById("canvas"); var ctx=canvas.getContext("2d"); var cw=canvas.width; var ch=canvas.height; // canvas styling for the clock ctx.strokeStyle='lightgray'; ctx.fillStyle='skyblue'; ctx.lineWidth=5;
// cache often used values var PI=Math.PI; var fullCircle=PI*2; var sa=-PI/2; // == the 12 o'clock angle in context.arc
// start the animation requestAnimationFrame(animate);
function animate(currentTime){
// get the current seconds value from the system clock var date=new Date(); var seconds=date.getSeconds();
// clear the canvas ctx.clearRect(0,0,cw,ch);
// draw a full circle (== the clock face); ctx.beginPath(); ctx.moveTo(100,100); ctx.arc(100,100,75,0,fullCircle); ctx.stroke(); // draw a wedge representing the current seconds value ctx.beginPath(); ctx.moveTo(100,100); ctx.arc(100,100,75,sa,sa+fullCircle*seconds/60); ctx.fill();
// request another loop of animation requestAnimationFrame(animate); }
}); // end $(function(){});</script></head><body> <canvas id="canvas" width=512 height=512></canvas></body></html>Don’t draw animations in your event handlers (a simple sketch app)
Section titled “Don’t draw animations in your event handlers (a simple sketch app)”During mousemove you get flooded with 30 mouse events per second. You might not be able to redraw your drawings at 30 times per second. Even if you can, you’re probably wasting computing power by drawing when the browser is not ready to draw (wasted == across display refresh cycles).
Therefore it makes sense to separate your users input events (like mousemove) from the drawing of your animations.
By not drawing in the event handlers, you are not forcing Canvas to try to refresh complex drawings at mouse event speeds.
By doing all drawing in requestAnimationFrame you gain all the benefits described in here Use ‘requestanimationFrame’ not ‘setInterval’ for animation loops.
Annotated Code:
<!doctype html><html><head><style> body{ background-color: ivory; } #canvas{border:1px solid red; }</style><script>window.onload=(function(){
function log(){console.log.apply(console,arguments);}
// canvas variables var canvas=document.getElementById("canvas"); var ctx=canvas.getContext("2d"); var cw=canvas.width; var ch=canvas.height; // set canvas styling ctx.strokeStyle='skyblue'; ctx.lineJoint='round'; ctx.lineCap='round'; ctx.lineWidth=6;
// handle windows scrolling & resizing function reOffset(){ var BB=canvas.getBoundingClientRect(); offsetX=BB.left; offsetY=BB.top; } var offsetX,offsetY; reOffset(); window.onscroll=function(e){ reOffset(); } window.onresize=function(e){ reOffset(); }
// vars to save points created during mousemove handling var points=[]; var lastLength=0;
// start the animation loop requestAnimationFrame(draw);
canvas.onmousemove=function(e){handleMouseMove(e);}
function handleMouseMove(e){ // tell the browser we're handling this event e.preventDefault(); e.stopPropagation();
// get the mouse position mouseX=parseInt(e.clientX-offsetX); mouseY=parseInt(e.clientY-offsetY);
// save the mouse position in the points[] array // but don't draw anything points.push({x:mouseX,y:mouseY}); }
function draw(){ // No additional points? Request another frame an return var length=points.length; if(length==lastLength){requestAnimationFrame(draw);return;}
// draw the additional points var point=points[lastLength]; ctx.beginPath(); ctx.moveTo(point.x,point.y) for(var i=lastLength;i<length;i++){ point=points[i]; ctx.lineTo(point.x,point.y); } ctx.stroke();
// request another animation loop requestAnimationFrame(draw); }
}); // end window.onload</script></head><body> <h4>Move mouse over Canvas to sketch</h4> <canvas id="canvas" width=512 height=512></canvas></body></html>Easing using Robert Penners equations
Section titled “Easing using Robert Penners equations”An easing causes some variable to change unevenly over a duration.
“variable” must be able to be expressed as a number, and can represent a remarkable variety of things:
- an X-coordinate,
- a rectangle’s width,
- an angle of rotation,
- the red component of an R,G,B color.
- anything that can be expressed as a number.
“duration” must be able to be expressed as a number and can also be a variety of things:
- a period of time,
- a distance to be travelled,
- a quantity of animation loops to be executed,
- anything that can be expressed as
“unevenly” means that the variable progresses from beginning to ending values unevenly:
- faster at the beginning & slower at the ending — or visa-versa,
- overshoots the ending but backs up to the ending as the duration finishes,
- repeatedly advances/retreats elastically during the duration,
- “bounces” off the ending while coming to rest as the duration finishes.
Attribution: Robert Penner has created the “gold standard” of easing functions.
Cite: https://github.com/danro/jquery-easing/blob/master/jquery.easing.js
// t: elapsed time inside duration (currentTime-startTime),// b: beginning value,// c: total change from beginning value (endingValue-startingValue),// d: total durationvar Easings={ easeInQuad: function (t, b, c, d) { return c*(t/=d)*t + b; }, easeOutQuad: function (t, b, c, d) { return -c *(t/=d)*(t-2) + b; }, easeInOutQuad: function (t, b, c, d) { if ((t/=d/2) < 1) return c/2*t*t + b; return -c/2 * ((--t)*(t-2) - 1) + b; }, easeInCubic: function (t, b, c, d) { return c*(t/=d)*t*t + b; }, easeOutCubic: function (t, b, c, d) { return c*((t=t/d-1)*t*t + 1) + b; }, easeInOutCubic: function (t, b, c, d) { if ((t/=d/2) < 1) return c/2*t*t*t + b; return c/2*((t-=2)*t*t + 2) + b; }, easeInQuart: function (t, b, c, d) { return c*(t/=d)*t*t*t + b; }, easeOutQuart: function (t, b, c, d) { return -c * ((t=t/d-1)*t*t*t - 1) + b; }, easeInOutQuart: function (t, b, c, d) { if ((t/=d/2) < 1) return c/2*t*t*t*t + b; return -c/2 * ((t-=2)*t*t*t - 2) + b; }, easeInQuint: function (t, b, c, d) { return c*(t/=d)*t*t*t*t + b; }, easeOutQuint: function (t, b, c, d) { return c*((t=t/d-1)*t*t*t*t + 1) + b; }, easeInOutQuint: function (t, b, c, d) { if ((t/=d/2) < 1) return c/2*t*t*t*t*t + b; return c/2*((t-=2)*t*t*t*t + 2) + b; }, easeInSine: function (t, b, c, d) { return -c * Math.cos(t/d * (Math.PI/2)) + c + b; }, easeOutSine: function (t, b, c, d) { return c * Math.sin(t/d * (Math.PI/2)) + b; }, easeInOutSine: function (t, b, c, d) { return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b; }, easeInExpo: function (t, b, c, d) { return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b; }, easeOutExpo: function (t, b, c, d) { return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b; }, easeInOutExpo: function (t, b, c, d) { if (t==0) return b; if (t==d) return b+c; if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b; return c/2 * (-Math.pow(2, -10 * --t) + 2) + b; }, easeInCirc: function (t, b, c, d) { return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b; }, easeOutCirc: function (t, b, c, d) { return c * Math.sqrt(1 - (t=t/d-1)*t) + b; }, easeInOutCirc: function (t, b, c, d) { if ((t/=d/2) < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b; return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b; }, easeInElastic: function (t, b, c, d) { var s=1.70158;var p=0;var a=c; if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3; if (a < Math.abs(c)) { a=c; var s=p/4; } else var s = p/(2*Math.PI) * Math.asin (c/a); return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; }, easeOutElastic: function (t, b, c, d) { var s=1.70158;var p=0;var a=c; if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3; if (a < Math.abs(c)) { a=c; var s=p/4; } else var s = p/(2*Math.PI) * Math.asin (c/a); return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b; }, easeInOutElastic: function (t, b, c, d) { var s=1.70158;var p=0;var a=c; if (t==0) return b; if ((t/=d/2)==2) return b+c; if (!p) p=d*(.3*1.5); if (a < Math.abs(c)) { a=c; var s=p/4; } else var s = p/(2*Math.PI) * Math.asin (c/a); if (t < 1) return -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*.5 + c + b; }, easeInBack: function (t, b, c, d, s) { if (s == undefined) s = 1.70158; return c*(t/=d)*t*((s+1)*t - s) + b; }, easeOutBack: function (t, b, c, d, s) { if (s == undefined) s = 1.70158; return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b; }, easeInOutBack: function (t, b, c, d, s) { if (s == undefined) s = 1.70158; if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b; return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b; }, easeInBounce: function (t, b, c, d) { return c - Easings.easeOutBounce (d-t, 0, c, d) + b; }, easeOutBounce: function (t, b, c, d) { if ((t/=d) < (1/2.75)) { return c*(7.5625*t*t) + b; } else if (t < (2/2.75)) { return c*(7.5625*(t-=(1.5/2.75))*t + .75) + b; } else if (t < (2.5/2.75)) { return c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b; } else { return c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b; } }, easeInOutBounce: function (t, b, c, d) { if (t < d/2) return Easings.easeInBounce (t*2, 0, c, d) * .5 + b; return Easings.easeOutBounce (t*2-d, 0, c, d) * .5 + c*.5 + b; },};Example Usage:
// include the Easings object from abovevar Easings = ...
// Demovar startTime;var beginningValue=50; // beginning x-coordinatevar endingValue=450; // ending x-coordinatevar totalChange=endingValue-beginningValue;var totalDuration=3000; // ms
var keys=Object.keys(Easings);ctx.textBaseline='middle';requestAnimationFrame(animate);
function animate(time){ var PI2=Math.PI*2; if(!startTime){startTime=time;} var elapsedTime=Math.min(time-startTime,totalDuration); ctx.clearRect(0,0,cw,ch); ctx.beginPath(); for(var y=0;y<keys.length;y++){ var key=keys[y]; var easing=Easings[key]; var easedX=easing( elapsedTime,beginningValue,totalChange,totalDuration); if(easedX>endingValue){easedX=endingValue;} ctx.moveTo(easedX,y*15); ctx.arc(easedX,y*15+10,5,0,PI2); ctx.fillText(key,460,y*15+10-1); } ctx.fill(); if(time<startTime+totalDuration){ requestAnimationFrame(animate); }}Set frame rate using requestAnimationFrame
Section titled “Set frame rate using requestAnimationFrame”Using requestAnimationFrame may on some systems update at more frames per second than the 60fps. 60fps is the default rate if the rendering can keep up. Some systems will run at 120fps maybe more.
If you use the following method you should only use frame rates that are integer divisions of 60 so that (60 / FRAMES_PER_SECOND) % 1 === 0 is true or you will get inconsistent frame rates.
const FRAMES_PER_SECOND = 30; // Valid values are 60,30,20,15,10...// set the mim time to render the next frameconst FRAME_MIN_TIME = (1000/60) * (60 / FRAMES_PER_SECOND) - (1000/60) * 0.5;var lastFrameTime = 0; // the last frame timefunction update(time){ if(time-lastFrameTime < FRAME_MIN_TIME){ //skip the frame if the call is too 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); // get next farme}requestAnimationFrame(update); // start animationAnimate from [x0,y0] to [x1,y1]
Section titled “Animate from [x0,y0] to [x1,y1]”Use vectors to calculate incremental [x,y] from [startX,startY] to [endX,endY]
// dx is the total distance to move in the X directionvar dx = endX - startX;
// dy is the total distance to move in the Y directionvar dy = endY - startY;
// use a pct (percentage) to travel the total distances// start at 0% which == the starting point// end at 100% which == then ending pointvar pct=0;
// use dx & dy to calculate where the current [x,y] is at a given pctvar x = startX + dx * pct/100;var y = startY + dx * pct/100;Example Code:
// canvas varsvar canvas=document.createElement("canvas");document.body.appendChild(canvas);canvas.style.border='1px solid red';var ctx=canvas.getContext("2d");var cw=canvas.width;var ch=canvas.height;// canvas stylesctx.strokeStyle='skyblue';ctx.fillStyle='blue';
// animating varsvar pct=101;var startX=20;var startY=50;var endX=225;var endY=100;var dx=endX-startX;var dy=endY-startY;
// start animation loop runningrequestAnimationFrame(animate);
// listen for mouse eventswindow.onmousedown=(function(e){handleMouseDown(e);});window.onmouseup=(function(e){handleMouseUp(e);});
// constantly running loop// will animate dot from startX,startY to endX,endYfunction animate(time){ // demo: rerun animation if(++pct>100){pct=0;} // update x=startX+dx*pct/100; y=startY+dy*pct/100; // draw ctx.clearRect(0,0,cw,ch); ctx.beginPath(); ctx.moveTo(startX,startY); ctx.lineTo(endX,endY); ctx.stroke(); ctx.beginPath(); ctx.arc(x,y,5,0,Math.PI*2); ctx.fill() // request another animation loop requestAnimationFrame(animate);}