Big Wheel Keep on Turning

With apologies to Tina Turner.

Creating a Canvas

HTML5 and JavaScript can be used to build all manner of business applications. But what about having a little fun? Let’s start looking at some of the things we can do with HTML5 Canvas.

What is a Canvas? It’s simply a drawing context. It has a coordinate system, the ability to set stroke and fill colors, draw shapes, and so on and so forth. Think of it as a programmatic version of MS Paint. And don’t worry if you’re not an artist. I’m not either.

How do we create a Canvas? Simple! Here’s the HTML you’ll need:

<canvas id="circle1Canvas" width="320" height="200">
</canvas>

HTML5 introduces a new <canvas> element. We need an id so we can reference this Canvas with our JavaScript. We also need to establish the size of our canvas using the width and height attributes. That’s it for creating a Canvas!

 

Accessing & Using a Canvas

Accessing a Canvas is simplicity in itself:

circle1Canvas = document.getElementById("circle1Canvas");

There’s not a lot though that you can do with a Canvas. What you really need in order to do anything is get a Context. That’s easy enough to do:

circle1Context = circle1Canvas.getContext("2d");

Don’t worry about the “2d” parameter for the time being. That’s the only option there is for now. It provides a measure of extensibility, for example there’s been talk of there being a “webgl” context that could be created in the future.

 

Ok, now I’ve Got a Context

Now we’re to the point where we can start doing something. But before we do that, let’s go over some basic Context fundamentals. There’s two important concepts we need to know when working with a Context:

  • Path
  • Properties

We start off by beginning a Path, drawing some shapes on the Path and then stroke the path—which forces it to be drawn.

// we have to begin a path in order to do anything
context.beginPath();
// our path will be comprised of an arc
context.arc(/* don't worry about parameters for now */);
// let's fill in the path 
context.fill();
// let's draw the path
context.stroke();

Upon seeing this code hopefully you were wondering how the fill color was determined? The line color? Line style and line width? This is where Properties come in to play.

A Context has the following properties that are of interest to us:

  • lineWidth: measured in pixels
  • lineCap: One of: “butt”, “round”, “square”
  • strokeStyle: #RRGGBB hexadecimal color tuple for line color
  • fillStyle: #RRGGBB hexadecimal color tuple for fill color

From our code snippet above context.stroke() is affected by the following Context properties:

  • lineWidth
  • lineCap
  • strokeStyle

Whereas context.fill() is affected by the property:

  • fillStyle

It’s important to understand that the functions fill() and stroke() apply to the entire path from start to finish, meaning the set of properties for everything in the path will be the same. If you need a different set of properties then you need to start another path. A Context can have several different paths drawn simultaneously as we shall see shortly.

 

Context Geometry

Before we can delve any further into the arc function parameters we need to understand the Context coordinate system. We need to know that because we must specify where to draw the arc.

The Context coordinate system places the origin, point (0,0), at the upper left-hand corner of the Canvas. As we go across the Canvas towards the right the X coordinate value increases. Likewise as we go down the Canvas towards the bottom the Y coordinate value increases. This means the bottom right-most corner of the Canvas has the coordinate (Canvas.width-1, Canvas.height-1).

This is a different coordinate system than you may be accustomed to from mathematics. Means have been provided to deal with this issue, which we’ll delve into in future posts.

 

Let’s Draw an Arc

The parameters for the arc function are:

  1. X coordinate: center of arc
  2. Y coordinate: center of arc
  3. start angle: in radians
  4. end angle: in radians
  5. true/false counter-clockwise/clockwise indicator

The start and end angles behave a little differently than you’d expect, though it makes sense in light of the Canvas coordinate space, where the Y coordinate increases as we go down the Canvas. Essentially the unit circle is flipped “upside down”, with π/2 being at the bottom and 3*(π/2) being at top.

We can easily compensate for this “flipping” of the unit circle by subtracting 2π from both our start and end angles. Then we’ll set the counter-clockwise indicator to true and we’ll have established a “traditional” unit circle—with π/2 being on top!

Now we can create the following simple JavaScript for drawing arcs:

// Draw arc on specified context
// context: context on which we're to draw the arc
// center: point -> x,y location of center of arc
// radius: radius of arc
// startAngle: starting angle in radians
// endAngle: ending angle in radians
// (angles are standard mathematical, going counter-clockwise)
function drawArc(context, center, radius, startAngle, endAngle) {
  context.beginPath();
  context.arc(center.x, center.y, radius, 
      2*Math.PI - startAngle, 2*Math.PI - endAngle, true);
  context.fill();
  context.stroke();
}

One final point (ha!)—the center is of type Point which I’ve defined as:

// define a simple mathematical point
function point(x, y) {
  this.x = x;
  this.y = y;
}

 

Let’s Draw a Wheel (Ok, a Circle)

It’s simple to use our drawArc function to draw a circle. I’ve created a drawCircle function capturing this functionality:

// Draw circle on specified context
function drawCircle(context) {
  center = new point(context.canvas.width/2,
      context.canvas.height/2);
  radius = context.canvas.height/2 - context.lineWidth/2;

  context.lineWidth = 15;
  context.lineCap = "butt";

  // Fill the arcs with Yellow
  context.fillStyle = "#ffff66";
        
  // Draw Red Arc
  context.strokeStyle = "#cc6666";
  drawArc(context, center, radius,
      0 - Math.PI/6, 0.5 * Math.PI);

  // Draw Green Arc
  context.strokeStyle = "#66cc66";
  drawArc(context, center, radius,
      0.5 * Math.PI, Math.PI + Math.PI/6);

  // Draw Blue Arc
  context.strokeStyle = "#6666cc";
  drawArc(context, center, radius,
      Math.PI + Math.PI/6, 0 - Math.PI/6);
}

Hopefully you’ll agree that factoring out the arc drawing code in our drawArc function makes this function easy to understand and maintain.

 

Let’s Get this Wheel Turning

Let’s first look at a function I’ve created to rotate this wheel:

// Rotate circle on specified context, rotating by specified
// angle which is measured in radians
function rotateCircle(context, angle) {
  width = context.canvas.width;
  height = context.canvas.height;
  context.clearRect(0, 0, width, height);
  context.translate(width/2, height/2);
  context.rotate(angle);
  context.translate(-width/2, -height/2);
  drawCircle(context);
}

You may think it’s obvious what the statement

context.rotate(angle);

is doing, but it’s actually quite subtle. It’s not rotating the objects that have been drawn on the Canvas, rather it’s transforming the Canvas coordinates. And it’s rotating the Canvas coordinates around the origin—the point (0,0). But right now the origin is at the upper left-hand corner of the Canvas. So we use the translate() function to move the origin to the center of the Canvas.

The translate() function though doesn’t take absolute X and absolute Y coordinate positions for its parameters. It takes differential X and differential Y coordinates instead. So after rotating the Canvas coordinate plane after translating the origin to the center of the Canvas, we need to remember to translate the origin back to where we found it.

Finally we re-draw our circle. But wait, there’s one more thing we need to do. If we were to re-draw our circle now it would superimpose the new circle over the circle we’ve previously drawn. So we need to erase it first. That’s what the function clearRect() accomplishes. Now we have a circle that appears to have been rotated about its center.

 

How Do We Keep the Wheel Turning?

We need to animate. Animation is a little tricky, though. We need to think about how fast we want our wheel to rotate (revolutions per second). How fluid of an animation we want (frames per second). And then use this information to determine the angle by which we need to rotate our wheel for each frame. Finally, we need to determine what’s going to trigger our animation routine.

The last part is simple. The window object provides the method setInterval(“eval”, interval). The first parameter is a string that will be eval()ed when the corresponding interval, expressed in milliseconds, has expired.

What we want to do then is provide an eval expression causing our rotate() function to get invoked. Here’s my solution:

window.onload=(function() {
  circle1Canvas = document.getElementById("circle1Canvas");
  circle1Animator = getCircle1Animator(circle1Canvas.getContext("2d"));

  function getCircle1Animator(context) {
    _period = 1000; // milliseconds, that's 1 revolution/second
    _fps = 20;      // 20 frames per second
    _angle = ((2*Math.PI)/(_period/1000))/_fps;
    return {
      period: _period, 
      fps: _fps,      
      angle: _angle,
      animate: (function(){
                  rotateCircle(context, _angle);
               })
    }
  }

  setInterval("circle1Animator.animate()", 1000/circle1Animator.fps);
}

You can see here I’ve set the period to 1000 ms (1 second), and the frames per second to 20. This results in relatively smooth animation. You can play with the period and frames per second on your own. You’re cautioned though to not increase the frames per second above 20, as that will make the interval specified to setInterval() drop lower than 50 ms, which is not recommended.

 

All Together Now

This is the entire source code, all in one spot.

<!doctype html>
<html>
  <head>
    <script type="text/javascript">

      // define a simple mathematical point
      function point(x, y) {
        this.x = x;
        this.y = y;
      }


      // Rotate circle on specified context, rotating by specified
      // angle which is measured in radians
      function rotateCircle(context, angle) {
        width = context.canvas.width;
        height = context.canvas.height;
        context.clearRect(0, 0, width, height);
        context.translate(width/2, height/2);
        context.rotate(angle);
        context.translate(-width/2, -height/2);
        drawCircle(context);
      }


      // Draw circle on specified context
      function drawCircle(context) {
        center = new point(context.canvas.width/2, context.canvas.height/2);
        radius = context.canvas.height/2 - context.lineWidth/2;

        context.lineWidth = 15;
        context.lineCap = "butt";

        // Fill the arcs with Yellow
        context.fillStyle = "#ffff66";
        
        // Draw Red Arc
        context.strokeStyle = "#cc6666";
        drawArc(context, center, radius,
            0 - Math.PI/6, 0.5 * Math.PI);

        // Draw Green Arc
        context.strokeStyle = "#66cc66";
        drawArc(context, center, radius,
            0.5 * Math.PI, Math.PI + Math.PI/6);

        // Draw Blue Arc
        context.strokeStyle = "#6666cc";
        drawArc(context, center, radius,
            Math.PI + Math.PI/6, 0 - Math.PI/6);
      }


      // Draw arc on specified context
      // context: context on which we're to draw the arc
      // center: point -> x,y location of center of arc
      // radius: radius of arc
      // startAngle: starting angle in radians
      // endAngle: ending angle in radians
      // (angles are standard mathematical, going counter-clockwise)
      function drawArc(context, center, radius, startAngle, endAngle) {
        context.beginPath();
        context.arc(center.x, center.y, radius, 
            2*Math.PI - startAngle, 2*Math.PI - endAngle, true);
        context.fill();
        context.stroke();
      }
 

      window.onload=(function() {
        circle1Canvas = document.getElementById("circle1Canvas");
        circle1Animator = getCircle1Animator(circle1Canvas.getContext("2d"));

        function getCircle1Animator(context) {
          _period = 1000; // milliseconds, that's 1 revolution/second
          _fps = 20;      // 20 frames per second
          _angle = ((2*Math.PI)/(_period/1000))/_fps;
          return {
            period: _period, 
            fps: _fps,      
            angle: _angle,
            animate: (function(){
                        rotateCircle(context, _angle);
                      })
          }
        }

        setInterval("circle1Animator.animate()", 1000/circle1Animator.fps);

      })

    </script>

    <body>
      <canvas id="circle1Canvas" width="320" height="200">
      </canvas>
    </body>
</html>

More to Explore

Another Look at JavaScript
Canvas Breakout

Advertisements

About taylodl

I'm an enterprise architect/developer who enjoys programming, math, music, physics, martial arts and beer
This entry was posted in HTML5, JavaScript and tagged , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s