{affiner}
is an extraction and improvement of the
low-level geometric and R 4.2 affine transformation feature
functionality used in {piecepackr} to
render board game pieces in {grid}
using a 3D oblique
projection.
The current goals are to:
{piecepackr}
users to use this
low-level geometric functionality without exporting it in
{piecepackr}
(which already has a large API) in case they
want to do things like implement custom polyhedral dice.{grid}
(and perhaps
{ggplot2}
).Some particular intended strengths compared to other R geometry packages:
{grid}
(e.g. oblique
projections and isometric projections).{grid}
. The
affine_settings()
function which reverse engineers
useGrob()
’s vp
and transformation
arguments is even available as a “standalone” file that can be copied
over into other R packages under the permissive Unlicense.as_coord2d()
method for angle()
objects lets you compute the regular polygon vertices and center using
polar
coordinatesaffine_settings()
and affineGrob()
or
grid.affine()
to render arbitrary “illustrated” grobs
within each of these parallelograms.library("affiner")
library("grid")
xy <- as_coord2d(angle(seq(90, 360 + 90, by = 60), "degrees"),
radius = c(rep(0.488, 6), 0))
xy$translate(x = 0.5, y = 0.5)
l_xy <- list()
l_xy$top <- xy[c(1, 2, 7, 6)]
l_xy$right <- xy[c(7, 4, 5, 6)]
l_xy$left <- xy[c(2, 3, 4, 7)]
gp_border <- gpar(fill = NA, col = "black", lwd = 12)
vp_define <- viewport(width = unit(3, "inches"), height = unit(3, "inches"))
colors <- c("#D55E00", "#56B4E9", "#009E73")
spacings <- c(0.25, 0.25, 0.2)
texts <- c("pkgname", "right\nface", "left\nface")
rots <- c(45, 0, 0)
fontsizes <- c(52, 80, 80)
sides <- c("top", "right", "left")
types <- gridpattern::names_polygon_tiling[c(5, 9, 7)]
l_grobs <- list()
grid.newpage()
for (i in 1:3) {
side <- sides[i]
xy_side <- l_xy[[side]]
if (requireNamespace("gridpattern", quietly = TRUE)) {
bg <- gridpattern::grid.pattern_polygon_tiling(
colour = "grey80",
fill = c(colors[i], "white"),
type = types[i],
spacing = spacings[i],
draw = FALSE)
} else {
bg <- rectGrob(gp = gpar(col = NA, fill = colors[i]))
}
text <- textGrob(texts[i], rot = rots[i],
gp = gpar(fontsize = fontsizes[i]))
settings <- affine_settings(xy_side, unit = "snpc")
grob <- l_grobs[[side]] <- grobTree(bg, text)
grid.affine(grob,
vp_define = vp_define,
transform = settings$transform,
vp_use = settings$vp)
grid.polygon(xy_side$x, xy_side$y, gp = gp_border)
}
isocubeGrob()
/ grid.isocube()
provides a
convenience wrapper around affineGrob()
/
grid.affine()
for the isometric cube case:
Our high-level strategy for rendering 3D objects is as follows:
Figure out the “physical” 3D coordinates of the cube face vertices in “inches”. These cube faces will correspond to target “3D viewports” we’ll want to render the illustrated cube face grobs into.
Project these 3D coordinates onto a “physical” xy-plane (corresponding to our graphics device) in “inches” using a parallel projection (in this example we’ll do a couple oblique projections and an isometric projection). Note since all parallel projections are affine transformations we know the projected vertices of a square “3D viewport” will project to the 2D coordinates of a “parallelogram viewport”.
(If they don’t already do so) translate these parallelograms so they lie within the graphics device view (i.e. the “parallelogram viewport” vertices are all in the upper right quadrant of the xy-plane).
alpha
angle between 0 and 90 degrees then any flat faces
lying directly on the xy-plane will stay where they are and any flat
faces on a parallel higher plane will only be shifted up/right. So in
this case one usually doesn’t need to such a translation assuming all
your “objects” were placed in the upper quadrant of the xy-plane to
begin with.Use affine_settings()
and grid.affine()
/ affineGrob()
to render the illustrated cube face “grobs”
within these affine transformed “parallelogram viewports”. The order
these are drawn is important but in this example we manually sorted them
ahead of time in an order that worked for our target
projections.
library("affiner")
library("grid")
xyz_face <- as_coord3d(x = c(0, 0, 1, 1) - 0.5, y = c(1, 0, 0, 1) - 0.5, z = 0.5)
l_faces <- list() # order faces for our target projections
l_faces$bottom <- xyz_face$clone()$
rotate("z-axis", angle(180, "degrees"))$
rotate("y-axis", angle(180, "degrees"))
l_faces$north <- xyz_face$clone()$
rotate("z-axis", angle(90, "degrees"))$
rotate("x-axis", angle(-90, "degrees"))
l_faces$east <- xyz_face$clone()$
rotate("z-axis", angle(90, "degrees"))$
rotate("y-axis", angle(90, "degrees"))
l_faces$west <- xyz_face$clone()$
rotate("y-axis", angle(-90, "degrees"))
l_faces$south <- xyz_face$clone()$
rotate("z-axis", angle(180, "degrees"))$
rotate("x-axis", angle(90, "degrees"))
l_faces$top <- xyz_face$clone()$
rotate("z-axis", angle(-90, "degrees"))
colors <- c("#D55E00", "#009E73", "#56B4E9", "#E69F00", "#CC79A7", "#0072B2")
spacings <- c(0.25, 0.2, 0.25, 0.25, 0.25, 0.25)
die_face_grob <- function(digit) {
if (requireNamespace("gridpattern", quietly = TRUE)) {
bg <- gridpattern::grid.pattern_polygon_tiling(
colour = "grey80",
fill = c(colors[digit], "white"),
type = gridpattern::names_polygon_tiling[digit],
spacing = spacings[digit],
draw = FALSE)
} else {
bg <- rectGrob(gp = gpar(col = NA, fill = colors[digit]))
}
digit <- textGrob(digit, gp = gpar(fontsize = 72))
grobTree(bg, digit)
}
l_face_grobs <- lapply(1:6, function(i) die_face_grob(i))
grid.newpage()
for (i in 1:6) {
vp <- viewport(x = unit((i - 1) %% 3 + 1, "inches"),
y = unit(3 - ((i - 1) %/% 3 + 1), "inches"),
width = unit(1, "inches"), height = unit(1, "inches"))
pushViewport(vp)
grid.draw(l_face_grobs[[i]])
popViewport()
grid.text("The six die faces", y = 0.9,
gp = gpar(fontsize = 18, face = "bold"))
}
# re-order face grobs for our target projections
# bottom = 6, north = 4, east = 5, west = 2, south = 3, top = 1
l_face_grobs <- l_face_grobs[c(6, 4, 5, 2, 3, 1)]
draw_die <- function(l_xy, l_face_grobs) {
min_x <- min(vapply(l_xy, function(x) min(x$x), numeric(1)))
min_y <- min(vapply(l_xy, function(x) min(x$y), numeric(1)))
l_xy <- lapply(l_xy, function(xy) {
xy$translate(x = -min_x + 0.5, y = -min_y + 0.5)
})
grid.newpage()
vp_define <- viewport(width = unit(1, "inches"), height = unit(1, "inches"))
gp_border <- gpar(col = "black", lwd = 4, fill = NA)
for (i in 1:6) {
xy <- l_xy[[i]]
settings <- affine_settings(xy, unit = "inches")
grid.affine(l_face_grobs[[i]],
vp_define = vp_define,
transform = settings$transform,
vp_use = settings$vp)
grid.polygon(xy$x, xy$y, default.units = "inches", gp = gp_border)
}
}
# oblique projection of dice onto xy-plane
l_xy_oblique1 <- lapply(l_faces, function(xyz) {
xyz$clone() |>
as_coord2d(scale = 0.5)
})
draw_die(l_xy_oblique1, l_face_grobs)
grid.text("Oblique projection\n(onto xy-plane)", y = 0.9,
gp = gpar(fontsize = 18, face = "bold"))
# oblique projection of dice on xz-plane
l_xy_oblique2 <- lapply(l_faces, function(xyz) {
xyz$clone()$
permute("xzy") |>
as_coord2d(scale = 0.5, alpha = angle(135, "degrees"))
})
draw_die(l_xy_oblique2, l_face_grobs)
grid.text("Oblique projection\n(onto xz-plane)", y = 0.9,
gp = gpar(fontsize = 18, face = "bold"))
# isometric projection
l_xy_isometric <- lapply(l_faces, function(xyz) {
xyz$clone()$
rotate("z-axis", angle(45, "degrees"))$
rotate("x-axis", angle(-(90 - 35.264), "degrees")) |>
as_coord2d()
})
draw_die(l_xy_isometric, l_face_grobs)
grid.text("Isometric projection", y = 0.9,
gp = gpar(fontsize = 18, face = "bold"))