Code
library(dplyr)
library(sf)
library(pracma)
library(ggplot2)
library(gganimate)
library(patchwork)
set.seed(0)- 1
- Useful additional matrix functions.
- 2
-
ggplotanimations.
30 Day Map Challenge, 2026 (but not really)
David O’Sullivan
November 3, 2026
The Bridges conference series is one of those things I’d like to have got around to when I was still being paid to be an academic. The work presented there lies at the intersection of art and mathematics. I’ve had reason to read papers on the conference archive and if you have even a passing interest in this sort of thing I recommend you do the same.
Anyway, on a recent poke around the archive I came across this paper by Kerry Mitchell:
Mitchell K. 2021. Iterated averaging of polygon vertices. In Proceedings of Bridges 2021: Mathematics, Art, Music, Architecture, Culture, ed. D Swart, F Farris, and E Torrence, 197–204. Phoenix, Arizona: Tessellations Publishing.
As the title makes clear, this paper presents a procedure where consecutive vertices of a polygon are iteratively averaged to produce a series of polygons. Overlaying the resulting polygons makes pretty pictures.
Needless to say, on encountering this I implemented it in Netlogo. You can download that model here if you’re interested, but R is perhaps a more generally useful language, so I thought I’d make some code to make the pictures in R also. And since today is Day 3 of the 30 Day Map Challenge themed ‘Polygons’ and this process makes lots of (not really) polygons and maps plots them, well… why not?
One important caveat is that these aren’t really polygons, because, as we’ll see, they’re tangled up messes of points connected. The edges intersect, and they generally fail any st_is_valid() tests for that reason. We don’t really care about this, because what we’re interested in is making pretty pictures, but it bears mentioning before anyone gets mad at me for calling these things polygons.
As usual, some packages:
ggplot animations.
The core procedures in iteratively averaging the vertices of a polygon are (i) averaging consecutive vertices1, and (ii) recentring and rescaling the resulting polygon so that it doesn’t collapse to a point (more on that rabbithole in a moment).
Because sf is a thin wrapper around matrices, it’s more convenient to do the manipulations of vertices based on a two-column matrix of the corners of a polygon, than on an sf polygon.
pracma::conv is a convenient way to average consecutive values in each column of the matrix.
head and tail operations clip the matrix back to its original size so we don’t unintentionally add extra corners.
Here’s an example of this function in operation:
[,1] [,2]
[1,] 1 2
[2,] 3 4
[3,] 5 6
[4,] 7 8
[5,] 9 10
[,1] [,2]
[2,] 2 3
[3,] 4 5
[4,] 6 7
[5,] 8 9
[6,] 5 6
Note in particular how the last row in the result is the average of the last row and first row of the original matrix. The x parameter in average_corners() allows for making the new corners closer to the first or last corner of each pair, although we won’t use that here.
Next, some functions to recentre and rescale the resulting polygons. If we don’t do this then polygons will collapse to a point.
Iterated averaging of the corners of a polygon is a process, almost but not quite the same as the mice problem which posits the question of what pattern we get if mice each position at the corner of a regular polygon head towards one another at constant speed. Well… not quite towards one another, but towards where they think the mouse they are pursuing will be by the time they get there. The result is a sequence of copies of the original polygon decreasing in size, and with the mice following logarithmic spiral curves.
Turns out this is a bit a rabbithole. Such pursuit problems started out as ‘the problem of the three dogs’ posed by Lucas in 1877, and have been of interest (inevitably) in military contexts where they relate to questions of how to aim missiles at moving targets.2 Less militaristically,3 pursuit problems also arise in coordinating autonomous vehicles to conform to desired formations or patterns, as in drone light shows.
Anyway, to recentre and rescale polygons (again, handled as a two-column matrix of points) we have the following function.
get_centre <- function(m) {
m |>
apply(2, range) |>
apply(2, mean)
}
mean_distance_to_centre <- function(m, centre) {
(m - centre)^2 |>
apply(1, sum) |>
sqrt() |>
mean()
}
recentre <- function(m, r = 1) {
centre <- m |> get_centre()
d <- mean_distance_to_centre(m, centre)
(m - centre) * r / d + centre
}r parameter.
We eventually want to convert these matrices to polygons, so we need this function:
Finally, we can put all these together to get a collection of polygons starting from a random collection of points and iteratively averaging the corners.
get_polygons <- function(n_sides, n_iterations = n_sides * 2, x = 0.5) {
m <-
matrix(rnorm(2 * n_sides), ncol = 2) |>
recentre()
m_list <- list(m)
for (i in 1:n_iterations) {
m_list[[i + 1]] <-
m_list[[i]] |>
average_corners(x = x) |>
recentre()
}
m_list |>
lapply(matrix_as_polygon) |>
cbind() |>
st_as_sfc() |>
data.frame() |>
st_sf()
}The first few polygons will be very messy, so it makes sense to drop a few, for our purposes.
And finally we can plot one. These are really pretty if we draw the polygon unfilled, using a low opacity colour.
Using gganimate::animate we can animate the sequence of polygons to get a better feel for the process.
Finally, here are some more examples.
Duh.↩︎
Absolutely the most crazily named ‘problem’ in this realm is the homicidal chauffeur problem, so named (according to wikipedia) as an “unclassified proxy for missile defence” problems!↩︎
Although given recent developments in drone warfare perhaps not very much less so…↩︎