Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Mastering the HTML5 Canvas: Part 1

DZone's Guide to

Mastering the HTML5 Canvas: Part 1

· Web Dev Zone
Free Resource

Learn how to build modern digital experience apps with Crafter CMS. Download this eBook now. Brought to you in partnership with Crafter Software

This post comes from Graham Murray at the Infragistics blog.

This is the first in a series of blog posts about mastering the HTML5 canvas element. We’ll be focusing first on animation as it is one of the best ways to leverage the canvas to bring new interaction types and visualizations to your web applications. Here’s a preview of what we are building for part one.

Canvas Overview

The HTML5 canvas element provides an immediate mode 2-D rendering API that we can use in our web applications. It depends on no browser plugins and it only requires a modern enough browser that supports the HTML5 canvas component. Even if you're trying to support older versions of Internet Explorer (8 and below), there are some polyfills that can be used to attempt to provide support for them (FlashCanvas, exCanvas, etc.).  

Because canvas is an immediate-mode rendering engine, rather than building an object graph that represents all the objects that are visible (as you would in a retained mode system like Silverlight or WPF), we will talk to the rendering context of the canvas and, for each frame, we will tell it which visual primitives to render at which locations.

Hello Circle

To begin with, let's render a circle on the screen.

We will assume that jQuery has been referenced for this web application to assist us in manipulating the required DOM elements succinctly. But jQuery is certainly not required in order to interact with the canvas. First, we will start with the DOM. We need to add a canvas element somewhere in our DOM hierarchy where we want to display the canvas content. In this case, it's the only element in my page body.

<canvas id="canvas"></canvas> 

I’ve provided an id so that we can easily find this element later when we want to render its content.

Now it's time to render the circle into the canvas. We need to start by acquiring the 2-D rendering context from the canvas. The rendering context is what surfaces to us the API that we need in order to render primitives into the canvas.

    $(function () {  
        var canv, context;  
          
        $("#canvas").attr("width", "500px").attr("height", "500px");  
        canv = $("#canvas")[0];  
        context = canv.getContext("2d");  

Here we are using jQuery to locate the canvas by id, then setting its width and height. Something that may confuse you initially is that there are really two widths and heights that are relevant to the canvas. The canvas element has attributes for width and height that represent the size of the bitmap that is used for rendering content within the canvas. Meanwhile, you can also set a CSS width and height on the canvas. These represent the size to which the bitmap generated by the canvas is scaled when displayed on the page. So, for example, if you created a 50px by 50px canvas and scaled it to 500px by 500px, it would look very blocky since you would be stretching a 50px square to fill a 500px square area. Most of the time, you will probably want the CSS size to be the same as the attribute size of the canvas.

Next, we extract a reference to the canvas DOM element from jQuery and call getContext(“2d”) to get the rendering context. Why getContext(“2d”)? This is because the HTML5 canvas also plays an integral role in WebGL beyond its 2-D API, but for now, we are talking about 2-D.

Then we render the actual circle. Here I’m defining a function called "draw" that will render the circle and then call it to do the actual rendering. Why did I split this up? This will become apparent when we add animation to the mix.

    function draw() {  
            context.fillStyle = "rgb(255,0,0)";  
            context.beginPath();  
            context.arc(150, 150, 120, 0, 2 * Math.PI, false);  
            context.fill();  
        }  
      
        draw();  

Here we are:

  • Setting the fill style of the canvas to red; you can use most syntax that a CSS color property will accept here.
  • Telling the context to begin a new path
  • Drawing an arc with a center at 150px and 150px with a radius of 120, sweeping from zero to 360 degrees, in a clockwise direction (Note that the API actually wants radians, not degrees.)
  • Telling the context to fill the path that we have defined, thus filling the circle

And here is the result.

Hello Animation

Now, let's animate our circle in some way.

First, here’s the modified logic from our previous version:

    var canv, context, lastTime, duration, progress, forward = true;  
          
    $("#canvas").attr("width", "500px").attr("height", "500px");  
    canv = $("#canvas")[0];  
    context = canv.getContext("2d");  
      
    lastTime = new Date().getTime();  
      
    duration = 1000;  
    progress = 0;  
      
    function draw() {  
        var elapsed, time, b;  
          
        time = new Date().getTime();  
        elapsed = time - lastTime;  
        lastTime = time;  
      
        if (forward) {  
            progress += elapsed / duration;  
        } else {  
            progress -= elapsed / duration;  
        }  
        if (progress > 1.0) {  
            progress = 1.0;  
            forward = false;  
        }  
        if (progress < 0) {  
            progress = 0;  
            forward = true;  
        }  
      
        b = 255.0 * progress;  
        context.fillStyle = "rgb(255," + Math.round(b.toString()) + ",0)";  
        context.beginPath();  
        context.arc(150, 150, 120, 0, 2 * Math.PI, false);  
        context.fill();  
    }  
      
    window.setInterval(draw, 1000 / 60.0);  

So what are the differences above?

First, we are using window.setInterval to call our draw method repeatedly. We are attempting to do this 60 times a second (1000 milliseconds / 60.0).

    window.setInterval(draw, 1000 / 60.0);  

Next, for each time we draw, we determine how much time elapsed since the last frame that was drawn and we update our progress toward the completion of the animation. When the progress reaches completion, we reverse the direction and decrease it back down to zero. Then, when we hit zero, we turn around and progress toward one again. This way, the animation just loops backward and forward continually.

time = new Date().getTime();  
elapsed = time - lastTime;  
lastTime = time;  
  
if (forward) {  
    progress += elapsed / duration;  
} else {  
    progress -= elapsed / duration;  
}  
if (progress > 1.0) {  
    progress = 1.0;  
    forward = false;  
}  
if (progress < 0) {  
    progress = 0;  
    forward = true;  
} 

Next, the progress is used to drive the color we are using to render the ellipse.

b = 255.0 * progress;  
context.fillStyle = "rgb(255," + Math.round(b.toString()) + ",0)"; 

And so we have an ellipse that animates in color.

Improving Our Animation

We can make a small adjustment to the above logic in order to improve the way that the animation is performed. There are two assumptions that the above logic makes that are not necessarily true: It assumes both that the machine running the logic will be fast enough to provide a 60 frames per second (fps) animation, and that the draw method will be called with reliable enough timing to create a smooth animated effect. The machine may be too slow to perform the animation update the required number of times per second, and setInterval is not guaranteed to be called reliably at all, especially if lots of logic is hammering the JavaScript event queue. Some modern browsers support an API that lets us do better than this when performing animation. We can essentially request that the browser notify us when an animation frame is ready, so that we can respond by rendering some content. This provides more reliable and smooth animation, and should let us avoid trying to animate more frames than the system is able to handle. Here is the modified version of the logic that will use a polyfill in order to use the requestAnimationFrame API, if present, and otherwise fall back on our previous method for animating if we are on a browser that does not support requestAnimationFrame.

    function ensureQueueFrame() {  
        if (!window.queueFrame) {  
            if (window.requestAnimationFrame) {  
                window.queueFrame = window.requestAnimationFrame;  
            } else if (window.webkitRequestAnimationFrame) {  
                window.queueFrame = window.webkitRequestAnimationFrame;  
            } else if (window.mozRequestAnimationFrame) {  
                window.queueFrame = window.mozRequestAnimationFrame;  
            } else {  
                window.queueFrame = function (callback) {  
                    window.setTimeout(1000.0 / 60.0, callback);  
                };  
            }  
        }  
    }  
      
    $(function () {  
        var canv, context, lastTime, duration, progress, forward = true;  
        ensureQueueFrame();  
      
        $("#canvas").attr("width", "500px").attr("height", "500px");  
        canv = $("#canvas")[0];  
        context = canv.getContext("2d");  
      
        lastTime = new Date().getTime();  
      
        duration = 1000;  
        progress = 0;  
      
        function draw() {  
            var ellapsed, time, b;  
            queueFrame(draw);  
      
            time = new Date().getTime();  
            ellapsed = time - lastTime;  
            lastTime = time;  
      
            if (forward) {  
                progress += ellapsed / duration;  
            } else {  
                progress -= ellapsed / duration;  
            }  
            if (progress > 1.0) {  
                progress = 1.0;  
                forward = false;  
            }  
            if (progress < 0) {  
                progress = 0;  
                forward = true;  
            }  
      
            b = 255.0 * progress;  
            context.fillStyle = "rgb(255," + Math.round(b.toString()) + ",0)";  
            context.beginPath();  
            context.arc(150, 150, 120, 0, 2 * Math.PI, false);  
            context.fill();  
        }  
      
        queueFrame(draw);  
    });  

Next Time

Next time, we will take on something much more ambitious: We will build an eye-catching animation that reacts to the mouse movement over the canvas. We will also simulate some 3-D effects using 2-D rendering but with 3-D matrix transformations.


Crafter is a modern CMS platform for building modern websites and content-rich digital experiences. Download this eBook now. Brought to you in partnership with Crafter Software.

Topics:

Published at DZone with permission of Josh Anderson, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}