Very simple pixel art in R

Two-frame sprite animation of Link from The Legend of Zelda walking forwards.

It’s dangerous to code alone…

tl;dr

You can use R’s image() function to convert a matrix to a pixelly graphic.

Pixel fixation

My last post was about the {emojiscape} package, which makes a little scene out of sampled emojis.

Following a similar approach, you could write a matrix by hand and plot it via the base function image(). Here’s a very basic example with a ‘glider’ from Conway’s Game of Life. Values of 0 are ‘dead’ cells and values of 1 are ‘live’.

glider_v <- c(0,0,0,0,0, 0,0,1,0,0, 0,0,0,1,0, 0,1,1,1,0, 0,0,0,0,0)
glider_m <- matrix(glider_v, 5)           # convert to matrix
glider_m <- glider_m[, ncol(glider_m):1]  # reverse cols
par(mar = rep(0, 4))                      # ensure no margins
image(glider_m)                           # plot it

Note that I input the vector values from what would become the top left to bottom right of the output matrix. The image() function doesn’t read them in this order, however, so I’ve added a step to reverse the column order so the plot output appears as I intended.

Also, image() normally outputs with labelled axes, but we can effectively hide those by minimising the margins par()ameter of the plot to 0.

Reprologoducibility

But really my motivation is to make a reproducible version of this blog’s logo: an insect composed of ‘pixels’ in a 16-by-16 square.

So, I’ve hand-coded a binary vector of length 256 (i.e. 16 * 16). The 0s and 1s here represent background and insect pixels, respectively. I’ve used line breaks to make it easier to create and edit the vector manually.

logo_v <- c(
  
  0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,
  0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,
  0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,
  0,0,1,0,0,1,0,1,1,0,1,0,0,1,0,0,
  
  0,0,0,1,0,1,0,1,1,0,1,0,1,0,0,0,
  0,0,0,1,0,0,1,1,1,1,0,0,1,0,0,0,
  0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,0,
  0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,
  
  0,0,1,0,1,1,0,1,1,0,1,1,0,1,0,0,
  0,0,0,1,0,1,0,1,1,0,1,0,1,0,0,0,
  0,0,0,0,0,1,0,1,1,0,1,0,0,0,0,0,
  0,0,0,0,1,1,0,1,1,0,1,1,0,0,0,0,
  
  0,0,0,1,0,1,0,1,1,0,1,0,1,0,0,0,
  0,0,0,1,0,1,0,1,1,0,1,0,1,0,0,0,
  0,0,1,0,0,1,0,1,1,0,1,0,0,1,0,0,
  0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0

)

I might as well make a (non-generic) function to matrixify (definitely a word) and plot the vector, so I can then tweak a few parameters as I please.

plot_logo <- function(
  x = logo_v,           # vector
  px = 16,              # width/length of output (square)
  col_0 = "black",      # colour for values of 0
  col_1 = "#1e8016",    # colour for values of 1
  lwd = 8               # to separate the squares
) {
  
  par(mar = rep(0, 4))  # set margins outside plot region
  
  m <- matrix(x, px)    # create a matrix from the vector
  m <- m[, ncol(m):1]   # reverse cols
  
  image(m, col = c(col_0, col_1))  # plot matrix, colour by number
  
  # If line width provided, draw lines between squares
  if (!is.null(lwd)) {
    px_half <- px * 2
    s <- seq(-1 / px_half, 1 + (1 / px_half), 1 / (px - 1))
    abline(h = s, v = s, col = col_0, lwd = lwd)
  }
  
}

Note that I added a line width argument (lwd). If specified, horizontal and vertical lines are drawn to give the impression that the squares are ‘separated’ from each other.

Here’s the logo.

plot_logo(lwd = 2)

And here’s what happens if we remove the lines and swap the colours, for example.

plot_logo(col_0 = "#1e8016", col_1 = "black", lwd = NULL)

And given it’s Pride Month:

for (i in rainbow(7)) {
  plot_logo(lwd = 1, col_0 = "white", col_1 = i)
}

Sprite delight

This approach is basically pixel-art-by-numbers, right?

So I’ve written and animated two frames of a classic videogame character, Link from The Legend of Zelda on the NES, using the {magick} package to create a gif.

There’s four colours in this one, so the vectors are no longer binary: there’s 0 for the background, 1 for green, 2 for skin and 3 for the darker spots.

The top part of the sprite doesn’t change between frames, but the bottom does. To avoid repetition, we can store the top part as a separate vector, then combine it with each frame’s lower section. It’s still a bit of a slog to input these by hand!

link_v_top <- c(
  0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,
  0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,
  0,0,2,0,1,3,3,3,3,3,3,1,0,2,0,0,
  0,0,2,0,3,3,3,3,3,3,3,3,0,2,0,0,
  
  0,0,2,2,3,2,1,2,2,1,2,3,2,2,0,0,
  0,0,2,2,3,2,3,2,2,3,2,3,2,2,0,0,
  0,0,0,2,2,2,2,2,2,2,2,2,2,3,0,0
)

link_v_b1 <- c(
  0,0,0,1,1,2,2,3,3,2,2,1,1,3,0,0,
  
  0,3,3,3,3,3,2,2,2,2,1,1,3,3,3,0,
  3,3,2,3,3,3,3,1,1,1,1,1,2,3,3,0,
  3,2,2,2,3,3,2,3,3,1,1,2,2,2,3,0,
  3,3,2,3,3,3,2,1,3,3,3,3,2,2,2,0,
  
  3,3,2,3,3,3,2,3,3,1,1,1,1,2,0,0,
  3,3,3,3,3,3,2,1,1,1,1,1,0,0,0,0,
  0,2,2,2,2,2,3,0,0,3,3,3,0,0,0,0,
  0,0,0,0,3,3,3,0,0,0,0,0,0,0,0,0
)

link_v_b2 <- c(
  0,0,0,0,1,2,2,3,3,2,2,1,3,3,0,0,
  
  0,0,3,3,3,3,3,2,2,2,1,1,1,2,0,0,
  0,3,3,2,3,3,3,3,1,1,1,1,1,2,0,0,
  0,3,2,2,2,3,3,2,3,3,1,1,3,0,0,0,
  0,3,3,2,3,3,3,2,1,3,3,3,1,0,0,0,
  
  0,3,3,2,3,3,3,2,3,3,1,1,1,0,0,0,
  0,3,3,3,3,3,3,2,1,1,1,3,0,0,0,0,
  0,0,2,2,2,2,2,0,0,3,3,3,0,0,0,0,
  0,0,0,0,0,0,0,0,0,3,3,3,0,0,0,0
)

# Combine vectors to get frames
link_f1 <- c(link_v_top, link_v_b1)
link_f2 <- c(link_v_top, link_v_b2)

Now we have the vectors representing Link for each frame of the animation. The approach now is like before: convert this to a 16 by 16 matrix and plot it. This time I’ve got a function that also saves the plots by first opening a png() graphics device and closing it at the end with dev.off(). I’ve saved these to a temporary directory for the purposes of the post, rather than my local disk.

tmp <- tempdir()  # store temporary folder path

# Function to write frame to temporary folder
write_link <- function(vec) {
  write_path <- file.path(tmp, paste0(substitute(vec), ".png"))
  png(write_path, width = 160, height = 160)
  link_m <- matrix(vec, 16)
  link_m <- link_m[, ncol(link_m):1]
  par(mar = rep(0, 4))
  link_cols <- c("white", "#7bc702", "#cc8f2d", "#6c430a")
  image(link_m, col = link_cols)
  dev.off()
}

# Write the frames
write_link(link_f1); write_link(link_f2)
## quartz_off_screen 
##                 2
## quartz_off_screen 
##                 2

We get a couple of messages to say that the devices have been closed, confirming the save.

Now we can use the {magick} package to create a gif: image_read() to load both PNG frames into a single object from their save location, and then image_animate() to combine the images into an output that flips between the two frames. You could also use image_write() to save this object to gif format.

# Generate a gif from the saved frames
png_paths <- list.files(tmp, "*.png$", full.names = TRUE)     # get file paths
frames <- magick::image_read(png_paths)                       # load the files
magick::image_animate(frames, fps = 2, dispose = "previous")  # combine frames

I’m not sure I’ll be coding the graphics for the whole game anytime soon…

Hip to be square

I’m not the first person to think or do this in R, I’m sure.

I did come across a really neato {pixelart} package and Shiny app by Florian Privé where you upload an image and it gets converted into a pixel form. As Florian said in his blogpost:

Kids and big kids can quickly become addicted to this

Yes. And that’s exactly why this post exists.

Let me know if you know of any more packages or whatever that do this sort of thing.


Session info

## ─ Session info ───────────────────────────────────────────────────────────────
##  setting  value                       
##  version  R version 4.0.3 (2020-10-10)
##  os       macOS Mojave 10.14.6        
##  system   x86_64, darwin17.0          
##  ui       X11                         
##  language (EN)                        
##  collate  en_GB.UTF-8                 
##  ctype    en_GB.UTF-8                 
##  tz       Europe/London               
##  date     2021-06-29                  
## 
## ─ Packages ───────────────────────────────────────────────────────────────────
##  package     * version date       lib source        
##  blogdown      0.21    2020-10-11 [1] CRAN (R 4.0.2)
##  bookdown      0.21    2020-10-13 [1] CRAN (R 4.0.2)
##  bslib         0.2.4   2021-01-25 [1] CRAN (R 4.0.2)
##  cli           2.5.0   2021-04-26 [1] CRAN (R 4.0.2)
##  digest        0.6.27  2020-10-24 [1] CRAN (R 4.0.2)
##  evaluate      0.14    2019-05-28 [1] CRAN (R 4.0.1)
##  highr         0.9     2021-04-16 [1] CRAN (R 4.0.2)
##  htmltools     0.5.1.1 2021-01-22 [1] CRAN (R 4.0.2)
##  jquerylib     0.1.3   2020-12-17 [1] CRAN (R 4.0.2)
##  jsonlite      1.7.2   2020-12-09 [1] CRAN (R 4.0.2)
##  knitr         1.32    2021-04-14 [1] CRAN (R 4.0.2)
##  magick        2.7.1   2021-03-20 [1] CRAN (R 4.0.2)
##  magrittr      2.0.1   2020-11-17 [1] CRAN (R 4.0.2)
##  R6            2.5.0   2020-10-28 [1] CRAN (R 4.0.2)
##  Rcpp          1.0.6   2021-01-15 [1] CRAN (R 4.0.2)
##  rlang         0.4.11  2021-04-30 [1] CRAN (R 4.0.2)
##  rmarkdown     2.7     2021-02-19 [1] CRAN (R 4.0.2)
##  sass          0.3.1   2021-01-24 [1] CRAN (R 4.0.2)
##  sessioninfo   1.1.1   2018-11-05 [1] CRAN (R 4.0.2)
##  stringi       1.6.1   2021-05-10 [1] CRAN (R 4.0.2)
##  stringr       1.4.0   2019-02-10 [1] CRAN (R 4.0.2)
##  withr         2.4.2   2021-04-18 [1] CRAN (R 4.0.2)
##  xfun          0.22    2021-03-11 [1] CRAN (R 4.0.2)
##  yaml          2.2.1   2020-02-01 [1] CRAN (R 4.0.2)
## 
## [1] /Users/matt.dray/Library/R/4.0/library
## [2] /Library/Frameworks/R.framework/Versions/4.0/Resources/library