Swim hamlet, swim!

Swim hamlet, swim!

Exploring gganimate

What: Using gganimate to create a Super Mario style hamlet video.

I quite liked watching the rstudio::conf2019 talk by Thomas Lin Pedersen where he introduces the new version of the gganimate. When he talked about the gganimate::view_follow() it reminded me quite a bit of old jump’n run games and I got tempted to try to play around with the package.

Of course I wanted to feature some hamlet stuff - my idea was to create some Super Mario style animation.

To do this I did a little preparation in Inkscape and created a svg file that would later represent the landscape our hamlet has to cross:

I exported the x/y node positions of the individual paths using a neat little Inkscape extension (by user Neon22).

I did the same for a slightly modified version of my signature hamlet where I opened all existing holes in the polygons.

Than it was time to start R:

library(tidyverse)
library(gganimate)
# We also import a small TV as top layer for the plots
screen <- hypoimg::hypo_read_svg('screen-cairo.svg')

We use a small configuration section to define the width our rolling window should have when scrolling through the landscape, as well as the distance between the individual stages of the animation (distance in nodes of the original path).

We also write a small import function for the node positions.

bin_W <- 250
step_W <- 20
pos_l <- 50
pos_r <- bin_W-pos_l

import_svg_coord <- function(file, shift = 0){
read_csv(file, col_names = c('x','y')) %>%
mutate(y=-y+max(y) + shift)
}

Then we load the coordinates of the hamlet, as well as for the different substrates and the swimming path.

fish <-read_tsv('draw_merged.tsv', col_names = c('x','y','id','type')) %>%
mutate(y=-y-min(y),
       y = y/max(x),
       y = y - mean(y),
       x= 1-x/max(x) -.5)

ground <- import_svg_coord('ground.csv', 10)
coral <- import_svg_coord('coral.csv', 10)
grass <- import_svg_coord('grass.csv', 10)
stone <- import_svg_coord('stones.csv', 10)
pth <- import_svg_coord('path.csv', 60)

As a first step of the animation, we divide the swimming path into chunks of 20 (step_W) nodes and assign stations - those are going to be the individual steps of the animation.

stations <- pth %>%
mutate(rn = row_number()) %>%
filter(rn > pos_l,
       rn < length(x)-pos_r) %>%
mutate(station = row_number() %/% step_W,
       is_station = (row_number() %% step_W) == floor(step_W/2)) %>%
filter(is_station,
       station > 1,
       station < 87)

Then, we need some functions to extract the region that is covered during each station.

# this function will exctract the spatial extend of each station
get_extent <- function(rn,station,...){
ground %>%
  filter(row_number() > (rn - pos_l),
         row_number() < (rn + pos_r)) %>%
  summarise(start = min(x),
            end = max(x)) %>%
  mutate(station = station)
}

# this function will exctract the nodes of the different substate types
get_ground <- function(rn,ground,station,...){
ground %>%
  filter(row_number() > (rn - pos_l),
         row_number() < (rn + pos_r)) %>%
  mutate(station = station)
}

# this function will exctract the spatial position of the hamlet for each station
get_location <- function(rn,station,...){
pth %>%
  filter(row_number() == rn)  %>%
  mutate(station = station)
}

Then, we transform the hamlet coordinates for each station. This will put the center of the hamlet onto the location of the selected node (pos_l).

get_fish <- function(x,y,station,fish_scale=1,...){
X <- x
Y <- y
fish %>%
  mutate(x = x*fish_scale,
         y = y*fish_scale,
         x = x+X,
         y = y+Y,
         station = station,
         station_id = str_c(station,id,sep = '-'))
}

Now, we apply the functions to generate the substrate and extend data sets (with assigned stations).

ground_stations <- stations %>%
purrr::pmap(get_ground, ground = ground) %>%
bind_rows()

coral_stations <- stations %>%
purrr::pmap(get_ground, ground = coral) %>%
bind_rows()

grass_stations <- stations %>%
purrr::pmap(get_ground, ground = grass) %>%
bind_rows()

stone_stations <- stations %>%
purrr::pmap(get_ground, ground = stone) %>%
bind_rows()

location_stations <- stations %>%
purrr::pmap(get_location) %>%
bind_rows()

extend_stations <- stations %>%
purrr::pmap(get_extent) %>%
bind_rows()

A quick summary of the compiled data sets:

ggplot()+
  coord_equal()+
  geom_area(data = ground, aes(x, y))+
  geom_path(data = pth, aes(x, y), color = 'red')+
  geom_point(data = location_stations,
             aes(x, y, group = station), color = 'red')

Next, we do the hamlet transformations:

fish_stations <- stations %>%
purrr::pmap(get_fish, fish_scale = 40) %>%
  bind_rows()

Finally it’s time to put the pieces together.

ggplot()+
  coord_equal()+
  # hamlets need water
  geom_rect(data = extend_stations, aes(xmin = start,
                                        xmax = end,
                                        ymin = 0,
                                        ymax = 400,
                                        group = station),
            fill = 'blue',alpha = .3)+
  # add substrates
  geom_area(data = stone_stations, aes(x,y,group=station), fill = '#b3b3b3')+
  geom_area(data = grass_stations, aes(x,y,group=station), fill = '#0db010')+
  geom_area(data = coral_stations, aes(x,y,group=station), fill = '#ffb867')+
  # add ground
  geom_area(data = ground_stations, aes(x,y,group=station))+
  # place hamlets
  geom_polygon(data = fish_stations, aes(x=x,y=y, group = station_id, fill = type))+
  # make up
  scale_fill_manual(values = c('1_bg' = "#D3D3D2",
                               '2_line' = 'black'),
                    guide=FALSE)+
  # stage
  theme_void()+
  # "cut !"
  annotation_custom(grob = screen) +
  transition_time(station) +
  gganimate::view_follow()

© 2021. All rights reserved. KH.