Making Art with R

September 14 2020
R Art

Canvas Cards

As a side project this year, I thought it would be fun to play around with making art in R. Because I’m not all that original, I will use an existing developer’s project as the guide for my development. In particular, Canvas Cards will be the basis of inspiration for each card.

To stop this from being a pure copy paste exercise, I want to establish some constraints for this project:

  • Only use packages for general data handling and plotting purposes (e.g. tidyverse, ggforce). General polygon creation / transformations will be “hand-crafted”.
  • 52 cards to be generated by the end of 2020 - aim for ~3 per week. Each card will be posted on the blog with a short discussion.
  • All code to be stored on open repo on GitHub.
  • End result package should be reasonably tidy with decent documentation.
  • The aim isn’t to replicate each card exactly, and to allow creativity to lead in different directions.

So, let’s give it a whirl!

Getting Started

So to begin with, we’re going to develop all the cards as part of an R package. This development pattern makes it much easier to maintain our code, and understand what dependencies we’re relying on. To start with we’ll take a quick and dirty approach to generate our first results. As we develop more cards, we’ll think about how we can leverage common patterns to simplify the codebase.

RStudio makes the setup of a new package project super straightforward through the wizard. In addition, we want to have this linked to GitHub for easy sharing of the code.

Helpers

We will be using ggplot2 to generate our canvases, however there are a few helpers we will want to structure the format of our outputs. Also, I really like the idea of seeing the steps taken to reach each card, based on Georgios Karamanis Tidy Tuesday submissions. So we’ll output an image every time we run the code, and use magick to stitch the images into a video GIF.

#' Save Plot
#'
#' @param plot
#' @param file_name
#' @param width
#' @param height
#' @param path
#'
#' @return
#' @export
#' @import ggplot2
save_plot <- function(
  plot,
  file_name,
  width,
  height,
  path = 'output',
  echo = TRUE,
  ...) {
  dir.create(glue("{path}/{file_name}/"), showWarnings = FALSE)
  temp_path <- glue("{path}/{file_name}/{format(Sys.time(), '%y-%m-%d-%H-%M-%S')}.png")
  output_path <- glue("{path}/{file_name}.png")

  # fig <- image_graph(width = width,
  #                    height = height,
  #                    res = 96)
  if (echo) print(plot)

  ggplot2::ggsave(plot,
                  filename = output_path,
                  width = width / 100,
                  height = height / 100,
                  device = 'png')

  fig <- output_path %>%
    image_read() %>%
    image_trim()

  image_write(fig, output_path)
  image_write(fig, temp_path)
}

#' @import magick
save_gif <- function(
  file_name,
  path = 'output') {
  files <- list.files(path=glue("{path}/{file_name}/"), pattern = '*.png', full.names = TRUE)
  files <- c(rep(last(files), 12), files)

  files %>%
    image_read() %>% # reads each path file
    image_join() %>% # joins image
    image_animate(fps=5) %>% # animates, can opt for number of loops
    image_write(glue("{path}/{file_name}.gif"))
}

Straight out of the gate there are a few basic geometric transformations which will be useful. In particular we have a polygon rotation function, and a simple way to generate a square using a centre point and height.

It was fun brushing up on my linear algebra to generate the rotation matrix! We will revisit these helpers later to improve them.

rotate_poly <- function(poly, origin, theta = -pi / 10) {
  R <- matrix(c(cos(theta), -sin(theta),
                sin(theta), cos(theta)),
              nrow = 2)

  map(poly, function(xy) {
    rotated <- R %*% (xy - origin)
    rotated[1:2, 1] + origin
  })
}

square_from_centre <- function(origin, height, rotate = FALSE, theta = -pi / 10) {
  xmin = origin[1] - height / 2
  xmax = origin[1] + height / 2
  ymin = origin[2] - height / 2
  ymax = origin[2] + height / 2

  square <- list(
    c(xmin, ymin),
    c(xmin, ymax),
    c(xmax, ymax),
    c(xmax, ymin),
    c(xmin, ymin)
  )

  if (rotate) square <- rotate_poly(square, origin, theta)

  return(square)
}

And with that we’re away!

comments powered by Disqus