I’ve been dreaming and coding curves lately. They play an integral role in a vector drawing API, which I’m writing for Fuse now. “Bezier curves” are so common we might think it’s simple. Attempting to draw a smooth line graph however, quickly disavows one of that notion. This is a short-insight into how one goes from a collection of points, to a smoothly drawn curve on the screen.
See also Rendering an SVG elliptical arc as bezier curves and Stuffing curves into boxes: calculating the bounds in this series.
A line graph and vector API
The key use-case I was implementing was drawing the line for a line graph. From a series of individual points, I needed to draw line segments that connected them.
This really doesn’t sound like a hard task, except we had no vector API yet. That was obviously the first step. I initially tried using “Skia”, so it’d be cross-platform. The size of the resulting library turned us off of it though, along with not having an up-to-date build process for iOS. Instead I turned to using native backends on each platform.
On iOS/OSX the backend is is Core Graphics. The API abstraction I made is quite small, hopefully making it easy to port to Android and Windows. The interface can draw straight lines, bezier curves, and arcs. Of course, Core Graphics doesn’t support arcs, but I’ll get back to that in another article.
With an API in place it should be easy to draw that line graph.
A smooth line graph
Things usually aren’t that simple. As much as straight line segments create an effective graph, it just don’t look as sexy as a smooth continuous line. The API has beziers. It’s just a matter of using them instead of straight lines.
The step from individual points to a series of bezier curves isn’t obvious at first. For each curve we need two end points and two control points. The end points are simply the input points. For the control points we need some calculation that will produce a smooth curve.
What does “smooth” mean? Fortunately in mathematics it means roughly the same as we think in real life. In simple terms it means the curve has a continuous derivative. For a curve this means it has a single tangent at each of our input points.
Fortunately I’d done this work before in our animation engine. You can specify the keyframes for an easing curve. That also needed to be smooth.
There are many different ways to create a smooth curve. I didn’t want to just pick one and go with it so I did a bit of research first. Curves are something mathemeticians love, so there is a lot of research. I came across Kochanek-Bartels splines. These were perfect. The resulting form of the curve can be adjusted by a few parameters called tension, bias, and continuity.
But it’s not a bezier
The Kochanek-Bartels spline is not bezier curve, but a cubic Hermite spline. In the animation engine this was fine since I was doing my own calculations. For the vector API it’s an issue though since we only support bezier splines.
Thankfully most vector APIs now support cubic splines. It wasn’t uncommon to find quadratic only splines before, and the API for them is still there. Anyway, cubic splines and cubic hermite splines are both cubic splines. This implies they are likely just a linear variation of each other.
I wrote down the matrices, stared at them, and grabbed some snack food. Simultaneous equations are usually not hard, just annoying. Of course we also have Maxima to help us. But it’s hard to type and eat at the same time so I searched instead. I found the conversion functions relatively quickly.
1 2 3 4 5 |
public static void CubicHermiteToBezier(float4 p0, float4 p1, ref float4 t1, ref float4 t2) { t1 = p0 + t1/3; t2 = p1 - t2/3; } |
It was surprisingly simple. The key difference is that the hermite form uses a tension vector, whereas the bezier is expressed with control points.
A smooth completion
That was the final piece I neeed to make the Curve
feature in Fuse. Just like Keyframe
in animation I exposed the Kochanek-Bartels
properties: Tension
, Bias
, and Continuity
. They provide a simple way to adjust the shape of the curve. For fine grained control I also allow overriding the tangents at each point in the curve. For convenience, and since the conversion is so easy, I also allow for specifying bezier control points.