by Paul Murrell http://orcid.org/0000-0002-3224-8858
Version 1: Thursday 01 November 2018
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.
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.
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.
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.
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.
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.
library(gridBezier)
x <- c(.2, .2, .8, .8, .8, .2)/3 y <- c(.5, .8, .8, .5, .2, .2) grid.circle(x, 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"))
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.
library(vwline)
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"))
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)
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)
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))
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).
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")
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)
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).
A variable-width Bezier line is drawn using the following algorithm:
BezierPoints
from the 'gridBezier' package).
BezierNormal
from the 'gridBezier' package).
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))
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.
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) grid.circle(x0, y0, r=unit(1, "mm"), gp=gpar(fill="black")) grid.segments(x0, y0, border$x[1], border$y[1], gp=gpar(lty="dotted")) grid.lines(border$x, border$y, gp=gpar(col="red", lwd=3))
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.
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.
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.
beginfig(1); z1 = (0, 0); z2 = (50, 50); z3 = (100, 0); penpos1(5, 180); penpos2(10, 90); penpos3(20, 0); penstroke z1e..z2e..z3e; endfig; end
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.
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).
Murrell, P. (2018). "Variable-Width Bezier Splines in R" Technical Report 2018-11, Department of Statistics, The University of Auckland. [ bib ]
This document
by Paul
Murrell is licensed under a Creative
Commons Attribution 4.0 International License.