Here's the result. Explanation below:

Any hacking with grobs should done after the individual frames of the animated plot have been created, but before they get drawn on the relevant graphics device. This window occurs in the plot_frame function of gganimate:::Scene.
We can define our own version of Scene that inherits from the original, but uses a modified plot_frame function with the grob hack lines inserted:
Scene2 <- ggproto(
"Scene2",
gganimate:::Scene,
plot_frame = function(self, plot, i, newpage = is.null(vp),
vp = NULL, widths = NULL, heights = NULL, ...) {
plot <- self$get_frame(plot, i)
plot <- ggplot_gtable(plot)
# insert changes here
plot$layout[which(plot$layout$name == "title"), c("l", "r")] <- c(2, max(plot$layout$r))
plot$layout[which(plot$layout$name == "subtitle"), c("l", "r")] <- c(2, max(plot$layout$r))
if (!is.null(widths)) plot$widths <- widths
if (!is.null(heights)) plot$heights <- heights
if (newpage) grid::grid.newpage()
grDevices::recordGraphics(
requireNamespace("gganimate", quietly = TRUE),
list(),
getNamespace("gganimate")
)
if (is.null(vp)) {
grid::grid.draw(plot)
} else {
if (is.character(vp)) seekViewport(vp)
else pushViewport(vp)
grid::grid.draw(plot)
upViewport()
}
invisible(NULL)
})
Thereafter, we have to replace Scene with our version Scene2 in the animation process. I've listed two approaches below:
Define a separate animation function, animate2, plus intermediate functions as required to use Scene2 instead of Scene. This is safer, in my opinion, as it doesn't change anything in the gganimate package. However, it does involve more code, & could potentially break in the future if the function definitions change at the source.
Over-write existing functions in the gganimate package for this session (based on the answer here). This requires manual effort each session, but the actual code changes required are very small, & probably won't break as easily. However, it also carries the risk of confusing the user, since the same function could lead to different results, depending on whether it's called before or after the change.
Approach 1
Define functions:
library(magrittr)
create_scene2 <- function(transition, view, shadow, ease, transmuters, nframes) {
if (is.null(nframes)) nframes <- 100
ggproto(NULL, Scene2, transition = transition,
view = view, shadow = shadow, ease = ease,
transmuters = transmuters, nframes = nframes)
}
ggplot_build2 <- gganimate:::ggplot_build.gganim
body(ggplot_build2) <- body(ggplot_build2) %>%
as.list() %>%
inset2(4,
quote(scene <- create_scene2(plot$transition, plot$view, plot$shadow,
plot$ease, plot$transmuters, plot$nframes))) %>%
as.call()
prerender2 <- gganimate:::prerender
body(prerender2) <- body(prerender2) %>%
as.list() %>%
inset2(3,
quote(ggplot_build2(plot))) %>%
as.call()
animate2 <- gganimate:::animate.gganim
body(animate2) <- body(animate2) %>%
as.list() %>%
inset2(7,
quote(plot <- prerender2(plot, nframes_total))) %>%
as.call()
Usage:
animate2(static_plot +
transition_states(Species,
transition_length = 3,
state_length = 1))
Approach 2
Run trace(gganimate:::create_scene, edit=TRUE) in console, & change Scene to Scene2 in the popup edit window.
Usage:
animate(static_plot +
transition_states(Species,
transition_length = 3,
state_length = 1))
(Results from both approaches are the same.)