Variable-width Bézier curves (2025)
Background
In 2013 I began teaching university classes, and quickly realized that I wanted to be able to do board work - where I write and draw by hand live in front of the class - but that my rooms did not have large enough boards to be clearly seen by the number of students they contained.
So I purchased a graphics tablet[^1]. I was dissatisfied with them all so the summer of 2014 I wrote my own boardwork application, and have kept tweaking and re-writing so that I now have eight completed: three in JavaScript[^2] and five in D[^3].
Throughout this development experience I've been continually frustrated by the lack of an important primitive in 2D graphics libraries: Bézier curves with a pen width coordinate. This post is devoted to that topic.
Pressure Models
There are many ways that a pressure input can be used by a 2D drawing program. Art programs often do quite complicated things with pressure, using it to vary opacity and scale and to pick a pen profile from a pressure-and-distance-varying sample.
That's too much for a simple board-work application: it's too computationally complex, results in too-large files, and can be distracting for those close enough to see it and difficult for those farther back in the room to make out.
Most programs suited to board work use pressure to indicate the width of a stroke with clear edges. This allows much more fluid writing and drawing than a fixed-width stroke system while also being visually crisp, computationally simple, and highly compressible for later file export.
There are three models for pressure-based stroke width that I've seen:
- fixed-axis linear offset
- stroke-aligned linear offset
- circular offset
Fixed-axis linear offset simulates how western calligraphy works when using a flexible nib. It can look fancy and appealing with a skilled scribe, but lacks the desired legibility for board work.
Stroke-aligned linear offsets fill a line perpendicular to stroke direction at each point along the stroke, the width of which is dependent on stroke pressure. This technique is computationally simple and in theory can create an appealing filled stroke shape, but it's very sensitive to the noise and jitter that come with most hand motions and digital stylus discretization because even small offsets of the pen can create arbitrary changes in the stroke direction, with a large visual impact on the resulting stroke.
To counteract this jitter, applications often add a smoothing effect, sometimes marketed as the mass or inertia of the virtual pen; such smoothing can make slow, careful strokes render nicely but removes high-frequency details and makes rapid writing look garbled.
Circular offsets fill a circle at each point along the stroke, the radius of which is dependent on stroke pressure. This technique is robust against jitter, rendering attractive strokes even at high frequencies, but may be computationally more expensive than linear offsets.
My board work tools all use circular offsets; circular offsets are the subject of the rest of this post.
From Samples to Splines
Pen data is delivered by the tablet through the operating system device drivers as a sequence of triples, where is a device-defined pressure input. I generally map using some weighting function based both on desired stroke width and how quickly I want the stroke radius to react to pressure; I find that something between and works well for me, depending on the specific tablet I'm using, where is a stroke width multiplier I connect to a keyboard shortcut and adjust while the application is running.
My first application created a pill shape between each pair of points: a circle at each point, both the same radius, connected by a rectangle the same width as the circles. This is legible and simple to implement, but has bumpy strokes when pressure varies and doesn't zoom in very well.
My second application created a trapezoidal pill shape between each pair of points: a circle at each point with different radii connected by a trapezoid tangent to them both. This looks much smoother than the pill approach, at the expense of slightly more computation.
Both of those approaches connect sample points with straight strokes. But when I am writing quickly I might only have half a dozen total triples for the entire circle of an o, and not equally spaced because my pen accelerates and decelerates as I write, which can make straight lines look noticeably angular. With a high-end graphics tablet and a close-to-native driver I get more samples per second and this effect is reduced, but with cheap tablets or an event-folding API like JavaScript the angularity is visually significant.
Enter the world of splines. Instead of connecting tablet-provided points with straight lines (with variable-width stroking) I connect them with smooth curves; in particular, cubic Bézier curves.
There are many ways to pick the curves based on the sample points, but I am satisfied with the visual quality of picking the slope of the curve at point based on the angle between points and, delaying the drawing by one sample to allow point to be input before drawing the curve between and.
Bézier Curves
Bézier curves are the workhorse of curve drawing on computers. They nicely mix computational efficiency, artist intuition, and numerical stability.
Bézier curves are a specific instance of Bernstein basis polynomials; I'll not further distinguish the two here, referring to them as Bézier even in contexts where Bernstein might be more appropriate.
Each Bézier curve is specified with a sequence of control points. The curve touches the first and last control points and travels towards those in between in order while being smooth and not exiting the convex hull of the control points. The more control points there are, the less influence each one exerts over the curve, with influence being more limited for those in the middle then those near the ends.
In the vast majority of cases, we use four control points, yielding a cubic Bézier curve[^4].
de Casteljau's Algorithm
An important algorithm for Bézier curves is de Casteljau's algorithm which can split a single Bézier curve into two Bézier curves at any point along the original.
Given a, the first and last point are part of the front and back split curves, respectively; then a new point is found along each line segment between adjacent points; the first and last of those new points are part of the front and back split curves, respectively; then a new point is found along each line segment between adjacent points; and so on until there's just one new point, which is part of both split curves.
These split the curve at:,, and the point on the curve at parameter.
If a single value on the curve is wanted, de Casteljau's algorithm can provide it, but uses multiplications to do so. For curves with many control points, Horner's algorithm uses multiplications and divisions to do so, making it more efficient for curves with more than three control points. Horner's algorithm operates in power basis, not Bernstein basis, so we compute Binomial coefficients to convert between those basis during the Horner's algorithm loop.
Hodographs, Separability, and Polynomials
An important property of Bézier curves is that their derivative (the rate at which their coordinates change) is also a Bézier curve, called the curve's hodograph. The hodograph has one fewer control points than the original curve; if the control points of the original are labeled for from to, then the control points of the hodograph are for from to; is the total parameter range of the curve (often taken to be 1).
Bézier curves are separable, in the sense that working with a Bézier curve with control points yields exactly the same results as working with three Bézier polynomials, one with control points, one with, and one with.
A Bézier polynomial can be squared by multiplying its control points by appropriate binomial coefficients; convolving the list of control points by itself; and dividing by appropriate binomial coefficients. For example, a cubic polynomial with control points is multiplied by to, convolved to, and divided by to.
Finding Roots
The roots of a Bézier polynomial can be found by positioning its control points evenly along the axis between 0 and 1 (that arrangement is called an explicit Bézier curve). There are then several tools that can be used to find roots:
- Because Bézier curves remain within the convex hull of their control points, finding where it crosses can be done by repeatedly finding the minimum and maximum where the convex hull crosses the axis and using de Casteljau's algorithm to remove the parts of the curve beyond those bounds. This will converge to the first and last root as quickly as Newton's method[^5]. It can also be used with just the minimum or just the maximum to find only one root.
- Bézier curves have a variation-diminishing property which means (among other things) that a Bézier polynomial can have no more roots than it's control points have sign changes: if the control points switch signs only twice, (e.g.) then the polynomial has at most 2 roots. If the curve has more than 2 roots, de Casteljau's algorithm can be used to split it into curves with ≤ 2 roots.
- If the first control point of a Bézier polynomial is 0, then a deflated polynomial removing that root but keeping any other roots can be defined with control points.
- If the last control point of a Bézier polynomial is 0, then a deflated polynomial removing that root but keeping any other roots can be defined with control points.
Stroking the Curve
Bézier curves are mathematical paths with extend but no width, which means that if we tried to draw them by coloring the pixels they touch then no pixels would be colored. To draw them visibly, we need to instead fill in the pixels that come within a certain distance of the curve. This is true of all Bézier curves, including those used in current graphics APIs, but arguably even more true when the radius of the stroke is a coordinate of the curve itself. The process of finding and drawing these pixels is called stroking the curve.
There are two basic approaches to stroking a curve.
- One option is to find many points along both edges of the stroke, connecting them into a big polygon with vertices close enough together to appear smooth, then fill in the polygon. There are several efficient algorithms for rendering polygons, including polygons with thousands of vertices.
- Another option is to check how far each pixel is from the curve and fill it if that distance is small enough to be part of the stroke. While objectively more work than the polygon approach, requiring significantly more arithmetic computations for all but the smallest curves, this approach is well suited to the highly-parallel SIMD implementations to run on GPUs.
I have occasionally seen a third approach proposed, a variant of the polygon option which uses Bézier curves instead of line segments to bound the stroke. Because these intermediate Bézier curves will themselves need to be rendered using one of the two approaches above, adding this intermediate step seem to be more of a convenience for implementers than an actual computational win.
Although the two approaches (polygonal bounds and pixel checking) are widely implemented for fixed-width strokes, I've not seen many implementations nor clear descriptions for variable-width strokes. The remainder of this post attempts to rectify that.
Stroke Outlines
The basic approach for creating stroke outlines is to find a sequence of points on the curve and for each find the point on the left and right side of the curve that lies on the edge of the stroke. The left-side points thus found are chained together, then connected (possibly via some kind of end cap) to the right-side points in reverse order, and then connected (again with an end cap) back to the left-side points.
For a 2D Bézier curve with radius as a third coordinate, the left- and right-side points of a point on the curve are not simply 90° off from the direction of the curve's motion. Instead, we find those points as follows:
The two points where a radius-circle at that point touches the edge of the stroke. Will fail if there are 0 or ∞ such points. Use de Casteljau's algorithm to find both the point on the curve and the matching point on its hodograph. Derive/compute some useful parts of those values: If or, fail. The left and right points on the stroke edge are If is 0, this is just, the points to the left and right of. When is positive, the stroke is getting wider going forward and the points are pushed back by, finding points on the trapezoid tangent to both and its next Bézier control point,.
There are two cases where this approach will not yield the desired smooth boundaries of the stroke. When, the curve may have a cusp with a sudden change in direction, causing the boundaries of the stroke before and after the cusp to be discontinuous. This discontinuity can be rendered correctly by breaking the stroke into two pieces with end caps on both. To avoid obvious visual artifacts[^6] it is important to have this break even if the exact point where is not sampled. If the convex hull of the control points of the hodograph of the part of the curve that connects t
Comments
No comments yet. Start the discussion.