by Paul Murrell
      
         http://orcid.org/0000-0002-3224-8858
      
      
        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.