Skip to content

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:

  • The animation code is synchronized with display refreshes for efficiency The clear + redraw code is scheduled, but not immediately executed. The browser will execute the clear + redraw code only when the display is ready to refresh. This synchronization with the refresh cycle increases your animation performance by giving your code the most available time in which to complete.
  • Every loop is always completed before another loop is allowed to start. This prevents "tearing", where the user sees an incomplete version of the drawing. The eye particularly notices tearing and is distracted when tearing occurs. So preventing tearing makes your animation appear smoother and more consistent.
    • 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

    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 created
    const textToDisplay = "This is an example that uses the canvas to animate some text.";
    const textStyle = "white";
    const BGStyle = "black"; // background style
    const textSpeed = 0.2; // in pixels per millisecond
    const 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 height
    var textWidth = ctx.measureText(textToDisplay).width; // get the text width
    var totalTextSize = (canvas.width + textHorMargin * 2 + textWidth);
    ctx.textBaseline = "middle"; // not put the text in the vertical center
    ctx.textAlign = "left"; // align to the left
    var textX = canvas.width + 8; // start with the text off screen to the right
    var 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 frame

    A 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.

  • In event handlers, save all the event variables that control where drawings are positioned on the Canvas. But don't actually draw anything.
  • In a `requestAnimationFrame` loop, render all the drawings to the Canvas using the saved information.
  • 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>

    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 duration
    var 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 above
    var Easings = ...
    // Demo
    var startTime;
    var beginningValue=50; // beginning x-coordinate
    var endingValue=450; // ending x-coordinate
    var 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 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 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 animation

    Use vectors to calculate incremental [x,y] from [startX,startY] to [endX,endY]

    // dx is the total distance to move in the X direction
    var dx = endX - startX;
    // dy is the total distance to move in the Y direction
    var 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 point
    var pct=0;
    // use dx & dy to calculate where the current [x,y] is at a given pct
    var x = startX + dx * pct/100;
    var y = startY + dx * pct/100;

    Example Code:

    // canvas vars
    var 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 styles
    ctx.strokeStyle='skyblue';
    ctx.fillStyle='blue';
    // animating vars
    var 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 running
    requestAnimationFrame(animate);
    // listen for mouse events
    window.onmousedown=(function(e){handleMouseDown(e);});
    window.onmouseup=(function(e){handleMouseUp(e);});
    // constantly running loop
    // will animate dot from startX,startY to endX,endY
    function 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);
    }