Tuesday, January 31, 2012

Trigonometry for Web Developers, Part Two

Recap of Part One
In part one of this series, we learned about degrees vs. radians, the orientation of the polar coordinate system, and how to use HTML and Javascript to draw an arc.    Continuing to build the pieces needed to display our analog meter example, this article introduces two new trig functions and demonstrates how to use them.   We'll also look at some additional drawing capabilities of the HTML canvas object as we use Javascript to draw tick marks and labels on the face of our meter.

Tick Marks
Our meter will have a small line, or "tick mark", associated with each whole value i.e. 0, 1, 2, and so on.   In order to put these lines on the outside edge of the arc we drew in the first part of this series, we'll need to determine the angle for each line and use that along with the arc's radius to determine the two end points of the line.   Once we have these points, we can easily draw them using the HTML canvas object:

The first task is to deterermine how far apart to place each tick mark.  Since our meter will display values between and including zero and 10, we will need 11 tick marks.    While it might be possible to determine an answer using only (x,y) coordinate pairs, it's actually quite simple to do using polar coordinates.    Recall that the meter extends from an angle of 210 degrees to an angle of 330 degrees.   The difference between these two values is known as the angular distance, and by dividing this value by the number of tick marks we can determine the angular distance between each tick mark.

For each tick mark, we will use a pair of trigonometric functions to determine the end points of the line.   The sine function will be used to determine the y-axis value, and the cosine function will be used to determine the x-axis value for each end of the line.    Either function by itself cannot give the complete answer - it only knows about angles and, in this case, knows nothing about the radius we need.     Additionally, these functions assume that the base (vertex) of the angle is located at  0,0 ... which is seldom the case.    To correct for these issues, we will use the following functions :
y = sine(angle) * radius + offset_y
x = cosine(angle) * radius + offset_x
The sine and cosine functions always return a value between -1.0 and +1.0, so to translate that value into something meaningful we must do two things:   multiply it by the radius of the arc, and add the offset to the center of the circle.   For any given angle, the formulas above will locate a point exactly on our arc - this is one end of the tick mark.   We still need to find the other end of the tick mark, however.   Fortunately, this is quite trivial - we simply adjust the radius slightly by subtracting the size of the tick mark.   Assume that we use "length" to signify the length of the tick mark, the slightly altered formulas then become :
y = sine(angle) * (radius-length) + offset_y
x = cosine(angle) * (radius-length) + offset_x

All of the above logic can be expressed in a relatively small amount of code:
// Tick marks
var numTicks    = 11,
    tickLength  = 10,
    step        = (endAngle - startAngle) / numTicks,
    angle       = startAngle;
ctx.beginPath();
for(var iX = 0; iX <= numTicks; ++iX) {
  ctx.moveTo( centerX + Math.cos(angle) * radius, 
              bottomY + Math.sin(angle) * radius);
  ctx.lineTo( centerX + Math.cos(angle) * (radius - tickLength), 
              bottomY + Math.sin(angle) * (radius - tickLength));
  angle += step;
}
ctx.stroke();
After specifying the number and size of tick marks to be drawn, the angular distance between each tick mark is calculated as we described earlier.    A loop is then used to draw each tick mark by using the moveTo() method to position the drawing cursor along with the lineTo() method to draw a line from the cursor position to the new position.

The complete HTML file for this example can be obtained from here.

Labels
Wouldn't it be nice if each of the tick marks had a label to go with it?   From a user experience perspective, a label facilitates faster comprehension of the data being displayed than would a tick mark alone.   Here's what we want to end up with :
The mechanism for drawing these labels is similar to those used to draw the tick mark.   We'll again make use of the sine and cosine functions to determine the location of each label, and we'll also introduce a couple of HTML canvas methods which greatly simplify drawing the labels.

As with the tick marks, the first step is to determine the angular distance between each label and is done the same way: by dividing the difference between the ending and starting angles by the number of items we wish to display.     Also, as with drawing the tick marks, the polar coordinates (the angle and the radius) and converted to an x,y coordinate pair through the use of cosine and sine functions.     Compare the code for drawing these labels with that for drawing the tick marks - you'll find many similarities:

//-------------------------------------------------
// Value labels
var   textMetrics,
      textHeight = 14,
      label;
angle = startAngle;
step  = (endAngle - startAngle) / numTicks;
for(var iX = 0; iX <= numTicks; ++iX) {
  label = '' + iX;      // Javascript way to force conversion to string type
  textMetrics = ctx.measureText( label );
  ctx.save();
  ctx.translate(centerX + Math.cos(angle) * radius,
                bottomY + Math.sin(angle) * radius);
  ctx.rotate( angle + Math.PI/2);
  ctx.fillText(label, -textMetrics.width/2, textHeight*2);
  ctx.restore();
  angle += step;
}

You'll also notice a few differences.    Since each label is potentially a different size (due to differing widths of the characters within the label), and since each label is rotated slightly differently, this code relies on some HTML canvas object methods to account for these concerns.   The first method, measureText(), is used to obtain the width of the label in pixels (oddly, this method does not return height as yet).      The save() and restore() methods are used to preserve the graphics context state that we alter with the translate() and rotate() methods.

The interesting things happen with the call to the translate() method.   This method is used to move the origin (point at coordinates 0,0) to a new location.   Using the formulas presented earlier, the code uses the translate() method to establish the new origin at the point where the label should be drawn.

Since we also would like the text to be at the same angle as the tick mark, we'll make use of the canvas object's rotate() method.   This method rotates the entire drawing context by the specified amount.   In this case, we'll rotate by the angle plus an additional 90 degrees, or π/2 radians.     Why the additional 90 degrees of rotation?   The additional 90 degrees of rotation is needed to account for the zero angle being on the right side of the polar coordinate circle .

Without adding the additional 90 degrees of rotation, the labels would appear to be drawn as though laying on their side.

The complete HTML file for this example can be found here, and additional background on the HTML canvas methods can be found here.

Up Next: Drawing the Needle
We're a lot closer to having a functional meter now but still have a couple of things left to do next time.   For now, we've covered several key topics:

  • Determining angular distances between a starting and ending angle
  • Converting from polar coordinates to x,y form
  • Use of HTML canvas translate() and rotate() methods

Next time, we'll add the needle to the meter and animate its movements - stay tuned!

Continue onto Part Three...

No comments:

Post a Comment