Wednesday, October 24, 2012

Drawing Curves with the HTML5 Canvas quadraticCurveTo() Function

The HTML5 Canvas API has a built-in bezier function to draw smoothly curved lines.  However, it requires you to specify a control point to draw the line.  If you have points you want to draw the line through, you will need to find the control point to properly draw the line.  Visually, you can see on the left that I have three green points that I want to draw a curve through.   The red point represents the control point required to draw that curve through those points.  The question is: how do I find that point so I can draw the line I want using the Canvas quadraticCurveTo()? Intuitively, it doesn't seem too difficult. You can look at the points and see the relationship between the second point and the control point and see its about twice the distance from the line formed between the first and last points. Additionally, it appears to be on a certain angle from that line.

If we draw some lines on the sample, you can see that the control point is found by translating P2 up and over by L1 and L2.  The question is how do we find L1 and L2?  The problem can be broken down into two steps:

1) L1 is the length of the line that is perpendicular to P1,P3 and runs to P2.

2) L2 is the length of the line from the midpoint of P1,P3 to the intersection found in step 1.

The first step requires us to project P2 onto P1,P3 to find the intersection point.  Once we have that point, the two length are easy to calculate to perform the translation on P2 to determine the control point. Below is the implementation of this process:

function findControlPoint(s1, s2, s3)
var // Unit vector, length of line s1,s3
ux1 = s3.x - s1.x,
uy1 = s3.y - s1.y,
ul1 = Math.sqrt(ux1*ux1 + uy1*uy1)
u1 = { x: ux1/ul1, y: uy1/ul1 },

// Unit vector, length of line s1,s2
ux2 = s2.x - s1.x,
uy2 = s2.y - s1.y,
ul2 = Math.sqrt(ux2*ux2 + uy2*uy2),
u2 = { x: ux2/ul2, y: uy2/ul2 },

// Dot product
k = u1.x*u2.x + u1.y*u2.y,

// Project s2 onto s1,s3
il1 = { x: s1.x+u1.x*k*ul2, y: s1.y+u1.y*k*ul2 },

// Unit vector, length of s2,il1
dx1 = s2.x - il1.x,
dy1 = s2.y - il1.y,
dl1 = Math.sqrt(dx1*dx1 + dy1*dy1),
d1 = { x: dx1/dl1, y: dy1/dl1 },

// Midpoint
mp = { x: (s1.x+s3.x)/2, y: (s1.y+s3.y)/2 },

// Control point on s2,il1
cpm = { x: s2.x+d1.x*dl1, y: s2.y+d1.y*dl1 },

// Translate based on distance from midpoint
tx = il1.x - mp.x,
ty = il1.y - mp.y,
cp = { x: cpm.x+tx, y: cpm.y+ty };

return cp;

Here, points are described by objects with a x and y property (ie. { x: 150, y: 210 }). To use the function, just pass in the points you want used to draw the curve and use the returned control point in the quadraticCurveTo() function:

function drawCurve($canvas, p1, p2, p3)
var ctx = $canvas[0].getContext('2d'),
cp = findControlPoint(p1, p2, p3);

ctx.strokeStyle = 'black';
ctx.strokeWidth = 1;

ctx.moveTo(p1.x, p1.y);

I created a demo in my sandbox to allow moving the points around and visually see the resulting control point and curve.

There is a small caveat to keep in mind - the second point won't always be on the extreme point of the curve. You can see this best by moving the point further to the side by the start/end points. I believe this has to do with the function assuming that the second point is at the midpoint of curve (in terms of the parametric function, this is t=0.5). Unfortunately, you have no control over that selection. One way to work around it is to use path interpolation to draw the curve.

The demo draws the same curve using the PathJS library which will put the second point much closer to the extreme of the curve. This might cause some sharper angles in some cases but will put the second point very close to where the curve changes directions. The image on the left uses the same points as the example above but used PathJS to interpolate the path.

With the examples here, you can now draw bezier curves with whatever information you have available. If you already have the control point, then you're all set. If not, the function presented here will provide an accurate point to use as the control point required by the quadraticCurveTo() function. The same basic approach can be extended to find two control points that are required in the cubicCurveTo() function as well.