You can use R’s image() function to convert a matrix to a pixelly graphic.
Note
I’ve now written a little R package called {pixeltrix}, which lets you click on squares in a plot window to generate a matrix of ‘pixels’. This means you don’t have to type out any vectors by hand. You can read more in some other blog posts.
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 matrixglider_m <- glider_m[, ncol(glider_m):1] # reverse colspar(mar =rep(0, 4)) # ensure no marginsimage(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.
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, # vectorpx =16, # width/length of output (square)col_0 ="black", # colour for values of 0col_1 ="#1e8016", # colour for values of 1lwd =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 colsimage(m, col =c(col_0, col_1)) # plot matrix, colour by number# If line width provided, draw lines between squaresif (!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.
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 frameslink_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 folderwrite_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 frameswrite_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 framespng_paths <-list.files(tmp, "*.png$", full.names =TRUE) # get file pathsframes <- magick::image_read(png_paths) # load the filesmagick::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.
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.
Note
Turns out that mikefc, aka coolbutuseless, (who else?) wrote a great blog post with a method for grabbing, plotting and animating sprites with the packages {png}, {raster}, {ggplot2} and {gganimate}. Slightly less painful than writing vectors by hand!
If you want to design your own sprites rather than copy others, try my little {pixeltrix} package for interactive pixel selection from a plot window, which returns a matrix.
Environment
Session info
Last rendered: 2023-07-21 19:29:29 BST
R version 4.3.1 (2023-06-16)
Platform: aarch64-apple-darwin20 (64-bit)
Running under: macOS Ventura 13.2.1
Matrix products: default
BLAS: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRblas.0.dylib
LAPACK: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.11.0
locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
time zone: Europe/London
tzcode source: internal
attached base packages:
[1] stats graphics grDevices utils datasets methods base
loaded via a namespace (and not attached):
[1] digest_0.6.33 fastmap_1.1.1 xfun_0.39 fontawesome_0.5.1
[5] magrittr_2.0.3 knitr_1.43.1 htmltools_0.5.5 rmarkdown_2.23
[9] cli_3.6.1 compiler_4.3.1 rstudioapi_0.15.0 tools_4.3.1
[13] evaluate_0.21 Rcpp_1.0.11 yaml_2.3.7 magick_2.7.4
[17] rlang_1.1.1 jsonlite_1.8.7 htmlwidgets_1.6.2