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. When 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!
N.B. if you understand a tiny bit of basic trig this will be transparent to you. If you don't it will be obscure as. If you are doing this work and you don't understand basic trig learn it. I recommend: https://www.khanacademy.org/math/trigonometry. If that is too hard, start at Khan Academy's basic algebra.
First we're going to deal with lines with arrow head. We want:
The normal user will just specify the source and destination points, and everything else will default.
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. If you need a basic review: https://www.khanacademy.org/math/trigonometry.
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.
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 (this is a different h at this point) distance at angle angle1, the point (topx1,topy1) is equal to
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.
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.
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.
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.
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.
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 = r2.
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!
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:
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:
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.
We set up our defaults.
Draw the arc.
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.
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.
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.