Smooth Canvas Animation

Jeff Jenkins picture Jeff Jenkins · Sep 25, 2012 · Viewed 10.5k times · Source

I'm trying to learn how to create smooth JavaScript animations using HTML5's canvas. For some reason, the animation is not smooth, but kind of "sputtery".

You can see the framework I've built on this jsFiddle, which only uses Webkit properties at the moment.

Another developer was able to create the same concept using their WebViews source code, which is based on Ext.js. For learning purposes, I wanted to avoid using any libraries in effort to better understand JavaScript. The WebViews concept can be viewed at this jsFiddle, which shows a much smoother animation.

I've read and tried all kinds of different scenarios from pulling the update calls out of the requestAnimationFrame into it's own loop, to translating the context to the draw position, to drawing to a back-buffer context and copying it to the viewport. The following code represents my best efforts so far.

Any suggestions on how to improve the performance in order to get an object to move smoothly without all the overhead of an external library?

Thank you in advance.

var app;
var now = then = delta = 0;
var viewport = document.createElement( 'canvas' );
var viewportContext = viewport.getContext( '2d' );

function App( )
{
    this.circle = {
        position : viewport.width / 2,
        radius : 10
    };
}

App.prototype.initialize = function( )
{
    app = this;
    document.body.appendChild( viewport );
    viewport.width = 320;
    viewport.height = 200;

    webkitRequestAnimationFrame( app.render, viewport );
};

App.prototype.render = function( )
{
    now = performance.webkitNow( );
    delta = ( now - then ) / 1000.0;
    then = now;

    app.update( delta );
    viewportContext.clearRect( 0, 0, viewport.width, viewport.height );
    app.draw( viewportContext );

    webkitRequestAnimationFrame( app.render, viewport );
};

App.prototype.draw = function( context )
{
    context.fillStyle = "white";
    context.beginPath( );
    context.arc( this.circle.position | 0, viewport.height / 2 | 0, this.circle.radius | 0, 0, Math.PI * 2, true );
    context.closePath( );
    context.fill( );
};

App.prototype.update = function( deltaTime )
{
    this.circle.position += viewport.width / 5 * deltaTime;

    if( this.circle.position >= viewport.width )
    {
        this.circle.position = 0;
    }
};

window.onload = function( )
{
    new App( ).initialize( );
};​

Answer

Shmiddty picture Shmiddty · Sep 25, 2012

See this page for many common optimizations, and great explanations about why and how they improve performance.

Also, for some reason, performance seems to increase on a more "medium-size" canvas. I'm not entirely sure why this is, but I believe it has to do with browser optimization.

You can note some performance gains with a few small tweaks here: http://jsfiddle.net/3TAVu/1/

Specifically, I removed the superfluous assignment to fillStyle here:

App.prototype.draw = function( context )
{
    context.beginPath( );
    context.arc( this.circle.position | 0, viewport.height / 2 | 0, this.circle.radius | 0, 0, Math.PI * 2, true );
    context.closePath( );
    context.fill( );
};

I also modified the render method by clearing only the relevant portion of the canvas instead of the entire thing:

App.prototype.render = function( )
{
    now = performance.webkitNow( );
    delta = ( now - then ) / 1000.0;
    then = now;

    var cX = app.circle ? (app.circle.position - app.circle.radius) : 0;
    var cY = Math.round(viewport.height/2) - app.circle.radius;
    var w = app.circle ? app.circle.radius * 2 : 0;

    viewportContext.clearRect(cX - 1, cY - 1, w + 2, w + 2);

    app.update( delta );
    app.draw( viewportContext );

    webkitRequestAnimationFrame( app.render, viewport );
};