Variable-width Bézier Curves
© 2025-12-30 Luther Tychonievich
How to draw them with current 2D graphics APIs, needed to supporting board work tablet tools.
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 tablet11 Initially a Wacom Intuos 4, though I’ve purchased several others since then and tried various tools that were extant at that time. 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 JavaScript22 Two used an old Firefox plugin to get pressure data, one backed by SVG and the other by Canvas; both stopped working when Firefox updated their plugin system. The other uses HTML5’s PointerEvent API and is backed by Canvas. and five in D33 Two using GTK3 and Cairo, one using GTK4 and GSK, two using software renderers of my own creation..
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.
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, and 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.
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 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 curve44 So called because the curve can be expressed as a cubic polynomial..
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.
de Casteljau’s Algorithm
- Input
-
- Control points
- Parameter value
- Output
-
- Control points
- Control points
These split the curve at : , , and the point on the curve at parameter .
- Define
- Let
- Let
- Let
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
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.
- Input
-
- Control points
- Parameter value
- Output
- The point on the curve at parameter
- Define
- Initialize
- Initialize
- Initialize
- Repeat for from to :
- Multiply by (makes )
- Multiply by and divide it by (makes )
- Replace with
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 .
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:
- Interval narrowing
-
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 method55 Newton’s method generally has quadratic convergence, unless the root it is finding has multiplicity greater than 1. Newton’s method can diverge if the starting guess is too far from the root; that kind of divergence cannot happen with Bézier interval narrowing.. It can also be used with just the minimum or just the maximum to find only one root.
- Splitting
-
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.
- Deflating
-
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 .
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.
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:
Stroke point pair for curve point (with visualizations)
- Input
-
- A Bézier curve
- A point on the curve
- Output
-
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:
- , the radius of the stroke at this point.
- , the spatial location of this point.
- , the spatial part of .
- , the radius part of
- , a 2D vector perpendicualr to .
- , the squared length of .
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 artifacts66 These artifacts manifest as the cusp being cut off, sometimes with a bow-tie-shaped gap in the stroke. 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 to adjacent sample points contains and the samples are close enough together that additional subdivision is not warranted then a break in the stroke is needed.
When , the radius is changing more rapidly than the point is moving meaning the circle at that point will be completely covered by that of the point just before () or after () it. This is often true for stretches of the curve, within which no strike boundaries should be drawn. Just outside of these omitted parts of the curve the left and right points of the stroke come together, creating natural splits in the stroke that do not require end caps.
Putting all of these together, we can derive the following algorithm:
Variable-width stroke outlines (with visualization)
- Initialize empty stacks of point pairs and Bézier control points.
- Initialize empty lists of left and right segments, were a segment is a list of points
- Initialize a list of end caps with the starting and ending control points.
- Find point pairs for start and for end of Bézier
- If is found (neither $= 0 nor ), append its points as singleton left and right segments
- Repeatedly pick one of the following cases:
If any coordinate of the hodograph is large,
or there’s no and the points are far apart,
or there’s no and the points are far apart,
or and ’s left or right points are far apart:- Split the current Bézier in half; push the back half onto the stack of Bézier control points and replace the current Bézier with the front half.
- Push onto the stack of point pairs.
- Find a new point pair for the end of the current Bézier.
If the convex hull of the hodograph contains the origin
- append ’s points as new singleton left and right segments
- add the current Bézier’s starting and ending control points as end caps
Otherwise
- if was not found but is:
- add ’s points as new singleton left and right segments
- if both and were found
- append to the most-recently-added left and right segment lists
- replace with
- if the stacks are empty, stop looping
- pop a Bézier off the stack as the new current Bézier
- pop a point pair off the stack as the new
- if was not found but is:
- Combine matched left and right segments as polygons; also draw circles for endcaps.
Note that if this outline is rendered as a set of triangles rather than as a full polygon, the points on opposite sides of the stroke are always generated in pairs where the line between them is internal to the stroke, so a triangle strip will always suffice for everything but the circular end caps.
<svg> element with each part of the stroke outline outlined and each sample point indicated by an inward-facing tick.
It is also possible to use cubic Bézier curves instead of line segments to join the same points on the stroke boundaries, using the derivative of each point found to pick the interior control points. This can be useful for exporting to printers and files that support cubic Bézier curves but do not support variable-width strokes. Note, though, that those derivatives approach infinity when the hodograph approaches zero, and if that is not detected and handled strange visual artifacts can result.
It is worth noting at the outset: this section will be noticeably less efficient than the outline technique from the previous section unless it is implemented with a high degree of parallelism in a SIMD processor like a GPU.
If we know the set of pixels that might be covered by the curve (such as a bounding box of the control points padded by their radii), we can check if each pixel individually is inside the stroke. The square of the distance between the pixel and the curve is a Bézier polynomial, as is the square of the radius of the curve at each point; subtracting those two gives a Bézier polynomial that is negative inside the stroke and positive outside of it.
For anti-aliasing, it can be helpful to know if a pixel is within 1 pixel diameter of the border and if so, haw far from the border and in what direction. An approximation of the distance from the edge of the stroke can be found as distance from stroke centerlinestroke radiusstroke radius, though this approximation is only good when the distance from the stroke centerline is close to the stroke radius.
Checking pixels against the stroke (with visualizations)
- Input
-
- A shared sequence of curve’s control points
- A per-thread pixel location
Subtract the pixel location from each control point’s and to make a pixel-relative Bézier curve with control points.
Square the , , and coordinates of the pixel-relative Bézier curve to make one with control points.
Combine the three coordinates with to create a 1-coordinate Bézier polynomial of distance from the stroke’s edge.
Do one of the following:
Find if the pixel is within the stroke by checking if there are any roots of the Bézier polynomial.
I found that 16 de Casteljau invocations was sufficient for visually satisfying results using only interval narrowing, no splitting: either 8 rounds clipping both the low and high ends of the curve or 16 rounds of clipping just one of the two. If the interval has not shrunk away after that many iterations, I render the pixel as being inside the stroke.
Find how far the pixel is from the stroke boundary by
Finding the minimum value of the Bézier polynomial;
Dividing the result by the value of the coordinate of the original curve at the same parameter;
Render as inside if the result is less than , as outside if it is greater than , and anti-aliasing in between, where is the width of one pixel.
Note that square pixels have different widths diagonally than they do axis-aligned; this method does not quite anti-alias the same as super-sampling, instead treating the pixels as if they were circles. Also note that an adjustment of this method could be used to stroke the outline of the stroke.
There are several options for finding the minimum, such as by finding the roots of the hodograph, splitting and using the range of control points of each part as bounds on the minimum, splitting near the minimum control point to find pieces with minima at their ends, and more general extrema finding such as hill climbing. I got good results using Newton hill climbing with Horner’s alorithm to evaluate hodographs, a 1/4 multiplier on the Newton update, 4 Newton iterations, running from 8 evenly-spaced starting points in parallel. These values were chosen by trial and error, not by any particular methodology, and are not guaranteed to converge to the correct answer in all cases.
<svg> element using a <rect> for each pixel.
<canvas> element using a WebGL2 fragment shader.
<canvas> element using a WebGL2 fragment shader.
Since the introduction of Adoboe’s original PostScript specification in 1982, both printable file formats and printer firmware have supported cubic Bézier curves and circular arcs, using them both as boundaries of regions to fill and as paths along which to make fixed-width circular-offset strokes. This includes formats supported directly by many printers (.ps, .eps, .pdf) and formats that have joint display and print roles (.svg, .ai). None of these formats has direct support for any type of variable-width strokes.
I dream of a day when variable-width stroking is added to these formats. It is, alas, only a dream because I have no idea how to make it a reality.
Lacking native representation of variable-width strokes, and noting that the boundaries of such a stroke are not perfectly represented using only cubic Bézier curves and circular arcs, the remaining option for exporting board work to such a file or printer is to approximate the outline of the strokes, as is done by the stroke outline algorithm.
To optimize file size and support smoother-looking boundaries of the strokes, it may be desireable to have fewer boundary points and to connect them with cubic Bézier curves instead of line segments. If the outlines of the curve were cubic polynomials, finding these curves exactly would be as simple as finding the parameter-space derivative of each stroke boundary point, as is done in one of the visualizations of variable-width stroke outlines. However, stroke outlines are strongly non polynomial in parameter space near where stroke direction reverses, requiring additional sampling and variation capping to avoid strong visual artifacts. Reparameterizing or otherwise fairing those regions may yield smoother outlines.
The stroke boundary algorithm could be extended to 3D by generating a ring (instead of a pair) of stroke boundary points at each sample point. I do not know of compelling uses for such a 3D element.
Circular offsets are related to the medial axis. Given a variable-width Bézier stroke that does not intersect itself nor have any points where the hodograph of the radius is greater than the hodograph of the position, the stroke’s medial axis is the Bézier curve.
Because of the relationship between the circumcenters of Delaunary triangulations and medial axes, a triangle strip of the stroke is close to being a Delaunay triangulation and the point of the curve that generated a pair of points is close to the circumcenter of the triangles connecting that point pair.
Implementations of these algorithms are provided with this page (inside <script> elements in its header) and are welcome to be used, with or with modifications, by anyone for any purpose without royalty or attribution, as per the unlicense. See the comment at the beginning of each <script> element for more information.