This is largely adapted from Danielle Navarro’s “Art from Code” lessons, that go into a lot more detail: https://art-from-code.netlify.app/


Setup a grid of points, or any set of x and y coordinates

smol_grid <- long_grid(x = 1:20, y = 1:20)

Generate gradient noise for every point on the grid. It represents the vectors that point along the highest gradients in a noise field.

The gradient noise isn’t actually needed in the final plot but it helps in illustrating what is happening.

smol_slope <- gradient_noise(
  generator = gen_simplex,
  seed = 1,
  frequency = .1,
  x = smol_grid$x,
  y = smol_grid$y
)

Generate curl noise vectors for every point on the grid, these are perpendicular to the gradient noise vectors and follow the lines of zero gradient in the field.

smol_curl <- curl_noise(
  generator = gen_simplex,
  seed = 1,
  frequency = .1,
  x = smol_grid$x,
  y = smol_grid$y
)

Combine grid, and noise vectors into a single data frame

smol_field <- smol_grid %>%
  mutate(
    z = gen_simplex(x, y, seed = 1, frequency = .1),
    slope_x = smol_slope$x,
    slope_y = smol_slope$y,
    curl_x = smol_curl$x,
    curl_y = smol_curl$y,
  )

Plot it with a color map for the “terrain” (the noise field) and arrows for the gradient and curl vectors.

ggplot(smol_field) +
  geom_contour_filled( aes(x, y, z = z), show.legend = FALSE, bins = 20) +
  geom_segment(
    mapping = aes(
      x = x, 
      y = y, 
      xend = x + slope_x * 2, 
      yend = y + slope_y * 2
    ), 
    colour = "white", 
    arrow = arrow(length = unit(0.1, "cm"))
  ) +
  geom_segment(
    mapping = aes(
      x = x, 
      y = y, 
      xend = x + curl_x * 2, 
      yend = y + curl_y * 2
    ), 
    colour = "black", 
    arrow = arrow(length = unit(0.1, "cm"))
  ) +
  theme_minimal() +
  coord_equal()

Placing points in such a field and letting it move along the curl vectors results in interesting paths. Move the points in a loop and for each iteration, save a copy of their current positions to a new data frame (traces) that can then be plotted to show the path each point has taken.

Add an identifier (path_id) to each point so that you can later group the traces into distinct lines.

tribble(
  ~x, ~y, ~path_id,
   1,  1,        1,
   2,  1,        2,
   3,  1,        3
) -> points

traces <- tibble()

for(i in 1:1000) {
  
  curl <- curl_noise(
    generator = gen_simplex,
    seed = 1,
    frequency = .1,
    x = points$x,
    y = points$y
  )
  
  points %>% mutate(
    x = x + curl$x * 0.5,
    y = y + curl$y * 0.5,
  ) -> points
  
  points %>% bind_rows(traces) -> traces
}

traces %>% ggplot(aes(x = x, y = y, group = path_id)) +
  geom_path() +
  coord_equal() +
  theme_minimal()

Add come color and an alpha channel that fades with the number of iterations, remove the plot axes, and play around with all the values.

library(scico)

tibble(
  x = 1:50,
  y = 0,
  path_id = 1:50
) -> points

traces <- tibble()

for(i in 1:2000) {
  
  curl <- curl_noise(
    generator = gen_simplex,
    seed = 1,
    frequency = .2,
    x = points$x,
    y = points$y
  )
  
  points %>% mutate(
    x = x + curl$x * 0.01,
    y = y + curl$y * 0.01,
    iteration = i
  ) -> points
  
  points %>% bind_rows(traces) -> traces
}

theme_void() %+replace% theme(
  legend.position="none",
  panel.background = element_rect(fill="#000000")
) -> th

traces %>%
  ggplot(aes(
    x = x,
    y = y,
    group = path_id,
    color = path_id,
    alpha = iteration
  )) +
  geom_path() +
  scale_color_scico(palette = "roma") +
  xlim(c(-10,60)) +
  ylim(c(-20, 20)) +
  th