Variable-Width Bezier Splines in R

by Paul Murrell

Version 1: Thursday 01 November 2018

Creative Commons License
This document by Paul Murrell is licensed under a Creative Commons Attribution 4.0 International License.

This report describes support for a new type of variable-width line in the 'vwline' package for R that is based on Bezier curves. There is also a new function for specifying the width of a variable-width line based on Bezier curves and there is a new linejoin and lineend style, called "extend", that is available when both the line and the width of the line are based on Bezier curves. This report also introduces a small 'gridBezier' package for drawing Bezier curves in R.

Table of Contents:

1. Bezier curves and Bezier splines

A Bezier curve (specifically a cubic Bezier curve) is a parametric curve based on four control points. The curve begins at the first control point with its slope tangent to the line between the first two control points and the curve ends at the fourth control point with its slope tangent to the line between the last two control points. In the diagram below, the four grey circles are control points and the black line is a Bezier curve relative to those control points.

plot of chunk unnamed-chunk-3

A Bezier spline is a curve that consists of several Bezier curves strung together. For example, in the diagram below, a Bezier spline is constructed from two Bezier curves. There are seven control points, but the fourth control point of the first curve is also the first control point of the second curve.

plot of chunk unnamed-chunk-4

The control points in the previous example have been carefully chosen so that the last two points of the first curve are collinear with the first two points of the second curve. This means that the Bezier spline is smooth overall because the slopes of the two Bezier curves are the same where they meet (at control point four). This does not have to be the case. In the example below, the Bezier spline has a sharp corner at control point four.

plot of chunk unnamed-chunk-5

2. The 'gridBezier' package

The 'grid' package (R Core Team, 2018) provides the grid.bezier function for drawing Bezier curves, but that function has two major drawbacks: the curve is only an X-spline approximation to a Bezier curve; and there is no support for drawing a Bezier spline. In the diagram below, the true Bezier curve is represented by a semitransparent blue line and the X-spline approximation that is produced by grid.bezier is represented by a semitransparent red line.

plot of chunk unnamed-chunk-6

The 'gridBezier' package (Murrell, 2018a) is a new package that provides an improved implementation of Bezier curves via the grid.Bezier function. This function also supports Bezier splines and has an open argument to allow for closed Bezier splines (the first control point is also the last control point in the spline). The following code shows the function in action. The first spline is just a single Bezier curve (through four control points), the second spline is two Bezier curves (seven control points), and the third spline is a closed spline (six control points with the first reused as the last) that has been filled.

x <- c(.2, .2, .8, .8, .8, .2)/3
y <- c(.5, .8, .8, .5, .2, .2), y, r=unit(1, "mm"), gp=gpar(col=NA, fill="grey"))
grid.Bezier(x[1:4], y[1:4], gp=gpar(lwd=3))
grid.Bezier(x[c(1:6, 1)] + 1/3, y[c(1:6, 1)], gp=gpar(lwd=3))
grid.Bezier(x[1:6] + 2/3, y[1:6], open=FALSE, gp=gpar(lwd=3, fill="grey"))
plot of chunk unnamed-chunk-8

3. The grid.offsetBezier() function

The 'vwline' package (Murrell, 2018b) has several functions for drawing variable-width lines (Murrell, 2017b). The function grid.offsetBezier has been added to the 'vwline' package to provide a variable-width line based on a Bezier spline.

The main arguments to grid.offsetBezier are x and y values that specify control points for a Bezier spline and w, which specifies a width for the Bezier spline. The following code describes a Bezier spline, consisting of two Bezier curves, with the width of the spline starting at zero and increasing smoothly to 1cm.

x <- c(.1, .2, .3, .4, .5, .6, .7)
y <- c(.4, .7, .7, .4, .1, .1, .4)
grid.offsetBezier(x, y, w=unit(0:1, "cm"))
plot of chunk unnamed-chunk-10

4. The BezierWidth() function

The width of a variable-width Bezier spline is described by specifying widths at different positions along the line. In the example above, unit(0:1, "cm") is interpreted as 0cm at the start of the line and 1cm at the end of the line.

A more detailed specification of the line width can be given by calling the widthSpline function. This allows us to described the width as an X-spline with control points located in a two-dimensional plane where the x-dimension represents distance along the line and the y-dimension represents the width of the line. The following expression shows a simple call to the widthSpline function that generates an X-spline with control points at the start and end of the line with widths 0cm and 1cm respectively. The diagram below the expression shows the width spline that is generated (the black line) and the curve below that shows the resulting variable-width line when that width specification is applied to a variable-width Bezier spline.

widthSpline(0:1, "cm", d=0:1)
plot of chunk unnamed-chunk-12
plot of chunk unnamed-chunk-13

The next expression shows a more complex widthSpline call that produces a width that decreases then increases. This width spline is applied to a variable-width Bezier spline below the code and diagram.

widthSpline(c(1, 0, 1), "cm", d=c(0, .7, 1), shape=1)
plot of chunk unnamed-chunk-15
plot of chunk unnamed-chunk-16

A new BezierWidth function has been added to 'vwline' to allow the width of a variable-width Bezier spline to be described using a Bezier curve instead of an X-spline. The following code shows how this function can be used to control the line width of a variable-width Bezier spline.

BezierWidth(c(1, 0, 0, 1), "cm", d=c(0, .5, .7, 1))
plot of chunk unnamed-chunk-18
plot of chunk unnamed-chunk-19

5. Line ends and line joins

We saw in the Bezier curves and Bezier splines Section that a Bezier spline may have a sharp corner at the juncture between different Bezier curves (the example is reproduced below).

plot of chunk unnamed-chunk-20

This means that we need to be able to select a line join style for Bezier splines of this sort. We also need to be able to specify a line end style and both of these become especially relevant when we are working with thick lines.

The grid.offsetBezier function has arguments linejoin and lineend (and mitrelimit) to allow control of line join and line end styles. The following code demonstrates three different styles that are offered by the 'vwline' package. The top spline has "mitre" line joins and ends, the middle spline has "round" line joins and ends, and the bottom spline has "bevel" joins and "square" ends. It is also possible to have "butt" ends, which are just shorter versions of "square" ends.

x <- c(.1, .2, .3, .4, .5, .6, .7)
y <- c(.4, .6, .6, .4, .6, .6, .4)
w <- widthSpline(c(2, 8, 8, 10), "mm", d=c(0, .25, .75, 1), shape=0)
grid.offsetBezier(x, y + .3, w,
                  linejoin="mitre", lineend="mitre", mitrelimit=10)
grid.offsetBezier(x, y, w,
                  linejoin="round", lineend="round")
grid.offsetBezier(x, y - .3, w,
                  linejoin="bevel", lineend="square")
plot of chunk unnamed-chunk-21

When the line width has been specified using BezierWidth, for grid.offsetBezier, a new line join and line end style called "extend" has been added. This style is similar to the "mitre" style, but it extends the curve of the line (and the width), rather than just extending the tangent of the line boundaries. In the following code, we draw variable-width lines similar to the previous example, but with a little more curvature. The top spline has "mitre" joins and ends and the bottom spline has "extend" joins and ends.

x <- c(.1, .15, .35, .4, .45, .65, .7)
y <- c(.4, .6, .6, .4, .6, .6, .4)
w <- BezierWidth(c(2, 8, 8, 10), "mm", d=c(0, .25, .75, 1))
grid.offsetBezier(x, y + .25, w,
                  linejoin="mitre", lineend="mitre", mitrelimit=10)
grid.offsetBezier(x, y - .25, w,
                  linejoin="extend", lineend="extend", mitrelimit=10)
plot of chunk unnamed-chunk-22

The mitrelimit has been raised (from the default value of 4) for both splines above so that the ends and joins will be "pointy" rather than being chopped off to "bevel" joins or "square" ends (which is what would happen with a lower mitrelimit). Even so, the "extend" line join in the bottom line has fallen back to a "bevel" join because the extended curve edges at the join do not actually intersect. The right-hand ends of both lines have fallen back to "square" ends because the width of the line is diverging at the right-hand end in both cases. However, notice that the fall-back "square" right-hand end for the "extend" style still has curved edges (whereas the fall-back "square" right-hand end for the "mitre" style has straight edges).

6. Bezier curve offsets

A variable-width Bezier line is drawn using the following algorithm:

  1. Generate points along the Bezier spline (using BezierPoints from the 'gridBezier' package).
  2. Generate normals at each point on the Bezier spline (using BezierNormal from the 'gridBezier' package).
  3. Calculate widths at each point on the Bezier spline (by generating points on the width spline and interpolating to the points on the Bezier spline).
  4. Multiply the width by the normal to get points on the edge of the variable-width line (the Bezier offset curve).
  5. Add line ends and line joins as described in Murrell, 2017a.

The success of this algorithm depends on selecting a good set of points along the Bezier spline in Step 1, so that the Bezier curve, and particularly its offset curve, are smooth.

It is easy to demonstrate a poor set of points, by specifying only 10 points along a Bezier curve, as shown below.

grid.offsetBezier(c(.2, .2, .8, .8), c(.2, .8, .8, .2),
                  w=unit(c(0, 1, 1, 0), "cm"), stepFn=nSteps(10))
plot of chunk unnamed-chunk-23

The grid.offsetBezier function provides the stepFn argument so that we can specify a different function for generating points along the curve. This function is called with arguments x and y (the control points for the curve) and range, which describes the range of t for which we need to generate points.

The default nSteps(100) function does a reasonable job in many cases because it generates 100 steps along the curve. Furthermore, because the steps are in terms of t, there is automatically a higher density of points at places of higher curvature.

Nevertheless, there are still extreme cases where this simple approach will not produce a smooth result. The nSteps approach is also far from optimal as it does not take into account the overall physical size of the curve on the page, so in many situations it is likely to generate more points than are required. A research article from the Anti-Grain Geometry Project discusses several more sophisticated algorithms for calculating step sizes.

7. Edge points on a variable-width Bezier spline

As with all other types of variable-width lines in the 'vwline' package, there is an edgePoints method for grobs that are generated by grid.offsetBezier, which allows us to generate points on the boundary of the variable-width line.

The following code provides a simple demonstration. We generate a variable-width Bezier spline grob, define an "origin", then ask for points on the edge starting from closest point to the origin and travelling half way around the boundary.

x <- c(.1, .2, .3, .4, .5, .6, .7)
y <- c(.4, .7, .7, .4, .1, .1, .4)
ob <- offsetBezierGrob(x, y,
                       w=BezierWidth(c(1, 0, 0, 1), "cm", d=c(0, .5, .7, 1)),
                       gp=gpar(col=NA, fill="grey"))
x0 <- unit(.5, "npc")
y0 <- unit(.9, "npc")
border <- edgePoints(ob, seq(0, .5, length.out=100), x0, y0)
grid.draw(ob), y0, r=unit(1, "mm"), gp=gpar(fill="black"))
grid.segments(x0, y0, border$x[1], border$y[1],
grid.lines(border$x, border$y, gp=gpar(col="red", lwd=3))
plot of chunk unnamed-chunk-24

This edge information can be useful for further drawing or calculations. For example, we can use it to position other graphical output relative to the variable-width line or to export the outline to another graphics system for rendering.

8. Summary

The 'gridBezier' package provides the grid.Bezier function for drawing Bezier curves in R. This is more accurate and more flexible than the grid.bezier function from 'grid'. A grid.offsetBezier function has been added to the 'vwline' package to allow drawing of variable-width Bezier splines. There is also a new BezierWidth function for describing the width of a variable-width line in terms of a Bezier spline. When a variable-width Bezier spline is drawn with a BezierWidth width, there is a new line end and line join style called "extend" that produces a better result than the "mitre" style, especially when the curvature of the line is high.

9. Discussion

Bezier curves are a very common way of describing curves in computer graphics, so it is useful to have the ability to draw them in R. The contribution of the 'gridBezier' function is to draw them properly (compared to the approximation offered by grid.bezier from the 'grid' package).

The 'knotR' package (Hankin, 2017) provides functions for generating points (and derivatives and other things) on cubic Bezier curves, but it does not provide functions for rendering the curves with 'grid'. The implementation of Bezier curve functions is also sufficiently straightforward that it makes more sense to implement them again in 'gridBezier' rather than add 'knotR' as a dependency.

The 'bezier' package (Olsen, 2014) also provides functions for generating points on Bezier curves, but is much more general (e.g., it allows for Bezier curves of any degree, not just cubic Bezier curves). Again, reimplementing the straightforward cubic Bezier calculations made more sense than imposing a package dependency and the 'bezier' package does not provide any support for 'grid' rendering.

Drawing variable-width Bezier splines is supported in other graphics systems. For example, the following MetaPost code produces a variable-width line using the penpos macro to specify a different pen size and rotation at different points on a path.

  z1 = (0, 0);
  z2 = (50, 50);
  z3 = (100, 0);
  penpos1(5, 180);
  penpos2(10, 90);
  penpos3(20, 0);
  penstroke z1e..z2e..z3e;

The width specification is more flexible in 'vwline' and there is more control over line join and line end styles.

Sophisticated drawing programs like Inkscape and Adobe Illustrator provide tools for variable-width lines (called "power stroke" and "width tool" respectively). The main difference of course is that these programs are interactive and mouse driven rather than code-based like R graphics.

The new "extend" line join and line end style was inspired by an Inkscape power stroke proposal (see slide 8). However, only some of that proposal has been implemented in Inkscape (as of Inkscape version 0.92.3). For example, there are "extrapolated" line joins, but nothing similar for line ends.

10. Technical requirements

The examples and discussion in this document relate to version 0.2-1 of the 'vwline' package, and version 1.0-0 of the 'gridBezier' package.

This report was generated within a Docker container (see Resources section below).

11. Resources

How to cite this document

Murrell, P. (2018). "Variable-Width Bezier Splines in R" Technical Report 2018-11, Department of Statistics, The University of Auckland. [ bib ]

12. References

[Hankin, 2017]
Hankin, R. K. S. (2017). knotR: Knot Diagrams using Bezier Curves. R package version 1.0-2. [ bib | http ]
[Murrell, 2017a]
Murrell, P. (2017a). Variable-width line ends and line joins. Technical Report 2017-02, University of Auckland. [ bib ]
[Murrell, 2017b]
Murrell, P. (2017b). Variable-width lines in R. Technical Report 2017-01, University of Auckland. [ bib | http ]
[Murrell, 2018a]
Murrell, P. (2018a). gridBezier: Bezier Curves in grid. R package version 1.0-0. [ bib ]
[Murrell, 2018b]
Murrell, P. (2018b). vwline: Draw variable-width lines. R package version 0.2-0. [ bib ]
[Olsen, 2014]
Olsen, A. (2014). bezier: Bezier Curve and Spline Toolkit. R package version 1.1. [ bib | http ]
[R Core Team, 2018]
R Core Team (2018). R: A Language and Environment for Statistical Computing. R Foundation for Statistical Computing, Vienna, Austria. [ bib | http ]

Creative Commons License
This document by Paul Murrell is licensed under a Creative Commons Attribution 4.0 International License.