Drawing with epicycles in R

Drawing with epicycles

  • Create a set of points to define the path
  • Take the discrete fourier transform of the points (as represented in the complex plane)
  • The magnitude and phase of the fourier transform components are the radius and phase offset of the circles to draw
library(ggplot2)
library(gganimate)
library(ggforce) # geom_circle
library(dplyr)
library(hershey) # vector font: https://github.com/coolbutuseless/hershey

Extract a set of points for the letter R

This uses the Hershey vector fonts.

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Choose a character from a Hershey vector font and extract x and y coords
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
char <- hershey::hershey %>%
  filter(font == 'futural', char == 'R')

x <- char$x
y <- char$y

FFT of the (x,y) coordinates

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Calculate a fourier transform of the points in the complex plane
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
f <- fft(complex(real = x, imaginary = y))


#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# From the FFT, calculate
# -  R     - radius
# -  phi   - phase shift
# -  omega - angular velocity
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
N     <- length(x)
R     <- Mod(f) / N
phi   <- Arg(f)
omega <- 2 * pi * seq(0, N-1) / N

Construct the epicycles at each timestep

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Debug/Sanity check:
# Recreate the original points by manually doing the inverse fourier transform.
# Generate points with timestamp so we can highlight in gganimate
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
xs <- c()
ys <- c()

for (t in seq(0, N)) {
  xo <- R * cos(omega * t + phi)
  yo <- R * sin(omega * t + phi)
  
  xs <- c(xs, sum(xo))
  ys <- c(ys, sum(yo))
}

rpoints <- data.frame(x = xs, y = ys, t = as.numeric(seq(0, N)))


#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Create a data.frame of the cumulative sum of the components at
# each timepoint.  This will give us the position of each of the circle
# centres at each time point
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
reconstruction <- list()

F <- 40  # how many steps between each input point

for (t in seq(0, N * F)) {
  t  <- t/F
  xo <- R * cos(omega * t + phi)
  yo <- R * sin(omega * t + phi)
  
  xc <- cumsum(xo)
  yc <- cumsum(yo)
  
  reconstruction[[length(reconstruction) + 1]] <- data.frame(x = xc, y = yc, t = t, R = R)
}

reconstruction <- do.call(rbind, reconstruction)

centres <- reconstruction %>%
  group_by(t) %>%
  mutate(R = lead(R)) %>%
  ungroup()

Plot epicycles at a single instant

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Plot
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
cdf <- reconstruction %>% filter(t == 0)
centres <- centres %>% filter(t == 0)
p <- ggplot() +
  geom_path(data = cdf, aes(x, y), alpha = 0.3) +
  geom_circle(data = centres, aes(x0 = x, y0 = y, r = R), na.rm = TRUE, colour = '#00000030') +
  geom_path(data = char, aes(x, y, group = stroke), size = 0.5, colour = 'black') +
  geom_point(data = char, aes(x, y), size = 1, colour = 'black') +
  geom_point(data = rpoints, aes(x, y), size = 3, colour = 'red') +
  theme_void() +
  coord_equal()

p

Animate the epicycles

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Plot
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
p <- ggplot() +
  geom_path(data = reconstruction, aes(x, y), alpha = 0.3) +
  geom_circle(data = centres, aes(x0 = x, y0 = y, r = R), na.rm = TRUE, colour = '#00000030') +
  geom_path(data = char, aes(x, y, group = stroke), size = 0.5, colour = 'black') +
  geom_point(data = char, aes(x, y), size = 1, colour = 'black') +
  geom_point(data = rpoints, aes(x, y), size = 3, colour = 'red') +
  theme_void() +
  coord_equal()
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# animate
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ga <- p + transition_manual(t)
a <- gganimate::animate(ga, nframes = N*F+1, fps = 20)
gganimate::anim_save("r.gif", a)