Interactive pixel art in R with {pixeltrix}

art
locator
pixeltrix
r
videogames
Author
Published

September 24, 2022

Gif of RStudio window. The console is running the code pixeltrix::click_pixels(6, 7, 3) and a plot appears with a 6 by 7 square grid. Some squares are clicked, changing them to light grey. A second click turns them dark grey. Eventually a little square character with a face is created. A matrix encoded the image is shown in the console when the escape key is pressed.

tl;dr

I’ve written {pixeltrix}, an R package that lets you select ‘pixels’ interactively from a plot window and returns your final image as a matrix. You could use it to design sprites, for example.

Pixel perfect

I’ve written before about creating very simple pixel art in R. To create a sprite of Link from The Legend of Zelda I had to write out by hand a vector that encoded its pixel values. It was tedious.

There are, however, a couple of options in R to take an image and extract the pixels from it: see Florian Privé’s Shiny app in the {pixelart} package and Mike Cheng’s (AKA coolbutuseless) blog post that also describes how to animate them.1

But what if you want to create a sprite from scratch? It would be great if you could click pixels interactively and be returned a matrix encoding your image.

I couldn’t find an R package to do this, so I decided to make a very simple one: {pixeltrix} (as in ‘pixel’ and ‘matrix’, but also as in ‘tricks’2, lol).

It’s written entirely in base R (no Shiny or server needed) and can be run in the R console. It’s basically a repeat loop that runs image() to plot squares3 (hereafter ‘pixels’) and locator()4 to let you click those pixels on and off. The coordinates of each click are matched to the nearest pixel centre, the pixel’s value is incremented by 1 (or wrapped back to zero) and the image is redrawn.

The package is still in development, but I think it’s reached a useable state for my own purposes.

ℹ️ Update

I lied. The package has been updated since this post. You can read about the changes in a more recent blogpost. Highlight: you can make animations now.

Enter the matrix

The package is available for download from GitHub. I have some ideas on how to improve it; go ahead and add your own ideas to the issues tracker.

# install.packages(remotes)  # if not yet installed
remotes::install_github("matt-dray/pixeltrix") # v0.1 in this post
library(pixeltrix)

The main function is click_pixels(), to which you pass plot dimensions (how many pixels tall and wide), the number of pixel ‘states’ (the number of values a pixel can take, so binary would be 2) and whether you want to put a grid over the plot (makes it easier to see where the pixels are).

click_pixels(
  n_rows   = 3,
  n_cols   = 3,
  n_states = 2,
  grid = TRUE
) -> x

This opens a plot window. Repeatedly click a pixel to cycle it through the number of states you asked for. For example, n_states = 4 means you cycle it through values of 0, 1, 2 and 3 (wrapping back to 0), which will be manifested in the plot as different shades of grey.

Note that you can only click one pixel at a time, so you’ll have to do a lot of clicking if your n_states value is high. Colouring stuff in really slowly is called ‘mindfulness’, I believe; good for your wellbeing.

When you’re done, you press the Esc key, or the ‘Finish’ button in the plot window of RStudio. I saved all the images below via the ‘Export’ button in RStudio.

A 3 by 3 square panel grid with darker squares in an x-shaped pattern.

You’re returned a matrix that contains the value of each pixel in your image. So if you had set n_states = 3, a twice-clicked pixel gets the value 2, an unclicked pixel defaults to a value of 0, etc.

x
     [,1] [,2] [,3]
[1,]    1    0    1
[2,]    0    1    0
[3,]    1    0    1

This matrix is basically a blueprint of the image you created. You can take this and do other things with it. Maybe you’ll make art by passing it to ggplot() to match each of the pixel-state values to a colour. Maybe you’ll use it to plan your crochet or cross-stitch5, or to teach spatial epidemiology (!).

If you want to edit your matrix, you can pass it into edit_pixels(). This means you don’t have to start over from scratch with click_pixels() if you only want to change something small. Note that you can provide a higher n_states value to edit_pixels() than the current maximum in the matrix you provided.

Sprite club

My main purpose for the package is to create simple sprites.

I used {pixeltrix} to make each of the sprites below. They took about a minute each. It would’ve taken much longer to write their matrices by hand and to keep passing them to image() to visuliase them and make sure there weren’t any mistakes.

Tamagotchi

Here’s a 1-bit original kuchipatchi sprite from the original 90s Tamagotchi digital pets. It uses the default of two pixel states (binary): so 0 for white and 1 for grey.

click_pixels(14, 14) -> tam_sprite

A 14 by 14 pixel grid with a two-toned sprite of a pet character from the original 90s Tamagotchi pets.

Click to expand matrix
tam_sprite
      [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12] [,13] [,14]
 [1,]    0    0    0    0    1    1    1    1    1     1     0     0     0     0
 [2,]    0    0    0    1    0    0    0    0    0     0     1     0     0     0
 [3,]    0    1    1    0    1    0    0    0    0     1     0     1     0     0
 [4,]    1    0    0    0    0    0    0    0    0     0     0     0     1     0
 [5,]    0    1    1    1    0    0    0    0    0     0     0     0     1     0
 [6,]    1    0    0    0    0    0    0    0    0     0     0     0     1     0
 [7,]    0    1    1    1    0    0    0    0    0     0     0     0     1     0
 [8,]    0    0    0    1    0    0    0    0    1     0     1     0     0     1
 [9,]    0    0    0    1    0    0    0    0    1     0     1     0     0     1
[10,]    0    0    0    1    0    0    0    0    0     1     0     0     0     1
[11,]    0    0    0    0    1    0    0    0    0     0     0     0     1     0
[12,]    0    0    0    0    0    1    0    1    1     1     0     1     0     0
[13,]    0    0    0    0    0    1    0    1    0     1     0     1     0     0
[14,]    0    0    0    0    0    0    1    0    0     0     1     0     0     0

Pokémon

Here’s the player character from the first generation of Pokémon games on the Game Boy. It uses three states (n_states = 3): value 0 is white, 1 is light grey and 2 is dark grey.

click_pixels(14, 16, 3) -> poke_sprite

A 14 by 16 pixel grid with a three-toned sprite of the main character from the first generation of Pokemon games for the Game Boy.

Click to expand matrix
poke_sprite
      [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12] [,13] [,14]
 [1,]    0    0    0    0    2    2    2    2    2     2     0     0     0     0
 [2,]    0    0    0    2    1    1    1    1    1     1     2     0     0     0
 [3,]    0    0    2    1    1    1    1    1    1     1     1     2     0     0
 [4,]    0    0    2    1    1    1    1    1    1     1     1     2     0     0
 [5,]    0    2    2    2    1    0    0    0    0     1     2     2     2     0
 [6,]    0    2    2    0    2    2    2    2    2     2     0     2     2     0
 [7,]    2    0    2    0    0    0    0    0    0     0     0     2     0     2
 [8,]    2    0    0    0    0    2    0    0    2     0     0     0     0     2
 [9,]    0    2    2    0    0    2    0    0    2     0     0     2     2     0
[10,]    0    2    2    2    0    0    1    1    0     0     2     2     2     0
[11,]    2    0    0    2    2    2    2    2    2     2     2     0     0     2
[12,]    2    0    0    2    2    2    2    2    2     2     2     0     0     2
[13,]    0    2    2    2    1    1    2    2    1     1     2     2     2     0
[14,]    0    0    2    1    2    2    1    1    2     2     1     2     0     0
[15,]    0    0    2    1    1    1    2    2    1     1     1     2     0     0
[16,]    0    0    0    2    2    2    0    0    2     2     2     0     0     0

Why?

Turns out the {pixeltrix} package is actually yak-shaving for another package I’m developing: {tamRgo}.

{tamRgo} is a (very much work-in-progress) conceptual package for a Tamagotchi-like experience in the R console. You get a persistent interactive digital pet on your computer whose stats update in ‘real time’ while you’re away.

I want to print a largeish canvas of pixels to visualise multiple pet ‘species’ and for the various interactions you can have (playing, feeding, cleaning). {pixeltrix} makes it much easier to design these scenes and returns matrices that I can add directly to {tamRgo}.

ℹ️ Update

I’ve now written a post about {tamRgo}, where you can see how {pixeltrix} was used for the character sprites.

Environment

Session info
Last rendered: 2023-06-29 18:52:13 CEST
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/Zurich
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] pixeltrix_0.2.1.9000

loaded via a namespace (and not attached):
 [1] htmlwidgets_1.6.2 compiler_4.3.1    fastmap_1.1.1     cli_3.6.1        
 [5] tools_4.3.1       htmltools_0.5.5   rstudioapi_0.14   yaml_2.3.7       
 [9] rmarkdown_2.22    knitr_1.43.1      jsonlite_1.8.5    xfun_0.39        
[13] digest_0.6.31     rlang_1.1.1       evaluate_0.21    

Footnotes

  1. You can even go into the third dimension (i.e. voxels) with Mike’s {isocubes} and its extension {oblicubes} by Trevor L Davies. I used these in a demo of ‘3D’ dungeon-making with my {r.oguelike} package.↩︎

  2. Illusions, Michael.↩︎

  3. The only awkward part is that image() doesn’t plot with bounds of 0 to 1. There’s an overhang dependent on the number of squares you want to draw. This results in some small but awkward calculations so that the clicks can be mapped properly to the nearest pixel and so the grid overlay can be placed correctly.↩︎

  4. I’ve written before about using the locator() function to select points on fictitious maps.↩︎

  5. But see also Sharon Machlis’s ‘Overlay Mosaic Crochet Pattern Chart Generator’ Shiny app for crochet patterns and Ben Vigreux’s {embroidr} for planning embroidery projects.↩︎

Reuse

CC BY-NC-SA 4.0