---
title: "Groups and Paths and Masks in R Graphics"
author: "Paul Murrell"
date: 2021-12-06
categories: ["Internals"]
tags: ["graphics"]
---
```{r eval=FALSE, echo=FALSE}
knitr::opts_chunk$set(
fig.width = 2, fig.height = 2
)
```
> **UPDATE:** (2023-05-18) The behaviour of compositing operators
was [modified in R version 4.3.0](https://www.stat.auckland.ac.nz/~paul/Reports/GraphicsEngine/compositing/compositing.html)
(affecting the "clear" and
"source" operators). The examples in this post have been
updated so that they produce the same output (just using a
different operator).
Support for gradient fills, pattern fills, clipping paths and masks
was added to the R graphics engine
[in R version 4.1.0](https://developer.r-project.org/Blog/public/2020/07/15/new-features-in-the-r-graphics-engine/).
The development version of R (likely to become R version 4.2.0)
contains support for several more graphical tools:
groups, compositing operators, and affine transformations, plus
some tweaks to paths and masks.
An R-level interface for these new features has been added to the
'grid' graphics package.
```{r eval=FALSE}
library(grid)
```
The following code demonstrates drawing a group with
the new `grid.group()` function. The basic idea
is that we can draw a group of shapes in isolation and then
add the result to the main image. In this case, we draw a
rectangle and a circle
as an independent group before adding them to the image.
One of the advantages of drawing groups in isolation is that we
can combine shapes using different compositing operators.
In this case, we use a "dest.out" operator, which means that,
rather than drawing the rectangle on top of the circle, the
rectangle creates a hole in the circle.
A green line was drawn first to show that there is a hole
in the circle, through which we can see the
green line.
```{r eval=FALSE, composite}
grid.segments(gp=gpar(col=3, lwd=50))
grid.group(rectGrob(width=.4, height=.2, gp=gpar(fill="black")),
"dest.out",
circleGrob(r=.4, gp=gpar(col=NA, fill=4)))
```
The following code demonstrates the new path-drawing facilities,
which includes the new function `grid.fill()` to fill a path.
A path can be created from any number of shapes and then we can
stroke or fill the path (or both). In this case, we describe
a path based on a rectangle and a circle.
When a path consists of overlapping shapes, the "inside" of the
path - the area that gets filled - can become complex.
We can control the "rule" that is used to decide the filled area.
In this case, we use the "even-odd" rule, which means that the
area inside the rectangle is actually outside the path;
the result again is a hole in the circle.
The path is filled with a blue colour (and no border).
```{r eval=FALSE}
grid.segments(gp=gpar(col=3, lwd=50))
path <- gTree(children=gList(circleGrob(r=.4),
rectGrob(width=.4, height=.2)))
grid.fill(path,
rule="evenodd",
gp=gpar(col=NA, fill=4))
```
The following code demonstrates the new luminance mask support,
which is available via the `as.mask()` function.
The `as.mask()` function creates a mask from a grob and a
`type`.
The `type` can be `"luminance"`, which means that the
luminance of the grob determines the semitransparency of the
masked output. In this case, we define a mask based on a
white circle with a black rectangle drawn on top.
When we push a viewport with a luminance mask, any subsequent drawing
will be opaque where the mask is white and transparent where the
mask is black (and semitransparent where the mask is grey).
In this case, having pushed a viewport with the mask,
we fill the entire image with blue and the result is a blue
circle (because the circle in the mask is white) with a hole
(because the rectangle in the mask is black).
```{r eval=FALSE, luminance, fig.keep="none"}
pdf("luminance-mask.pdf", width=2, height=2)
grid.segments(gp=gpar(col=3, lwd=50))
mask <- gTree(children=gList(circleGrob(r=.4,
gp=gpar(col=NA, fill="white")),
rectGrob(width=.4, height=.2,
gp=gpar(col=NA, fill="black"))))
pushViewport(viewport(mask=as.mask(mask, "luminance")))
grid.rect(gp=gpar(fill=4))
dev.off()
```
```{r eval=FALSE, echo=FALSE}
system("pdftoppm -png -rx 96 -ry 96 luminance-mask.pdf > luminance-mask.png")
```
The remaining examples demonstrate affine transformations, using the
`grid.define()` function and the `grid.use()` function. If we
define a group (without drawing it) in one viewport and then use the group in a
different viewport, the group is transformed based on differences
in location, size, and rotation of the two viewports. In this
case, we define a group based on a circle and a rectangle (using the
"dest.out" operator so that the rectangle creates a hole in the circle),
in a viewport
that is the full size of the image, then we push a viewport that
is only one-third the height of the image and use the group
that we defined.
This produces a vertically squashed version of the group because the
viewport we are using the group in is much shorter than the viewport
that the group was defined in.
```{r eval=FALSE, transform-group}
grob <- groupGrob(rectGrob(width=.4, height=.2, gp=gpar(fill="black")),
"dest.out",
circleGrob(r=.4, gp=gpar(col=NA, fill=4)))
grid.define(grob, name="donut")
grid.segments(gp=gpar(col=3, lwd=50))
pushViewport(viewport(height=1/3))
grid.use("donut")
popViewport()
```
The following code is similar, but this time we use the group in
a viewport that is one-third the width of the image, so the group
is horizontally squashed.
Another difference in this example is that the group is based on
a path that is filled using the "even-odd" rule, which demonstrates
that we can combine these new features of groups and paths.
```{r eval=FALSE, transform-fill}
grid.newpage()
grob <- fillGrob(path,
rule="evenodd",
gp=gpar(col=NA, fill=4))
grid.define(grob, name="donut")
grid.segments(gp=gpar(col=3, lwd=50))
pushViewport(viewport(width=1/3))
grid.use("donut")
popViewport()
```
The following code demonstrates that we can use a group multiple
times. In this case, we use the group in two further viewports,
both of which are still square, but smaller than the image and
shifted to the left or right.
Another difference in this example is that the group is based on
a rectangle that is drawn within a viewport that has a luminance
mask applied; further evidence of our ability to employ the
various graphical tools in combination with each other.
```{r eval=FALSE, transform-mask, fig.keep="none"}
pdf("luminance-mask-squashed.pdf", width=2, height=2)
vp <- viewport(mask=as.mask(mask, "luminance"))
grob <- rectGrob(gp=gpar(fill=4), vp=vp)
grid.define(grob, name="donut")
grid.segments(gp=gpar(col=3, lwd=50))
pushViewport(viewport(x=.25, width=.5, height=.5))
grid.use("donut")
popViewport()
pushViewport(viewport(x=.75, width=.5, height=.5))
grid.use("donut")
popViewport()
dev.off()
```
```{r eval=FALSE, echo=FALSE}
system("pdftoppm -png -rx 96 -ry 96 luminance-mask-squashed.pdf > luminance-mask-squashed.png")
```
The new features have only been implemented on a subset of graphics
devices so far: `cairo_pdf()`, `cairo_ps()`,
`x11(type="cairo")`, `png(type="cairo")`,
`jpeg(type="cairo")`, `tiff(type="cairo")`, `svg()`,
`quartz()` (from R 4.3.0), and `pdf()`.
Furthermore, most compositing operators only work on the Cairo devices
or `quartz()`,
Cairo devices only support alpha masks, and `quartz()` only supports
luminance masks.
R packages that implement graphics devices will need to be updated and
reinstalled for the new R version.
Further discussion and more detail about the new features and
how they have been implemented can be found in a series of
technical reports: one on
[groups](https://stattech.wordpress.fos.auckland.ac.nz/2021/11/15/2021-02-groups-compositing-operators-and-affine-transformations-in-r-graphics/),
one on
[paths](https://stattech.wordpress.fos.auckland.ac.nz/2021/11/16/2021-03-stroking-and-filling-paths-in-r-graphics/),
and one on
[masks](https://stattech.blogs.auckland.ac.nz/2021/12/01/2021-04-luminance-masks-in-r-graphics/).