Creating and combining multiple animations with gganimate and magick

Published

September 13, 2020

Credit

Thanks to Robert Walker for showing how to combine multiple animations using the magick package in R.

Animating cars

In a previous blogpost, I showed how to create a new geom to plot cars as seen from top and rear. This post is about animating them in a way to synchronie their timing.

gganimate package is a powerful animation package in R, but as of this writing, it does not have a function to combine multiple animations. So, we’ll use the magick package to combine the animations.

Load data

I am using a dataset that contains car positions, speed and distance (also called as spacing).

suppressPackageStartupMessages( library(tidyverse) )
suppressPackageStartupMessages( library(magick) )
suppressPackageStartupMessages( library(gganimate) )
suppressPackageStartupMessages( library(here) )
suppressPackageStartupMessages( library(readr) )

df <- read_csv("vb.csv")

-- Column specification --------------------------------------------------------
cols(
  Time_s = col_double(),
  ED_x_m = col_double(),
  LV_x_m = col_double(),
  LV_length_m = col_double(),
  LV_width_m = col_double(),
  visual_angle_W = col_double(),
  visual_angle_H = col_double(),
  tau = col_double(),
  ED_speed_mps = col_double(),
  LV_speed_mps = col_double(),
  LV_frspacing_m = col_double(),
  ED_gas_pedal_pos = col_double(),
  ED_brake_pedal_force_kg = col_double(),
  tau_inv = col_double()
)
df
# A tibble: 49 x 14
   Time_s ED_x_m LV_x_m LV_lengt~1 LV_wi~2 visua~3 visua~4   tau ED_sp~5 LV_sp~6
    <dbl>  <dbl>  <dbl>      <dbl>   <dbl>   <dbl>   <dbl> <dbl>   <dbl>   <dbl>
 1      1  4341.  3991.       5.39    2.29 0.00185 0.00128  55.1    22.4       0
 2      2  4335.  3991.       5.39    2.29 0.00189 0.00130  52.8    22.9       0
 3      3  4329.  3991.       5.39    2.29 0.00193 0.00133  50.7    23.5       0
 4      4  4322.  3991.       5.39    2.29 0.00197 0.00136  48.6    24.0       0
 5      5  4315.  3991.       5.39    2.29 0.00201 0.00138  46.9    24.3       0
 6      6  4308.  3991.       5.39    2.29 0.00205 0.00141  45.3    24.7       0
 7      7  4302.  3991.       5.39    2.29 0.00210 0.00145  43.7    25         0
 8      8  4294.  3991.       5.39    2.29 0.00215 0.00148  42.1    25.3       0
 9      9  4287.  3991.       5.39    2.29 0.00220 0.00152  40.6    25.7       0
10     10  4280.  3991.       5.39    2.29 0.00226 0.00156  39.1    26.0       0
# ... with 39 more rows, 4 more variables: LV_frspacing_m <dbl>,
#   ED_gas_pedal_pos <dbl>, ED_brake_pedal_force_kg <dbl>, tau_inv <dbl>, and
#   abbreviated variable names 1: LV_length_m, 2: LV_width_m,
#   3: visual_angle_W, 4: visual_angle_H, 5: ED_speed_mps, 6: LV_speed_mps
# i Use `print(n = ...)` to see more rows, and `colnames()` to see all variable names

As discussed in the previous blogpost, I had to adjust the coordinates to make one car appear approaching another car. So, we estimate the first and last coordinates in the data to do that.

# Largest x coordinate of following vehicle------------------
  first_ed_x_coord <- df %>% pull(ED_x_m) %>% range() %>% tail(1)
  last_ed_x_coord <- df %>% pull(ED_x_m) %>% range() %>% head(1)

Creating animations with gganimate

Now, the following code shows three animations created using gganimate.

Top view

# animation of position---------------------------------------
  ani_car_b <- ggplot(df) +
    geom_car(aes(x=abs(ED_x_m-first_ed_x_coord), 
                 y=first_ed_x_coord-last_ed_x_coord, length=4.64, width=2.078, fill="ed")) +
    geom_text(aes(x=abs(ED_x_m-first_ed_x_coord), y = (first_ed_x_coord-last_ed_x_coord)+3,
                  label = paste("Following Car\nSpeed =", ED_speed_mps, "m/s")),
              color="darkgray"
    ) +
    geom_car(aes(x=abs(LV_x_m-first_ed_x_coord), y=first_ed_x_coord-last_ed_x_coord, length=LV_length_m,
                 width=LV_width_m, fill="lv")) +
    geom_text(aes(x=abs(LV_x_m-first_ed_x_coord), y = (first_ed_x_coord-last_ed_x_coord)+3, 
                  label = paste("Lead Car\nSpeed =", LV_speed_mps, "m/s")),
              color="darkgray") +
    
    geom_segment(aes(x = abs(ED_x_m-first_ed_x_coord)+(0.5*4.64),
                     xend = abs(LV_x_m-first_ed_x_coord)-(0.5*LV_length_m),
                     y = first_ed_x_coord-last_ed_x_coord,
                     yend= first_ed_x_coord-last_ed_x_coord), 
                 arrow = arrow(length = unit(0.1, "inches"), ends = "both")) +
    geom_text(aes(x= ((abs(ED_x_m-first_ed_x_coord)+(0.5*4.64))+(abs(LV_x_m-first_ed_x_coord)-(0.5*LV_length_m)))/2, y = (first_ed_x_coord-last_ed_x_coord)+3.5, 
                  label = paste("Spacing =", LV_frspacing_m, "m")),
              color="darkgray") +
    
    coord_equal(ratio=0.7) +
    scale_fill_manual(values = c("blue", "black")) +
    theme_void() +
    theme(legend.position = "none",
          axis.text = element_blank(),
          axis.title = element_blank(),
          axis.ticks = element_blank()
    ) +
    transition_manual(Time_s) +
    ease_aes() +
    view_follow()

ani_car_b
nframes and fps adjusted to match transition

Pedal positions and inverse time-to-collision

The following animation shows how the gas and brake pedal positions change over time. The variable tau-inverse represents the inverse of time-to-collision (seconds until collision happens if cars don’t change their speeds).

# animation of pedals/looming---------------------------------------
pedals_pos_b <- ggplot(data = df,
                         mapping = aes(x = Time_s)) +
    geom_line(aes(y = tau_inv*10), color = "black") +
    geom_text(aes(y = tau_inv*10,
                  label = paste("tau-inv =", round(tau_inv, 2))), color = "black",
              size = 5) +
    geom_area(aes(y = tau_inv*10), fill = "gray",
              position = "identity", alpha=0.6)+
    
    geom_line(aes(y = scale(ED_gas_pedal_pos)), color = "darkgreen") +
    geom_text(aes(y = scale(ED_gas_pedal_pos)),
              label = "Gas Pedal", color = "darkgreen") +
    
    geom_line(aes(y = scale(ED_brake_pedal_force_kg)), color = "red") +
    geom_text(aes(y = scale(ED_brake_pedal_force_kg)),
              label = "Brake Pedal", color = "red") +
    
    geom_text(x= 10, y = 4, 
              aes(label = paste("Brake Pedal Force =", 
                                round(ED_brake_pedal_force_kg), "kg"))) +
    theme_void() +
    transition_reveal(Time_s)
  

 pedals_pos_b

Driver’s front view

The following animation shows what the driver in the following car sees from the windscreen. It represents how the image of the lead vehicle grows on the following driver’s retina.

# animation of car rear---------------------------------------
ani_retina_b <- ggplot(df ) +
    geom_car_rear(aes(x=0, y=0, length=visual_angle_W,
                      width=visual_angle_H), fill="black") +
    theme_void() +
    theme(axis.text = element_blank(),
          axis.title = element_blank(),
          axis.ticks = element_blank())+
    coord_fixed(ratio = 0.7) +
    transition_manual(Time_s) 
  
 ani_retina_b
nframes and fps adjusted to match transition

Rendering animations and saving them

Now that the three animations are created, we can render them using gganimate::animate function. Without doing this, the animations are rendered every time we call them.

# Rendering gifs-------------------------------------------------
  a_gif <- animate(ani_car_b, height = 3, width = 8, end_pause = 15,
                   units = "in", res = 150, fps = 5, duration=10)
  b_gif <- animate(pedals_pos_b, fps = 5, duration=10,
                   height = 3.5, width = 4.5, end_pause = 15,
                   units = "in", res = 150)
  c_gif <- animate(ani_retina_b, fps = 5, duration=10,
                   height = 3.5, width = 3.5, end_pause = 15,
                   units = "in", res = 150)
  
## saving  
anim_save(filename = "a_gif.gif",
          animation = a_gif)
anim_save(filename = "b_gif.gif",
          animation = b_gif)
anim_save(filename = "c_gif.gif",
          animation = c_gif)

Combining animations

To combine the above three animations, we are going to use the magick package.

Reading the gif images

# Convertig the rendered gifs to magick class-------------
  a_mgif <- image_read(path = "a_gif.gif")
  b_mgif <- image_read(path = "b_gif.gif")
  c_mgif <- image_read(path = "c_gif.gif")

By the reading the saved gif images (animations) using magick::image_read(), we convert the animation from the gif_image class to magick-image class.

Aligning the animations

Finally, we combine the animations using a for-loop and magick::image_append:

# Aligning gifs-------------------------------------------------
  bc_gif <- image_append(c(b_mgif[1], c_mgif[1]), stack = FALSE)
  for(i in 2:50){
    combined <- image_append(c(b_mgif[i], c_mgif[i]), stack = FALSE)
    bc_gif <- c(bc_gif, combined)
  }

  new_gif <- image_append(c(a_mgif[1], bc_gif[1]), stack = TRUE)
  for(i in 2:50){
    combined <- image_append(c(a_mgif[i], bc_gif[i]), stack = TRUE)
    new_gif <- c(new_gif, combined)
  }

 new_gif