A glass shattering book draw with gganimate

It’s time for a Twitter book draw again: every month, a random Locke Data Twitter follower wins an excellent data science book! This month’s book was Weapons of Math Destruction : How Big Data Increases Inequality and Threatens Democracy. The animation I chose to create was inspired by the idea of destruction and by my wanting to try out the fantastic new API of the gganimate package, and a very fast new gif encoder, gifski.

I am not a designer nor an artist, but I like imagining new animations to announce the book winner, that I want to be fun, and useful by illustrating the use of some nifty R tools. That’s a good way for me and you to learn new R skills! I’ve reported on my efforts with magick to create a crystal ball for chibi Steph, R package for image manipulation, and with particles, R package for simulating, well, particles, to move followers’ names around!.

This month I thought about what destruction meant to me, and got the rather simple idea to have glass shatter, thus revealing the winner’s name.

Feel free to skip over this section and go to the code directly, unless you want to know more about my search and process to write said code!

I wondered how I could shatter glass, i.e. cut a blue square in small pieces and make these pieces fall and somehow ended up thinking that I could draw random points and then use their Voronoi tiles as fragments. I’m not sure glass shattering really looks like this but I decided against experimenting with my windows.

I wasn’t too sure how to make and use the fragments though. I re-read Thomas Lin Pedersen’s excellent post about making a Christmas card using Voronoi tesselation but as nice and well-explained as it was, it wasn’t exactly what I was after. I set out to find a way to create Voronoi tiles as polygons and then to shift at least some of them downwards to create the animation.

I tried using the deldir package a bit to create Voronoi tesselation out of random points but its output didn’t make me too happy since it was segments rather than polygons. Re-creating polygons from them didn’t sound too easy. I ditched this idea and googled keywords such as Voronoi and polygons and R and then sf the geospatial package since the first results indicated Voronoi tesselation was well supported for mapping stuff, and found this very useful Stack Overflow post that made me adapt an example from sf documentation.

Once I had the polygons, the rest was animation as usual, well not really, since gganimate new API by Thomas Lin Pedersen is different, and so powerful! I haven’t watched Thomas’ useR! keynote talk about The Grammar of animation yet, but I look forward to it since I both admire his packages, and his blog writing. I only use very basic gganimate stuff here. A good intro to its grammar can be found at https://github.com/thomasp85/gganimate/tree/master#gganimate- .

I was also glad to read that gifski is now on CRAN for all your gif making needs (it’s actually the default gif renderer of gganimate). This package by Jeroen Ooms is not only a wrapper to the fastest gif renderer around which is cool enough in itself, but also the first CRAN package that interfaces a Rust library! Read more about gifski in this tech note.

The first part of the code consisted of drawing random points and using them as a basis for a Voronoi tesselation inside a box.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# adapted from https://r-spatial.github.io/sf/reference/geos_unary.html
lims <- c(0, 10)
# sample 200 points inside an environment
# with a fix seed
with(set.seed(42),
 points <- sf::st_multipoint(matrix(runif(n = 400, lims[1],
 lims[2]),,2)))

# get a square box that'll be the area of our animation 
box <- sf::st_polygon(list(rbind(c(lims[1], lims[1]),
 c(lims[2], lims[1]),
 c(lims[2], lims[2]),
 c(lims[1], lims[2]),
 c(lims[1], lims[1]))))

# Get the Voronoi polygons of the points
v <- sf::st_sfc(sf::st_voronoi(points, sf::st_sfc(box)))
# Keep only the part of them inside the box
voronoi <- sf::st_intersection(sf::st_cast(v), box)

# Get a data.frame with the polygons
get_df_from_polygon <- function(polygon, index){
 mat <- as.matrix(polygon)
 tibble::tibble(x = mat[,1],
 y = mat[,2],
 tile = index)
}

df <- purrr::map2_df(voronoi, 1:length(voronoi),
 get_df_from_polygon)

The animation will consist of two parts:

  • The fragments appearing by drawing their borders with different shades from blue like the background to white.

  • The fragments falling.

The code below creates the PNGs corresponding to the first part of the animation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fs::dir_create("august_frames")

plot_one <- function(step, df){
 cols <- colorRampPalette(c("#2165B6", "white"))(10)
 ggplot(df) +
 geom_polygon(aes(x = x, y = y, group = tile),
 fill = "#2165B6", col = cols[step]) +
 theme_void()

 outfil <- file.path("august_frames",
 sprintf("plot1_%02d.png", step))
 ggsave(outfil, width = 6.7, height = 6.7)
 magick::image_read(outfil) %>%
 magick::image_resize("480x480") %>%
 magick::image_write(outfil)


}


purrr::walk(1:10, plot_one, df)

Then I made the data.frame a bit bigger by adding a second state with most tiles fallen at the bottom. I first create a variable border that indicates if the tile pertains to the sides or top because magically these tiles won’t fall.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
df <- dplyr::group_by(df, tile) %>%
 dplyr::mutate(border = any(x %in% lims) |
 any(y == lims[2]))

# Define the second state with tiles fallen
df2 <- df
# a bit random but it does look ok!

df2 <- dplyr::group_by(df2, tile) %>%
 dplyr::mutate(y = ifelse(!border,
 -2 - min(y) + y,
 y)) %>%
 dplyr::ungroup()

df$frame <- 1
df2$frame <- 2
dfall <- dplyr::bind_rows(df, df2)

This was followed by the animating of the second part with gganimate, here after creating a fake winner (sorry Steph, you won’t get to gift yourself the book). Because we’ll need to use PNGs from the first part, we don’t use gganimate’s built-in gif or video renderers, but instead save PNGs.

Important note: gganimate latest version isn’t on CRAN yet, follow the instructions in its GitHub repository or download a built source version of the package from Appveyor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
winner <- list(name = c("Steph Locke"))
p <- ggplot(dfall) +
 annotate("text", label = winner$name,
 x = 5, y = 5,
 size = 12, family = "Roboto",
 col = "#E8830C") +
 geom_polygon(aes(x = x, y = y, group = tile),
 fill = "#2165B6", col = "white") +
 # Here comes the gganimate code
 # create transition, with the variable frame
 transition_states(
 frame,
 transition_length = 1,
 state_length = 1,
 wrap = FALSE
 ) +
 # use an ease that makes tiles fall faster
 # at the end of the transition
 ease_aes('exponential-in') +
 theme_void() +
 coord_cartesian(xlim = lims, ylim = lims)

 # create and save frames

 animate(p,
 renderer = file_renderer(dir = "august_frames",
 prefix = "plot2_") )

Now we can use all the PNGs that are in the “august_frames” folder, with gifski!

1
2
3
4
5
6
7
8
9
10
11
images <- sort(as.character(
 fs::dir_ls("august_frames")))

gifski::gifski(images,
 gif_file = path,
 delay = 0.1,
 width = 480,
 height = 480)

# clean
 fs::dir_delete("august_frames")

And voilà, a glass shattering animation!

Glass shattering book winner reveal

In this post I explained how I created an animation of glass shattering.

R packages that are important to know for producing animations are:

Have fun animating R visualizations! And don’t forget that next month, again a random Locke Data follower will win a great book: you should follow Locke Data on Twitter to be in with a chance of winning!