Building a fast spline renderer with HTML5
I made a spline-drawing program with HTML5 Canvas. This write-up describes some of the methods I used.
You can check out the demo here, and you can find the source code here.
Background
Given points , a cubic spline is a piecewise cubic polynomial that passes through each pair, and has the property that , , and are continuous on .
For any set of points, there are many different cubic splines that interpolate them.
Cubic splines have coefficients, but there are only constraints on their coefficients. We will impose two more constraints, and then we can uniquely define a “natural” cubic spline from the control points.
A natural cubic spline is a cubic spline that also satisfies the condition that .
Representing Parametric Splines
Given a set of points , we want to construct a spline that interpolates them in order.
We can’t do this with splines in a single dimension. So we parameterize, and consider a parametric function that interpolates . You can show that if and are splines for and , then will be a spline for .
There are basically no restrictions on our choice of (except that ). And our choice of dramatically affects the parametric natural spline we get.
We have a couple of nice options for our .
- We can choose . The result will be the only “smooth” ( and continuous) cubic piecewise polynomial that travels through the at one-second intervals. Also, this choice of evenly spaced simplifies some of the spline calculations.
- Or, we can choose our to be proportional to the Euclidean distance between and . The result will be a spline with relatively constant speed. Visually, the generated spline is a very smooth-looking interpolation of .
I implemented both of these – you can use the “MODE” toggle to switch between choices 1 and 2 (2 is the default).
Finding the closest spline segment to a point
Given a point and a parametric cubic spline , we want to find the segment of the spline is closest to.
Since splines are continuous, we can just apply basic optimization techniques:
- For each spline segment , find the critical points of the distance .
- The critical point with the smallest is the absolute minimum for this spline.
- Choose the spline segment with the smallest minimum distance.
But we run into a problem: the squared distance from a cubic function to a point is a sixth-degree polynomial, so the derivative is a fifth-degree polynomial. There is no explicit formula!
Durand-Kerner
We used a simple iterative algorithm, called Durand-Kerner, to find all five roots simultaneously. Notice that some of the roots will be complex numbers – we will discard these.
The Wikipedia article is a pretty good read, but the basic idea of Durand-Kerner is the fact that if
then we can derive that
and notice that a fixed point iteration on the second expression (and its related versions) can solve for all five roots simultaneously. We choose initial complex values . Then we compute
until , , , and for some tolerance .
Pseudocode
function distToCurve(curve: Curve, { x, y }: Point) {
// always check endpoints
const tValuesToCheck = [0.0, 1.0];
// find other local extrema of distance function
const coeffs = getCoefficientsOfSquaredDistance(curve, { x, y });
const roots = durandKernerRoots(derivative(coeffs));
for (const t of roots) {
// only consider t-values within the segment
if (t >= 0.0 && t <= 1.0) {
tValuesToCheck.push(t);
}
}
// check each point and return minimum distance
return Math.min(...tValuesToCheck.map(t => dist(curve.at(t), { x, y }));
}
Other methods
I read an interesting paper about using Newton’s method and quadratic minimization to solve this problem. The authors were using cubic splines to model roads for a driving simulator, and, in real-time, wanted to find where the vehicle was on the road given the coordinates.
Drawing the curve
The last problem is to render the cubic polynomial continuously (without gaps) on a grid of pixels.
The problem is in how we choose which t-values to draw. If we pick 100 evenly-spaced t’s, then our curve might look like this:
Even if we pick 1000 evenly-spaced t’s, our rendered curve will still have small local defects where the velocity is high.
The problem with using a bigger number of evenly-spaced t-values is that it’s a lot of wasted work to draw the points (since when the curve has low velocity, the image of the t-values will overlap). But, if we’re adaptive in how we choose the t-values, we can produce an image that looks like
without doing thousands of unnecessary computations. In other words, we want to find a minimal set of , where , such that, if we plot the points as the 1x1 pixels they live inside, then those pixels will be adjacent.
We will derive an adaptive algorithm to find the . Let’s impose an additional restriction – if we have for all that
then our set of squares will certainly be adjacent. Let’s consider just the -dimension. We want to find a such that
If we approximate from using
then we have that
and
and by the same argument for , we have that the optimal can be approximated with
If the first-derivative approximation for is close (which it “usually” is), the generated will give us connected squares. Even better, the number of generated will be close to the minimal number of that still give us connected squares (since our were chosen so that = 1)
Here is the pseudocode for the adaptive step-size.
let dt = 0;
for (let t = 0; t < 1; t += dt) {
// evaulate x(t), y(t), x'(t), y'(t)...
// update t using adaptive algorithm
dt = Math.min(1 / Math.abs(dx), 1 / Math.abs(dy));
// plot the point
putPixel(x, y, color);
}
Conclusion
I had a lot of fun building this, and I got to learn about a simultaneous root-finding algorithm and I got to figure out a decent way to do adaptive polynomial graphing.
Thanks for reading!