When drawing diagrams we often like to point at things with arrows. In the How to use arcTo() on an HTML5 canvas tutorial I showed how arcTo could make a nice arrow head, but cheated, and did the example on a nice horizontal line. In this tutorial we'll generalize it for lines at any angle, and then apply what we've learned to make arrow heads on the end of arcs too. We we go along, you'll notice arcs and lines with arrows in the diagrams. They didn't have them originally, but when I got to the end, I went back and used the new functions drawArcedArrow() and drawArrow() wherever seemed appropriate. Yay!

First we're going to deal with lines with arrow head. We want:

- the arrow head can be on the beginning of the line, the end of the line, or both
- We want to be able to specify the angle θ at which a side of the arrow head comes back from the end of the line, but with a meaningful default
- We want to specify the length of the arrow head, but again with meaningful defaults
- We want to have choices of heads with filled and unfilled, and even the chance to a user to pass in a custom function to use to draw the head. We'll make a filled head with a curved back the default. (The arrows in the diagram are default arrows.)

The normal user will just specify the source and destination points, and everything else will default.

drawArrow(x1,y1,x2,y2,style,which,angle,length)

- x1,y1 - the line of the shaft starts here
- x2,y2 - the line of the shaft ends here
- style - type of head to draw
- 0 - filled head with back a curve drawn with arcTo

n.b. some browsers have an arcTo bug that make this look bizarre - 1 - filled head with back a straight line
- 2 - unfilled but stroked head
- 3 - filled head with back a curve drawn with quadraticCurveTo
- 4 - filled head with back a curve drawn with bezierCurveTo
- function(ctx,x0,y0,x1,y1,x2,y2,style) - a function provided by the user to draw the head. Point (x1,y1) is the same as the end of the line, and (x0,y0) and (x2,y2) are the two back corners. The style argument is the this for the function. An example that just draws little circles at each corner of the arrow head is shown later in this document.

- 0 - filled head with back a curve drawn with arcTo
- which - which end(s) get the arrow
- 0 - neither
- 1 - x2,y2 end
- 2 - x1,y1 end
- 3 - (that's 1+2) both ends

- angle - the angle θ from shaft to one side of arrow head - default π/8 radians (22 1/2°, half of a 45°)
- length - the distance d in pixels from arrow point back along the shaft to the back of the arrow head - default 10px

So we're going to draw a line, and then draw the arrow with the sides at some angle to it. To do that we need to know the angle of the line. To calculate the angle we're going to use some basic trig. I'm not going to teach you about trig, I assume you know it.

So, the angle of a line is given atan(dy/dx), or atan((y2-y1)/(x2-x1)). If we really did it that way, we'd have to be careful about dividing by zero if the x's were the same, and we'd have to figure out which quadrant we were in and add π to the angle if we were in quadrant II or III.

Lucky for us, there's another javascript method in Math that does all that for us, Math.atan2(y,x). It returns the angle α as negative angles (-π <= α <= 0) for quadrants I and II, and as positive angles (0 <- α <= π) for quadrants III and IV.

Consider the line from (x0,y0) to (x1,y1).

atan2(y1-y0,x2-x0) give us its angle, but the line for the arrow head comes back in the opposite direction. To figure out its angle, we need to add θ to the opposite of α. In radians, opposite of α is π + α. So the angle of the top side of the arrow head is π + α + θ and the angle of bottom line of the arrow head would be π + α - θ.

We have the angle of each side of the arrow head, and we have d, but if instead we had h (the hypotenuse) we could easily calcuate the x and y coordinates of the two corners of the back of the arrow barb.

Since the cos(θ) = d/h, then a little algebra tells us that h=d/cos(θ). Now, d is a length, so is always a positive number, the cosine, depending on the angle could be positive or negative. We want the hypotenuse to also be a length so we'll take the absolute value. Math.abs(d/Math.cos(angle)).

Once we have the length of the hypotenuse, then using basic trig, can get the x and y values of the back corner of the top of the arrow head pretty easily. Starting at the point (x2,y2) and going h distance at angle angle1, the point (topx1,topy1) is equal to

(x2+Math.cos(angle1)*h,y2+Math.sin(angle1)*h)

Similarly, given the angle of the bottom side of the arrow head (angle2),
the x and y values of the back corner (botx,boty) of the bottom side of
the arrow head are
(x2+Math.cos(angle2)*h,y2+Math.sin(angle2)*h)

// calculate the angle of the line
var lineangle=Math.atan2(y2-y1,x2-x1);
// h is the line length of a side of the arrow head
var h=Math.abs(d/Math.cos(angle));

Calculate the angle of the line so that we can use it to find the angle
of the top and bottom lines of the arrow head and use *that* to
calculate the (x,y) positions of their ends and draw them.

if(which&1){ // handle head at far end
var angle1=lineangle+Math.PI+angle;
var topx=x2+Math.cos(angle1)*h;
var topy=y2+Math.sin(angle1)*h;

First, as we discussed above, we find the angle of the line by adding Math.PI to the line's angle to get its opposite angle. Then we add the passed in angle of the barb to result. After that, we can easily find the (x,y) coordinates of the corner of the barb by basic trig.

var angle2=lineangle+Math.PI-angle;
var botx=x2+Math.cos(angle2)*h;
var botx=y2+Math.sin(angle2)*h;
toDrawHead(ctx,topx,topy,x2,y2,botx,boty,style);
}

The coordinates of the bottom corner are found just the same way, and then we call another method to actually draw the head, passing the three corners and telling it the style.

if(which&2){ // handle head at near end
var angle1=lineangle+angle;
var topx=x1+Math.cos(angle1)*h;
var topy=y1+Math.sin(angle1)*h;
var angle2=lineangle-angle;
var botx=x1+Math.cos(angle2)*h;
var boty=y1+Math.sin(angle2)*h;
ctx.beginPath();
toDrawHead(ctx,topx,topy,x1,y1,botx,boty,style);
}

Similary, we handle the code for the other end of the arrow, calculating the points and passing them to the head drawing routine. The main difference is that we don't have to add Math.PI to the lineangle, since it's already going the same way at the lines for the sides of the arrow head.

var drawArrow=function(ctx,x1,y1,x2,y2,style,which,angle,d)
{
'use strict';
if(typeof(x1)=='string') x1=parseInt(x1,10);
if(typeof(y1)=='string') y1=parseInt(y1,10);
if(typeof(x2)=='string') x2=parseInt(x2,10);
if(typeof(y2)=='string') y2=parseInt(y2,10);
which=typeof(which)!='undefined'? which:1; // end point gets arrow
angle=typeof(angle)!='undefined'? angle:Math.PI/8;
d =typeof(d) !='undefined'? d :10;
style=typeof(style)!='undefined'? style:3;
// default to using drawHead to draw the head, but if the style
// argument is a function, use it instead
var toDrawHead=typeof(style)!='function'?drawHead:style;

For each of the arguments that can have defaults, we check to see if they are set, and if so we use their values. If not, we set them to default values.

Additionally, for style, we check to see if its a function. If so, we use that for our function to draw heads, otherwise we use our function drawHead. I'm not going to talk about drawHead, since it's just simple applications of canvas drawing routines, but you can look at it for yourself, it's in canvasutilities.js Instead, I'm going to show you how to write your own head drawing routine to pass in.

var headDrawer=function(ctx,x0,y0,x1,y1,x2,y2,style)
{
var radius=3;
var twoPI=2*Math.PI;
ctx.save();
ctx.beginPath();
ctx.arc(x0,y0,radius,0,twoPI,false);
ctx.stroke();
ctx.beginPath();
ctx.arc(x1,y1,radius,0,twoPI,false);
ctx.stroke();
ctx.beginPath();
ctx.arc(x2,y2,radius,0,twoPI,false);
ctx.stroke();
ctx.restore();
}

There's very little to say about this, it just draws a circle at each point. You would use it like drawArrow(x1,y1,x2,y2,headDrawer) (assuming you default the choices about which end, length and angle). You can see it in use in the silly moving diagram below. If you see big dark things it's because the size of the heads on the one with random values has randomly gotten really big. The random angle between the side of the head and the shaft might have gotten bigger than 90 degrees as well. If you wait it will randomly get smaller, or the angle will randomly get smaller, or you can refresh to get the smaller start value.

There's very little to do differently with arcs, we've solved all of
the problems, and just need to figure out the arguments to pass to the
head drawing method. To point it the right way, we need to know the
angle that the end of an arc makes. That's the instantaneous slope of
the curve at that point. If you've had first semester calculus, you
know that you get that from the first derivative of the equation for
the circle. Every point of a circle centered at (a,b) meets the
equation (x-a)^{2} + (y-b)^{2} = r^{2}.

Differentiating implicitly we get: 2(x-a)+2(y-b)dy/dx=0.

Simplifying we get dy/dx=(a-x)/(y-b). Notice that the part with the x goes on the top, even though we usually expect on a line that the slope is the change in y divided by the change in x. It's ok. The math doesn't lie. Later we'll call atan2 to get the angle, and we'll pass these values derived from this application of calculus to it. Who says no one needs calculus!

lineangle=Math.atan2(x-sx,sy-y)

In this case (x,y) will be the center, and (sx,sy) will be the end point on the arc. The atan2 returns the angle of the line tangent to the arc at (sx,sy).

So given an arc, if we can figure out the end points, we should pretty easily be able to figure out what direction to point the arrow head.

We'll be given an arc like this:

drawArcedArrow(ctx,x,y,r,startangle,endangle,anticlockwise,style,which)

- ctx - the 2d drawing context
- x,y - the center of the circle the arc is part of
- r - the radius of the circle
- startangle - angle the arc begins
- endangle - angle the arc ends
- anticlockwise - boolean true if arc is drawn counterclockwise
- style - style of arrow head as in drawArrow above
- which - which end gets the arrow head as in drawArrow above

To draw the arc with the arrow head, we're going to reuse the code that we wrote to draw arrows by calling it. What we'll do is find the angle of the line tangent to the end of the arc, move back 10 pixels from the end and draw a 10 pixel line, with a 10 pixel head. To make sure the line doesn't show up, we set the strokeStyle used to draw lines like this:

strokeStyle='rgba(0,0,0,0)';

rgba lets us set the alpha, or opacity, of the line. The first three values, the r, g, and b, don't matter, because we set the fourth value to 0 which gives complete transparency, the line is invisible. In the diagram, I left the line so you can see that it lies upon the tangent line.

var drawArcedArrow=
function(ctx,x,y,r,startangle,endangle,anticlockwise,style,which,angle,d)
{
'use strict';
style=typeof(style)!='undefined'? style:3;
which=typeof(which)!='undefined'? which:1; // end point gets arrow
angle=typeof(angle)!='undefined'? angle:Math.PI/8;
d =typeof(d) !='undefined'? d :10;

We set up our defaults.

ctx.save();
ctx.beginPath();
ctx.arc(x,y,r,startangle,endangle,anticlockwise);
ctx.stroke();

Draw the arc.

var sx,sy,lineangle,destx,desty;
ctx.strokeStyle='rgba(0,0,0,0)'; // don't show the shaft
var origwhich=which;

Make our arrow shafts invisible, and remember which end of the arc that we'll be adding heads to. The reason we remember, is that the which we pass to the drawArrow() routine from here is always the same. We always draw back along the tangent line from the end of the arc, so we want the source end to be the end that gets the arrow head. That's a which of 2.

if(origwhich&1){ // draw the destination end
sx=Math.cos(startangle)*r+x;
sy=Math.sin(startangle)*r+y;
lineangle=Math.atan2(x-sx,sy-y);
if(anticlockwise){
destx=sx+10*Math.cos(lineangle);
desty=sy+10*Math.sin(lineangle);
}else{
destx=sx-10*Math.cos(lineangle);
desty=sy-10*Math.sin(lineangle);
}
drawArrow(ctx,sx,sy,destx,desty,style,2,angle,d);
}

Just as discussed above, we figure out the end point, (sx,sy), we used that to figure out the angle of the tangent line, lineangle, and then we figure out a point 10 pixels away.

Finally, we draw an arrowed line from the end of the arc to the point 10 pixels away on the tangent line, making sure to tell drawArrow() to point the arrow at the end we came from.

if(origwhich&2){ // draw the origination end
sx=Math.cos(endangle)*r+x;
sy=Math.sin(endangle)*r+y;
lineangle=Math.atan2(x-sx,sy-y);
if(anticlockwise){
destx=sx-10*Math.cos(lineangle);
desty=sy-10*Math.sin(lineangle);
}else{
destx=sx+10*Math.cos(lineangle);
desty=sy+10*Math.sin(lineangle);
}
drawArrow(ctx,sx,sy,destx,desty,style,2,angle,d);
}
ctx.restore();
}

This works just the same as the code for the other end, the only difference is that we use the endangle instead of the start angle to find the end point of the arc, and direction we go to find the point on the tangent line is reversed.

Thanks to Ceason who pointed to a problem where an argument to drawArrow could be a string that only looked like a number. An addition would instead concatenate and the result would be then used as a REALLY big number:) Thanks Ceason! Thanks to Ryan Cook who pointed out that x1=parseInt(x1); should be x1=parseInt(x1,10) so that a leading zero doesn't get the string parsed as octal.

This is a javascript application that uses Date, our drawArrow(), and some other canvas drawing commands. It's not complicated, but it is, like many canvas applications are, tedious and boring;p If you want to know more, just look at the source for this page. The routine is down at the bottom. The clock function gets called once and calls setInterval to call drawclock() once a second. drawclock draws the clock every second. Just read it. It's all pretty obvious. I like the way that each time the second hand hits 12 the minute and hour hands move. It looks really mechanical. There's a slightly improved version in the canvasutilities.js file. It wraps the whole thing in an object you can instantiate and call the start method on. Look at my home page for an example of its use.