Curves and Splines
There are numerous types of curves, each with their own unique set of properties. Curves can be defined by a mathematical formula the determines the point (x, y) on the curve as we move along the curve from t = 0 to t = 1. Typically the formula will include a number of control points that are used to guide the curve.
Bézier Curves
The most common curve is the Bézier curve. It is defined by the endpoints P0 and P3, the control points P1 and P2 and the following mathematical formula. Move the slider from t = 0 to t = 1 for a visual demonstration of the curve drawing.
A problem with Bézier curves for the purpose of drawing vines, is that when we combine several curve segments together they don’t always connect smoothly.
B-Splines
Another type of curve is the b-spline. It is defined by the control points P0, P1, P2, P3 and the following mathematical formula.
Unlike the Bézier curve, the b-spline doesn't necessarily pass directly through any of the given points. The control points act as guides that influence the direction of the curve. A useful property of b-splines is that several can easily be combined into a longer, seamless curve. The following JavaScript function draws a b-spline by moving along the curve from t = 0 to t = 1 in increments of 0.1. At each increment, we determine the coordinates of the point on the curve, and draw a line connecting it with the next. We pass the function the context and an array of four points (each point is JavaScript object with x and y properties).
function drawBSpline(context, points) {
var ax, ay, bx, by, cx, cy, dx, dy;
context.beginPath();
for (var t = 0; t < 1; t += 0.1) {
ax = (-points[0].x + 3 * points[1].x - 3 * points[2].x + points[3].x) / 6;
ay = (-points[0].y + 3 * points[1].y - 3 * points[2].y + points[3].y) / 6;
bx = (points[0].x - 2 * points[1].x + points[2].x) / 2;
by = (points[0].y - 2 * points[1].y + points[2].y) / 2;
cx = (-points[0].x + points[2].x) / 2;
cy = (-points[0].y + points[2].y) / 2;
dx = (points[0].x + 4 * points[1].x + points[2].x) / 6;
dy = (points[0].y + 4 * points[1].y + points[2].y) / 6;
context.moveTo(
ax * Math.pow(t, 3) + bx * Math.pow(t, 2) + cx * t + dx,
ay * Math.pow(t, 3) + by * Math.pow(t, 2) + cy * t + dy
);
context.lineTo(
ax * Math.pow(t + 0.1, 3) + bx * Math.pow(t + 0.1, 2) + cx * (t + 0.1) + dx,
ay * Math.pow(t + 0.1, 3) + by * Math.pow(t + 0.1, 2) + cy * (t + 0.1) + dy
);
}
context.stroke();
}
Animated B-Splines
To achieve the effect of growing vines we need to show each segment of the b-spline as it is drawn, rather than draw the complete curve. We'll take a slightly different approach to drawing the curve.
We remove the loop, and pass the value of t as a parameter to the drawing function. When the function is called it will draw only a single segment of the curve starting at t.
At the end of the function, we use requestAnimationFrame to rerun our function to draw the next segment. The function won't be run right away, but rather when the browser is ready to repaint the screen. (This typically happens 60 times per second.) After t has reached 1 we can stop doing this.
function animatedBSpline(context, points, t) {
// Draw curve segment
var ax = (-points[0].x + 3 * points[1].x - 3 * points[2].x + points[3].x) / 6;
var ay = (-points[0].y + 3 * points[1].y - 3 * points[2].y + points[3].y) / 6;
var bx = (points[0].x - 2 * points[1].x + points[2].x) / 2;
var by = (points[0].y - 2 * points[1].y + points[2].y) / 2;
var cx = (-points[0].x + points[2].x) / 2;
var cy = (-points[0].y + points[2].y) / 2;
var dx = (points[0].x + 4 * points[1].x + points[2].x) / 6;
var dy = (points[0].y + 4 * points[1].y + points[2].y) / 6;
context.beginPath();
context.moveTo(
ax * Math.pow(t, 3) + bx * Math.pow(t, 2) + cx * t + dx,
ay * Math.pow(t, 3) + by * Math.pow(t, 2) + cy * t + dy
);
context.lineTo(
ax * Math.pow(t + 0.1, 3) + bx * Math.pow(t + 0.1, 2) + cx * (t + 0.1) + dx,
ay * Math.pow(t + 0.1, 3) + by * Math.pow(t + 0.1, 2) + cy * (t + 0.1) + dy
);
context.stroke();
// Keep going until t = 1
if (t < 1) requestAnimationFrame(function() {
animatedBSpline(context, points, t + 0.1);
});
}
// Kick things off at t = 0
animatedBSpline(context, points, 0);