Offsetting Lines and Polygons in 'grid'

by Jack Wong and Paul Murrell http://orcid.org/0000-0002-3224-8858

Version 1: Wednesday 14 December 2022


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


This document describes new functions in the 'gridGeometry' package for R that generate offset regions and Minkowski Sums for lines and polygons.

Table of Contents:

1. Introduction

The 'gridGeometry' package (Murrell, 2019; Murrell, 2022a) provides a 'grid' interface to the polyclip() function from the 'polyclip' package (Johnson and Baddeley, 2019). This means that we can generate and draw a complex shape in 'grid' by combining simple 'grid' shapes. For example, the following code defines a collection of simple circles: a large circle and several smaller circles that are arranged around the boundary of the large circle.

library(grid)
t <- seq(0, 2*pi, length.out=13)[-1]
x <- .5 + .3*cos(t)
y <- .5 + .3*sin(t)
bigC <- circleGrob(r=.3, gp=gpar(fill=NA))
littleC <- circleGrob(x, y, r=.05, gp=gpar(fill=NA))
grid.draw(bigC)
grid.draw(littleC)
plot of chunk unnamed-chunk-6

The next code generates a more complex "gear" shape by subtracting the smaller circles from the large circle using the grid.polyclip() function from the 'gridGeometry' package.

library(gridGeometry)
grid.polyclip(bigC, littleC, "minus",
              gp=gpar(fill="grey"))
plot of chunk unnamed-chunk-8

This report describes an extension of the 'gridGeometry' package to provide interfaces to three other functions from the 'polyclip' package: polyoffset(), polylineoffset(), and polyminkowski().

2. Drawing offset regions

Two main functions have been added to 'gridGeometry' for drawing offset regions: grid.polyoffset() and grid.polylineoffset().

The grid.polyoffset() function takes a closed shape, such as a rectangle or circle, and generates an offset region based on a fixed offset given by delta. For example, the following code defines a rectangle and then draws an offset region based on that rectangle with an offset of 5mm. The original rectangle is shown as a dotted outline.

r <- rectGrob(width=.5, height=.5, gp=gpar(lty="dotted", fill=NA))
grid.polyoffset(r, unit(5, "mm"), gp=gpar(fill="grey"))
grid.draw(r)
plot of chunk unnamed-chunk-9

The offset can also be negative, as shown in the following code.

grid.polyoffset(r, unit(-5, "mm"), gp=gpar(fill="grey"))
grid.draw(r)
plot of chunk unnamed-chunk-10

The grid.polylineoffset() function takes an open shape, such as a line or curve, and draws an offset region based on a given delta. For example, the following code draws an offset region based on a series of straight line segments (the original line segments are shown as a dotted line).

l <- linesGrob(c(.25, .25, .75, .75), c(.25, .75, .75, .25),
               gp=gpar(lty="dotted"))
grid.polylineoffset(l, unit(5, "mm"), gp=gpar(fill="grey"))
grid.draw(l)
plot of chunk unnamed-chunk-11

The new offset region functions in 'gridGeometry' can be combined with the existing grid.polyclip() function to produce an even wider variety of shapes. For example, the following code generates a "square donut" by starting with a rectangle, generating two offset regions (one with positive delta and one with negative delta), and then subtracting the inner offset region from the outer offset region.

grid.newpage()
outer <- polyoffsetGrob(r, unit(5, "mm"))
inner <- polyoffsetGrob(r, unit(-5, "mm"))
grid.polyclip(outer, inner, "minus", gp=gpar(fill="grey"))
grid.draw(r)
plot of chunk unnamed-chunk-12

End types and join types

In all of the examples so far, the offset regions have been created with rounded corners, but there are other options. The jointype argument can be used to select the corner style:

With grid.polylineoffset() we can control the shape of corners with jointype as above and we can control the shape of the offset region at the ends of the line or curve with the endtype argument. .

There are also two unusual end types that join the end of the line to the start of the line, in which case there are no longer ends to worry about and only the join type matters.

3. Complex offset regions

This section looks at some more complicated scenarios for calculating offset regions and some lower-level functions that have been added to 'gridGeometry'.

Offset regions for multiple shapes

The examples from the previous section have only considered the situation where we are generating an offset region based on a single shape. In this section we consider what happens when we have more than one shape. For example, in the following code we create a 'grid' grob that draws two (overlapping) circles.

circles <- circleGrob(1:2/3, r=.3, gp=gpar(fill=NA))
grid.draw(circles)
plot of chunk unnamed-chunk-21

The first important point is that 'gridGeometry' will "reduce" a grob that draws multiple shapes before sending it to 'polyclip'. This happens automatically, but the following code explicitly performs the reduce, using the reduceGrob() function, to show what normally happens by default. In this case, we get a single shape that is the union of the two circles (drawn as a dotted outline so that we can add it to the offset region examples below).

circleUnion <- reduceGrob(circles, gp=gpar(lty="dotted", fill=NA))
grid.draw(circleUnion)
plot of chunk unnamed-chunk-22

When we calculate an offset region for the two circles we get a single region (shown in grey below). This makes sense because we are actually calculating an offset of the union of the two circles (shown as a dotted outline).

grid.polyoffset(circles, unit(5, "mm"), gp=gpar(fill="grey"))
grid.draw(circleUnion)
plot of chunk unnamed-chunk-23

If we use a negative offset, we still get a single region, again because we are offsetting the union of the circles.

grid.polyoffset(circles, unit(-5, "mm"), gp=gpar(fill="grey"))
grid.draw(circleUnion)
plot of chunk unnamed-chunk-24

However, if we increase the (negative) offset, it is possible to end up with two regions as the result, even though we are offsetting a single region.

grid.polyoffset(circles, unit(-10, "mm"), gp=gpar(fill="grey"))
grid.draw(circleUnion)
plot of chunk unnamed-chunk-25

We can also control how the circles are reduced. For example, the following code explicitly reduces the circles using a "flatten" operator, which keeps the circles as two separate circles.

circleFlatten <- reduceGrob(circles, op="flatten",
                            gp=gpar(lty="dotted", fill=NA))
grid.draw(circleFlatten)
plot of chunk unnamed-chunk-26

When we specify an explicit reduce="flatten" to grid.polyoffset(), we can end up sending multiple shapes to 'polyclip'.

The following code generates an offset region based on two separate circles. The result is a single region, just the same as when we offset the union of the circles, because the offset regions of the two circles overlap.

grid.polyoffset(circles, unit(5, "mm"), reduce="flatten", gp=gpar(fill="grey"))
grid.draw(circleFlatten)
plot of chunk unnamed-chunk-27

However, if we use a negative offset, the result is two separate regions (because the offset regions of the two circles do not overlap).

grid.polyoffset(circles, unit(-5, "mm"), reduce="flatten", gp=gpar(fill="grey"))
grid.draw(circleFlatten)
plot of chunk unnamed-chunk-28

In summary, it is possible to start with either a single shape or multiple shapes and end up with either a single shape or multiple shapes, depending on how (or whether) we reduce the input shapes and on whether the resulting offset regions (if there are more than one) overlap each other.

4. Low-level offset regions

In the previous sections, we have demonstrated functions that allow us to provide 'grid' grobs as input and obtain grobs as output. The 'gridGeometry' package also provides a lower-level interface that just works with coordinates rather than grobs.

As an example of working at a lower level, consider the simple circle shape shown below.

c <- circleGrob(r=.3)
grid.draw(c)
plot of chunk unnamed-chunk-29

Suppose we want to create a "donut" shape based on this circle. We need an offset larger than the circle and an offset smaller than the circle, but because the circle is a closed shape, we can only use grid.polyoffset() to produce either a larger circle or a smaller circle.

The following code uses the lower-level xyListFromGrob() function to generate an xy-list (a list of sets of x/y coordinates) from the circle grob.

pts <- xyListFromGrob(c)
pts
  [[1]]
  x: 1.6 1.598816 1.595269 ... [100 values]
  y: 1 1.037674 1.0752 ... [100 values]

The result is a set of points around a circle.

grid.points(pts[[1]]$x, pts[[1]]$y, default.units="in", pch=".")
plot of chunk unnamed-chunk-31

Now that we are dealing with a lower-level set of coordinates, rather than higher-level grob, we can treat these coordinates as an open shape rather than a closed shape. For example, the following code uses the xyListToLine() function to generate a series of line segments that go most of the way, but not all of the way, around the circle (there is a small gap on the right of the circle).

grid.draw(xyListToLine(pts))
plot of chunk unnamed-chunk-32

The next code also treats the coordinates as an open shape, but it calls the polylineoffset() function to generate a set of offset coordinates. This creates an offset on both sides of the line and, because we specify endtype="closedline", does so all of the way around the circle. The result is still just coordinates, but it is now a list of 2 sets of x/y coordinates (the outer edge of the donut and the inner edge of the donut).

offsetpts <- polylineoffset(pts, delta=unit(2, "mm"), endtype="closedline")
offsetpts
  [[1]]
  [[1]]$x
    [1] 1.0401476 1.0450844 1.0826100 1.0875176 1.1247465 1.1296054 1.1663906 1.1711817 1.2073780
   [10] 1.2120825 1.2475470 1.2521462 1.2867391 1.2912149 1.3247995 1.3291342 1.3615781 1.3657546
   [19] 1.3969297 1.4009316 1.4307148 1.4345262 1.4628001 1.4664060 1.4930589 1.4964450 1.5213718
   [28] 1.5245249 1.5476271 1.5505347 1.5717212 1.5743717 1.5935590 1.5959420 1.6130542 1.6151604
   [37] 1.6301300 1.6319510 1.6447190 1.6462476 1.6567636 1.6579937 1.6662162 1.6671431 1.6730396
   [46] 1.6736595 1.6772067 1.6775173 1.6787013 1.6787013 1.6775173 1.6772067 1.6736595 1.6730396
   [55] 1.6671431 1.6662162 1.6579937 1.6567636 1.6462476 1.6447190 1.6319510 1.6301300 1.6151604
   [64] 1.6130542 1.5959420 1.5935590 1.5743717 1.5717212 1.5505347 1.5476271 1.5245249 1.5213718
   [73] 1.4964450 1.4930589 1.4664060 1.4628001 1.4345262 1.4307148 1.4009316 1.3969297 1.3657546
   [82] 1.3615781 1.3291342 1.3247995 1.2912149 1.2867391 1.2521462 1.2475470 1.2120825 1.2073780
   [91] 1.1711817 1.1663906 1.1296054 1.1247465 1.0875176 1.0826100 1.0450844 1.0401476 1.0024733
  [100] 0.9975267 0.9598524 0.9549156 0.9173900 0.9124824 0.8752535 0.8703946 0.8336094 0.8288183
  [109] 0.7926220 0.7879175 0.7524530 0.7478538 0.7132609 0.7087851 0.6752005 0.6708658 0.6384219
  [118] 0.6342454 0.6030703 0.5990684 0.5692852 0.5654738 0.5371999 0.5335940 0.5069411 0.5035550
  [127] 0.4786282 0.4754751 0.4523729 0.4494653 0.4282788 0.4256283 0.4064410 0.4040580 0.3869458
  [136] 0.3848396 0.3698700 0.3680490 0.3552810 0.3537524 0.3432364 0.3420063 0.3337838 0.3328569
  [145] 0.3269604 0.3263405 0.3227933 0.3224827 0.3212987 0.3212987 0.3224827 0.3227933 0.3263405
  [154] 0.3269604 0.3328569 0.3337838 0.3420063 0.3432364 0.3537524 0.3552810 0.3680490 0.3698700
  [163] 0.3848396 0.3869458 0.4040580 0.4064410 0.4256283 0.4282788 0.4494653 0.4523729 0.4754751
  [172] 0.4786282 0.5035550 0.5069411 0.5335940 0.5371999 0.5654738 0.5692852 0.5990684 0.6030703
  [181] 0.6342454 0.6384219 0.6708658 0.6752005 0.7087851 0.7132609 0.7478538 0.7524530 0.7879175
  [190] 0.7926220 0.8288183 0.8336094 0.8703946 0.8752535 0.9124824 0.9173900 0.9549156 0.9598524
  [199] 0.9975267 1.0024733
  
  [[1]]$y
    [1] 0.3224827 0.3227933 0.3263405 0.3269604 0.3328569 0.3337838 0.3420063 0.3432364 0.3537524
   [10] 0.3552810 0.3680490 0.3698700 0.3848396 0.3869458 0.4040580 0.4064410 0.4256283 0.4282788
   [19] 0.4494653 0.4523729 0.4754751 0.4786282 0.5035550 0.5069411 0.5335940 0.5371999 0.5654738
   [28] 0.5692852 0.5990684 0.6030703 0.6342454 0.6384219 0.6708658 0.6752005 0.7087851 0.7132609
   [37] 0.7478538 0.7524530 0.7879175 0.7926220 0.8288183 0.8336094 0.8703946 0.8752535 0.9124824
   [46] 0.9173900 0.9549156 0.9598524 0.9975267 1.0024733 1.0401476 1.0450844 1.0826100 1.0875176
   [55] 1.1247465 1.1296054 1.1663906 1.1711817 1.2073780 1.2120825 1.2475470 1.2521462 1.2867391
   [64] 1.2912149 1.3247995 1.3291342 1.3615781 1.3657546 1.3969297 1.4009316 1.4307148 1.4345262
   [73] 1.4628001 1.4664060 1.4930589 1.4964450 1.5213718 1.5245249 1.5476271 1.5505347 1.5717212
   [82] 1.5743717 1.5935590 1.5959420 1.6130542 1.6151604 1.6301300 1.6319510 1.6447190 1.6462476
   [91] 1.6567636 1.6579937 1.6662162 1.6671431 1.6730396 1.6736595 1.6772067 1.6775173 1.6787013
  [100] 1.6787013 1.6775173 1.6772067 1.6736595 1.6730396 1.6671431 1.6662162 1.6579937 1.6567636
  [109] 1.6462476 1.6447190 1.6319510 1.6301300 1.6151604 1.6130542 1.5959420 1.5935590 1.5743717
  [118] 1.5717212 1.5505347 1.5476271 1.5245249 1.5213718 1.4964450 1.4930589 1.4664060 1.4628001
  [127] 1.4345262 1.4307148 1.4009316 1.3969297 1.3657546 1.3615781 1.3291342 1.3247995 1.2912149
  [136] 1.2867391 1.2521462 1.2475470 1.2120825 1.2073780 1.1711817 1.1663906 1.1296054 1.1247465
  [145] 1.0875176 1.0826100 1.0450844 1.0401476 1.0024733 0.9975267 0.9598524 0.9549156 0.9173900
  [154] 0.9124824 0.8752535 0.8703946 0.8336094 0.8288183 0.7926220 0.7879175 0.7524530 0.7478538
  [163] 0.7132609 0.7087851 0.6752005 0.6708658 0.6384219 0.6342454 0.6030703 0.5990684 0.5692852
  [172] 0.5654738 0.5371999 0.5335940 0.5069411 0.5035550 0.4786282 0.4754751 0.4523729 0.4494653
  [181] 0.4282788 0.4256283 0.4064410 0.4040580 0.3869458 0.3848396 0.3698700 0.3680490 0.3552810
  [190] 0.3537524 0.3432364 0.3420063 0.3337838 0.3328569 0.3269604 0.3263405 0.3227933 0.3224827
  [199] 0.3212987 0.3212987
  
  
  [[2]]
  [[2]]$x
    [1] 0.9672723 0.9346737 0.9023329 0.8703776 0.8389339 0.8081258 0.7780749 0.7488999 0.7207158
   [10] 0.6936340 0.6677612 0.6431997 0.6200463 0.5983923 0.5783234 0.5599186 0.5432506 0.5283852
   [19] 0.5153810 0.5042894 0.4951541 0.4880113 0.4828890 0.4798075 0.4787790 0.4798075 0.4828890
   [28] 0.4880113 0.4951541 0.5042894 0.5153810 0.5283852 0.5432506 0.5599186 0.5783234 0.5983923
   [37] 0.6200463 0.6431997 0.6677612 0.6936340 0.7207158 0.7488999 0.7780749 0.8081258 0.8389339
   [46] 0.8703776 0.9023329 0.9346737 0.9672723 1.0000000 1.0327277 1.0653263 1.0976671 1.1296224
   [55] 1.1610661 1.1918742 1.2219251 1.2511001 1.2792842 1.3063660 1.3322388 1.3568003 1.3799537
   [64] 1.4016077 1.4216766 1.4400814 1.4567494 1.4716148 1.4846190 1.4957106 1.5048459 1.5119887
   [73] 1.5171110 1.5201925 1.5212210 1.5201925 1.5171110 1.5119887 1.5048459 1.4957106 1.4846190
   [82] 1.4716148 1.4567494 1.4400814 1.4216766 1.4016077 1.3799537 1.3568003 1.3322388 1.3063660
   [91] 1.2792842 1.2511001 1.2219251 1.1918742 1.1610661 1.1296224 1.0976671 1.0653263 1.0327277
  [100] 1.0000000
  
  [[2]]$y
    [1] 0.4798075 0.4828890 0.4880113 0.4951541 0.5042894 0.5153810 0.5283852 0.5432506 0.5599186
   [10] 0.5783234 0.5983923 0.6200463 0.6431997 0.6677612 0.6936340 0.7207158 0.7488999 0.7780749
   [19] 0.8081258 0.8389339 0.8703776 0.9023329 0.9346737 0.9672723 1.0000000 1.0327277 1.0653263
   [28] 1.0976671 1.1296224 1.1610661 1.1918742 1.2219251 1.2511001 1.2792842 1.3063660 1.3322388
   [37] 1.3568003 1.3799537 1.4016077 1.4216766 1.4400814 1.4567494 1.4716148 1.4846190 1.4957106
   [46] 1.5048459 1.5119887 1.5171110 1.5201925 1.5212210 1.5201925 1.5171110 1.5119887 1.5048459
   [55] 1.4957106 1.4846190 1.4716148 1.4567494 1.4400814 1.4216766 1.4016077 1.3799537 1.3568003
   [64] 1.3322388 1.3063660 1.2792842 1.2511001 1.2219251 1.1918742 1.1610661 1.1296224 1.0976671
   [73] 1.0653263 1.0327277 1.0000000 0.9672723 0.9346737 0.9023329 0.8703776 0.8389339 0.8081258
   [82] 0.7780749 0.7488999 0.7207158 0.6936340 0.6677612 0.6431997 0.6200463 0.5983923 0.5783234
   [91] 0.5599186 0.5432506 0.5283852 0.5153810 0.5042894 0.4951541 0.4880113 0.4828890 0.4798075
  [100] 0.4787790

As a final step, we can use xyListToPath() to convert the coordinates back to a grob for drawing.

donut <- xyListToPath(offsetpts, gp=gpar(fill="grey"))
grid.draw(donut)
plot of chunk unnamed-chunk-34

5. Applications

As with the original grid.polyclip() function, the new functions in 'gridGeometry' have value as an additional tool for creating shapes in R graphics. This is potentially useful because new (graphical) tools help us to think about (graphical) problems in new ways and allow us to come up with new (graphical) solutions.

As a simple example, consider the more complicated "gear" shape shown below. We saw in the introduction how easy it is to create the "teeth" around the outside of the gear shape, but how can we create the wedge-shaped holes in the interior of the gear shape, particularly with rounded corners on each wedge?

a more complex gear shape

With the concept of offset regions (with rounded corners) in our toolbox, it is easier to think of ways to generate this shape. To start with, we will use grid.polyclip() to subtract horizontal and vertical bars from a medium-sized circle. The following code shows the circles and bars that we are working with.

medC <- circleGrob(r=.2, gp=gpar(fill=NA))
hbar <- rectGrob(height=.1, gp=gpar(fill=NA))
vbar <- rectGrob(width=.1, gp=gpar(fill=NA))
grid.draw(medC)
grid.draw(hbar)
grid.draw(vbar)
plot of chunk paths

The following code subtracts the bars from the medium-sized circle, which leaves four wedges.

wedges <- polyclipGrob(medC, gList(hbar, vbar), "minus",
                         gp=gpar(fill="grey"))
grid.draw(wedges)
plot of chunk wedges

The next code generates offset regions based on the four wedges. This produces the curved corners on the wedges.

roundedWedges <- polyoffsetGrob(wedges, unit(1, "mm"),
                                gp=gpar(fill="grey"))
grid.draw(roundedWedges)
plot of chunk unnamed-chunk-35

The final step is to subtract those wedges from the large circle that we started with in the introduction (as well as subtracting the small circles to make the teeth).

grid.polyclip(bigC, gList(littleC, roundedWedges), "minus",
              gp=gpar(fill="grey"))
plot of chunk fullcog

The first author has also made use of the new offset facilities in 'gridGeometry' to create shapes for an acoustic similation app called AiHear (Beresford and Wong, 2022). For example, the offset facilities made it easy to create an "H" shape, with rounded ends on the verticals, as shown below.

The relatively complex final outline is not described directly. Instead, a simple series of straight lines is described (shown in grey below) and then the final result is obtained from the union of the offset regions based on the straight lines.

l1 <- linesGrob(x=rep(0.3, 2), y=c(0.25, 0.75), gp=gpar(col="grey"))
l2 <- linesGrob(x=rep(0.7, 2), y=c(0.25, 0.75), gp=gpar(col="grey"))
l3 <- linesGrob(x=c(0.3, 0.7), y=rep(0.5, 2), gp=gpar(col="grey"))
l <- gList(l1, l2, l3)
grid.draw(l)
grid.polylineoffset(l, 0.05, endtype = "openround", jointype = "round",
                    gp=gpar(fill=NA))
plot of chunk H

This is a nice example of a simple solution to a problem only becoming apparent once the idea of offset regions has been introduced. The image below shows the "H" that was created in R being used in the AiHear app.

An example of a statistical graphics application is the creation of non-standard data symbols. For example, the code below draws "donut" data symbols in a 'ggplot2' plot (Wickham, 2016; the 'gggrid' package is used to combine low-level 'grid' drawing with the 'ggplot2' plot; Murrell, 2022b). This code builds a "donutGrob" object for each data point and defines a makeContent() method for "donutGrob"s so that the calculation of offset regions happens when the data points are drawn (rather than when they are created).

donutGrob <- function(x, y) {
    gTree(x=x, y=y, cl="donutGrob")
}
makeContent.donutGrob <- function(x) {
    circles <- mapply(circleGrob, x$x, x$y, MoreArgs=list(r = unit(1.5, "mm")),
                      SIMPLIFY=FALSE)
    pts <- lapply(circles, xyListFromGrob)
    offsetpts <- lapply(pts, polylineoffset, delta=unit(.5, "mm"),
                        endtype="closedline")
    donuts <- lapply(offsetpts, xyListToPath, gp=gpar(fill="white"))
    setChildren(x, do.call(gList, donuts))
}
donut <- function(data, coords) {
    donutGrob(coords$x, coords$y)
}
library(gggrid)
ggplot(mtcars) +
    grid_panel(donut, aes(x=disp, y=mpg, colour=as.factor(am), fill=hp))
plot of chunk unnamed-chunk-38

The difference between drawing donut symbols and just open circles using a thick line width is that the donuts have a separate (interior and exterior) border and fill. This is emphasised in the following variation where the donut borders are coloured to represent whether cars are manual (blueish) or automatic (reddish) and the donut fills represent the horsepower (dark to light). The manual car with the highest horsepower is clearly visible.

donutGrob <- function(x, y, colour, fill) {
    gTree(x=x, y=y, colour=colour, fill=fill, cl="donutGrob")
}
cook <- function(offsetpts, colour, fill) {
    xyListToPath(offsetpts, gp=gpar(col=colour, fill=fill, lwd=2))
}
makeContent.donutGrob <- function(x) {
    circles <- mapply(circleGrob, x$x, x$y, MoreArgs=list(r = unit(1.5, "mm")),
                      SIMPLIFY=FALSE)
    pts <- lapply(circles, xyListFromGrob)
    offsetpts <- lapply(pts, polylineoffset, delta=unit(.5, "mm"),
                        endtype="closedline")
    donuts <- mapply(cook, offsetpts, x$colour, x$fill, SIMPLIFY=FALSE)
    setChildren(x, do.call(gList, donuts))
}
donut <- function(data, coords) {
    donutGrob(coords$x, coords$y, coords$colour, coords$fill)
}
ggplot(mtcars) +
    grid_panel(donut, aes(x=disp, y=mpg, colour=as.factor(am), fill=hp))
plot of chunk unnamed-chunk-40

6. Minkowski Sums

In addition to the functions for generating offset regions, support has also been added to the 'gridGeometry' package for generating Minkowski Sums. The main function is grid.minkowski(), which takes two shapes, a "pattern" and a "path", and generates a new shape by "adding" the pattern to the path.

For certain cases, a Minkowski Sum generates a result just like an offset region. For example, the following code adds a circle pattern to a triangular path. This is done by adding the circle to every location on the triangle. The result (black outline with grey fill) is like an offset region for the triangle (the original triangle is shown in red and examples of the circle being added to the vertices of the triangle are shown in green).

circle <- circleGrob(0, 0, r=.1, gp=gpar(col="green", fill=NA))
triangle <- polygonGrob(c(.3, .5, .7), c(.3, .7, .3),
                        gp=gpar(col="red", fill=NA))
grid.minkowski(circle, triangle, gp=gpar(lwd=3, fill="grey"))
plot of chunk unnamed-chunk-41

However, the pattern can be any shape, so the final result is more general than the offset regions described previously. For example, the following code adds a rectangle pattern to the triangle.

rect <- rectGrob(0, 0, width=.2, height=.2, gp=gpar(col="green", fill=NA))
grid.minkowski(rect, triangle, gp=gpar(lwd=3, fill="grey"))
plot of chunk unnamed-chunk-42

The examples so far have used a pattern that is centred on (0, 0), so the result is just an expanded version of the original path. However, the pattern can also induce a translation of the path. As an extreme example, the following code adds a tiny circle to the triangle. The circle has almost zero width so it does not expand the triangle, but the circle is at (0, .2) so the result is translated vertically.

pt <- circleGrob(0, .2, r=0.001, gp=gpar(col="green"))
grid.minkowski(pt, triangle, gp=gpar(lwd=3, fill="grey"))
plot of chunk unnamed-chunk-43

In full generality, the Minkowski Sum produces the sum of all vectors within the pattern and the path. This is shown in the following diagram: the region that we are drawing within is represented by a dotted rectangle, with the origin, (0, 0), at the bottom-left corner of the dotted rectangle; the pattern, a rectangle, is drawn in blue to show that it is offset vertically from the origin; the path, a triangle, is drawn in red; and examples of the pattern being added to the path (at the triangle vertices) are drawn in green. The final Minkowski Sum is the black region with a grey fill.

smallRect <- rectGrob(0, .2, width=.2, height=.2, gp=gpar(col="green", fill=NA))
grid.minkowski(smallRect, triangle, gp=gpar(lwd=3, fill="grey"))
plot of chunk unnamed-chunk-44

The path can also be an open shape. For example, the following code adds a rectangle pattern to a line.

line <- linesGrob(c(.3, .5, .7), c(.3, .7, .3),
                  gp=gpar(col="red"))
grid.minkowski(rect, line, gp=gpar(lwd=3, fill="grey"))
plot of chunk unnamed-chunk-45

This usage allows us to create shapes by stroking a path with a pen, in the spirit of METAFONT (Knuth, 1989) or METAPOST (Hobby, 1998). For example, the following code creates an ellipsoid pen (pattern) and strokes the line with that pen (adds the pattern to the path).

pen <- xsplineGrob(c(-.1, 0, .1, 0), c(-.1, .1, .1, -.1),
                   open=FALSE, shape=1,
                   gp=gpar(col="green", fill=NA))
grid.minkowski(pen, line, gp=gpar(lwd=3, fill="grey"))
plot of chunk unnamed-chunk-46

7. Summary

New functions in the 'gridGeometry' package provide access to more of the facilities in the 'polyclip' package. The main interface for generating offset regions is provided by grid.polyoffset() for closed shapes and grid.polylineoffset() for open shapes. There is also a new interface for generating Minkowski Sums via the grid.minkowski() function.

8. Technical requirements

The examples and discussion in this report relate to version 0.4-0 of the 'gridGeometry' package.

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

9. Resources

How to cite this report

Wong, J., and Murrell, P. (2022). "Offsetting Lines and Polygons in 'grid'" Technical Report 2022-03, Department of Statistics, The University of Auckland. Version 1. [ bib | DOI | http ]

10. References

[Beresford and Wong, 2022]
Beresford, T. and Wong, J. (2022). Implementing a portable augmented/virtual reality auralisation tool on consumer-grade devices. In Kessissoglou, N. and Buret, M., editors, Acoustics 2021 Making Waves, Proceedings of the Annual Conference of the Australian Acoustical Society. [ bib ]
[Hobby, 1998]
Hobby, J. (1998). A User's Manual for MetaPost. [ bib ]
[Johnson and Baddeley, 2019]
Johnson, A. and Baddeley, A. (2019). polyclip: Polygon Clipping. R package version 1.10-0. [ bib | http ]
[Knuth, 1989]
Knuth, D. E. (1989). The Metafont Book. Addison-Wesley Longman Publishing Co., Inc., USA. [ bib ]
[Murrell, 2019]
Murrell, P. (2019). A geometry engine interface for 'grid'. Technical Report 2019-01, Department of Statistics, The University of Auckland. version 4. [ bib | DOI | http ]
[Murrell, 2022a]
Murrell, P. (2022a). Constructive geometry for complex grobs. Technical Report 2022-01, Department of Statistics, The University of Auckland. version 1. [ bib | DOI | http ]
[Murrell, 2022b]
Murrell, P. (2022b). gggrid: Draw with 'grid' in 'ggplot2'. R package version 0.2-0. [ bib | http ]
[R Core Team, 2022]
R Core Team (2022). R: A Language and Environment for Statistical Computing. R Foundation for Statistical Computing, Vienna, Austria. [ bib | http ]
[Wickham, 2016]
Wickham, H. (2016). ggplot2: Elegant Graphics for Data Analysis. Springer-Verlag New York. [ bib | http ]

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