vignettes/HowToUse/spiro.Rmd
spiro.Rmd
If you do not have the remotes
package, install it from CRAN by running this code:
install.packages("remotes")
Install spiro from GitHub by running this code:
remotes::install_github("wjschne/spiro")
Software to make digital spirographs is not hard to find, but I had several reasons to make my own spirograph generator using R:
Enter the spiro package. It can do traditional spirographs like this one:
library(spiro)
library(magrittr)
spiro(
fixed_radius = 7,
cycling_radius = 6,
pen_radius = 2,
draw_fills = F,
file = "simple_spiro.svg"
)
The spiro package can take things a little further than traditional spirographs:
spiro(
fixed_radius = 1231,
cycling_radius = 529,
pen_radius = 1233,
color_groups = 67,
color_cycles = 59,
windings = 96,
points_per_polygon = 50,
file = "viridis_weave.svg",
colors = scales::viridis_pal()(67)
) %>%
add_background(color = "black")
A spirograph is made with a pen attached to a circle that rolls around another circle:
The primary function of the spiro package is the the spiro function. By default, it saves an .svg file to your working directory and opens the file in the program that opens .svg files by default. I recommend setting the default .svg viewer to Google Chrome so that you can see the animations that spiro can add to your spirographs. To change the default viewer in Windows see here. For Mac.
The basic parameters to play with on a spirograph are the radius of the fixed circle, the radius of the moving circle, and the pen placement radius inside the moving circle. To make the spirograph from the animation, the radius of the fixed circle is 3, the radius of the cycling circle is 1, and the radius of the pen placement is 2. Technically, this is a hypotrochoid because the moving circle is inside the fixed circle.
spiro(
fixed_radius = 3,
cycling_radius = 1,
pen_radius = 2,
file = "hypotrochoid.svg")
An epitrochoid is made with the moving circle outside the fixed circle.
To make an epitrochoid, set the cycling_radius
parameter to a negative value:
spiro(
fixed_radius = 3,
cycling_radius = -1,
pen_radius = 2,
file = "epitrochoid.svg")
In the animations above, the spirographs were complete after the small circle wound around the larger circle just once. Depending on the ratio of the fixed_radius
to the cycling_radius
, some spirographs need to wind more than once before the spirograph repeats itself. If you can reduce the fraction of cycling_radius
/ fixed_radius
such that both the numerator and denominator are integers with no common factors, the reduced numerator is the number of times the spirograph winds before repeating.
By default, the number of windings is set to be equal to the cycling_radius
. This means that you might accidentally make a spirograph that has repeated several. If the spirograph is blank, it is likely that the spirograph has repeated an even number of times. For example, if the spirograph repeats an even number of times, the fills will appear to be blank. For example, fixed_radius
= 3, cycling_radius
= 1 will produce a deltoid. If these parameters are doubled to 6 and 2, respectively, the shape will appear the same, but the fills will double up and disappear. However, if you set draw_fills
to FALSE
(or set the fill rule
parameter is set to “winding”), you will see that the spirograph is there.
If you set either the fixed_radius
or the cycling_radius
to an irrational number (e.g., the square root of 2), the spirograph will never repeat itself—in theory. In practice, however, computers round irrational numbers, and thus the spirograph will repeat itself eventually. By using irrational numbers in either or both circles’ radii, you can see that playing with with the windings
parameter can give startlingly different results.
Same radii but with more windings:
If you want just lines instead of fills, set draw_fills
to FALSE:
spiro(
fixed_radius = 3,
cycling_radius = -1,
pen_radius = 2,
draw_fills = FALSE,
file = "epitrochoid_lines.svg")
If you want lines and fills together, draw a filled spirograph, and then color the lines with the add_lines
function.
Because the spiro function outputs the name of the file it creates and the first parameter of the add_lines
function is the name of the input file, it is possible to to use the add_lines
function with a pipe function (%>%
) workflow.
The figure can be split into multiple parts. Setting the color_groups
parameter to 2, splits the figure in half:
spiro(
fixed_radius = 6,
cycling_radius = 5,
pen_radius = 5,
color_groups = 2,
file = "six_points.svg")
By default, the colors are selected from the viridis palette via the scales
package. If you want to select different colors, you can do so directly with a vector of colors:
spiro(
fixed_radius = 6,
cycling_radius = 5,
pen_radius = 5,
color_groups = 2,
colors = c("blue", "black"),
file = "six_points_blueblack.svg")
You can use any color palette as well. Here we use the rainbow palette:
spiro(
fixed_radius = 6,
cycling_radius = 5,
pen_radius = 5,
color_groups = 12,
colors = rainbow(12),
file = "six_points_rainbow.svg")
A great list of color palettes and functions can be found in Emil Hvitfeldt’s paletteer package.
Imagine that an image has 30 points and 5 colors. This means that there are 5 groups of 6 points to make 6-sided polygons (or lines). If you set the color_cycles
parameter to 2, spiro will cycle through the 5 colors twice. Thus, there will be 10 groups of 3 points each to create polygons (or lines). There will be 2 polygons of each color.
You can recycle the colors as many times as you wish. Here are 12 colors recycled 3 times (i.e., color_cycles
= 3). Thus, there are 3 × 12 = 36 segments in this spirograph.
spiro(
fixed_radius = 6,
cycling_radius = 5,
pen_radius = 5,
color_groups = 12,
colors = rainbow(12),
color_cycles = 3,
points_per_polygon = 80,
file = "six_points_rainbow_3.svg")
Recycling colors can dramatically change the look of the figure. Here is a single cycle of 5 colors:
spiro(
fixed_radius = 121,
cycling_radius = 100,
pen_radius = 13,
color_groups = 5,
color_cycles = 1,
points_per_polygon = 1000,
transparency = .5,
file = "blurry_checkers.svg"
) %>%
add_background(color = "black")
Here is the spirograph with the same 5 colors recycled 200 times:
spiro(
fixed_radius = 121,
cycling_radius = 100,
pen_radius = 13,
color_groups = 5,
color_cycles = 200,
points_per_polygon = 20,
transparency = 0.5,
file = "blurry_circles.svg"
) %>%
add_background(color = "black")
Notice that the points_per_polygon
was set to a lower value in the example above. By default, each recycled group has 1000 points each time the figure winds around the fixed circle. By default, the number of windings is equal to the cycling radius. In this example, that would mean 5 color groups × 200 color cycles × 100 windings, × 1000 points = 100 million points. By setting points_per_polygon
to 20, there are now “only” 5 color groups × 200 color cycles × 20 points = 20,000 points. This means that the file size will be much smaller and the image quality is not noticeably reduced.
You can set the background using the pipe (%>%
) operator and the add_background
rainbow_colors <- hsv(
h = seq(1 / 16, 1, length.out = 16),
s = 0.7,
v = 0.7)
spiro(
fixed_radius = 16,
cycling_radius = 5,
pen_radius = 5,
file = "rainbowspikes.svg",
color_groups = 16,
color_cycles = 2,
points_per_polygon = 50,
colors = rainbow_colors
) %>%
add_background(color = "gray80")
You can round the corners if you wish. Rounding ranges from 0 to 1, with 0 being a square and 1 being a circle. Here the corners are set so that 10% of each side is rounded.
spiro(
fixed_radius = 16,
cycling_radius = 5,
pen_radius = 5,
file = "rainbowspikes_rounded.svg",
color_groups = 16,
color_cycles = 2,
points_per_polygon = 50,
colors = rainbow_colors
) %>%
add_background(color = "gray80", rounding = 0.1)
If you want a radial gradient background, use the add_background_gradient
function. You can set the colors and the stops. If you do not set the stops manually, they will be evenly spaced from 0 to 1.
spiro(
fixed_radius = 21,
cycling_radius = -20,
pen_radius = 35,
transparency = 1,
colors = "black",
file = "blue_gem.svg"
) %>%
add_lines(colors = "white") %>%
add_background_gradient(
colors = c(
rep(c("lightcyan2", "royalblue4"), 9),
rep("white",2)),
rounding = 1,
radius = 1)
Here is an example of manually setting gradient stops:
spiro(
fixed_radius = 16,
cycling_radius = 5,
pen_radius = 5,
file = "rainbowspikesdonut.svg",
color_groups = 16,
color_cycles = 2,
points_per_polygon = 50,
colors = rainbow_colors,
transparency = 0.7
) %>%
add_background_gradient(
colors = c("white", "black", "black", "white"),
stops = c(.27, .34, .93, 1),
rounding = 1,
radius = 1)
In the above image, the rounding was set to 1 to make a circular background. The radius was set to 1 so that the gradient would extend to the edge of the circle. The default radius of \(\sqrt{2}\) extends to the corner of a square background.
Let’s merge two .svg images starting with this image:
spiro(
fixed_radius = 3,
cycling_radius = 1,
colors = "royalblue",
file = "deltoid_blue.svg")
Now let’s make the same image but rotated 60 degrees and in a different color:
spiro(
fixed_radius = 3,
cycling_radius = 1,
rotation = 60 / 180 * pi,
colors = "firebrick",
file = "deltoid_red.svg")
Now let’s merge the images:
image_merge(
input = c("deltoid_blue.svg", "deltoid_red.svg"),
output = "redblue.svg")
It is possible to merge more than two images. In this example, six images are created as a vector. If we leave the file name unspecified, the spiro function will generate a unique file name for each image. These individual files will be deleted because we have set the delete_input
parameter to TRUE
in the image_merge
function.
c(spiro(
fixed_radius = 17,
cycling_radius = 3,
colors = "dodgerblue4"),
spiro(
fixed_radius = 17,
cycling_radius = 4,
colors = "white"),
spiro(
fixed_radius = 17,
cycling_radius = 5,
colors = "dodgerblue3"),
spiro(
fixed_radius = 17,
cycling_radius = 6,
colors = "white"),
spiro(
fixed_radius = 17,
cycling_radius = 7,
colors = "dodgerblue2"),
spiro(
fixed_radius = 17,
cycling_radius = 8,
colors = "white")) %>%
image_merge(
output = "dodgerblue_snowflake.svg",
delete_input = TRUE)
This kind of multi-layer spirograph can be created more efficiently by specifying a tibble with the parameters we want to vary. Then we pipe the tibble into the pmap
function from the purrr
package, which will apply the spiro
function to each row in the tibble. We use the pmap_chr
variant of pmap
because we want to be sure that the output is a vector of file names (i.e., in the “character” format).
The spirograph produced by the code below is the same as above, and thus the image is not duplicated.
By default, the spiro
function makes the image as large as possible by setting the plot limits to the most extreme point (i.e., the point with the largest r in polar coordinates. For example, if the most extreme point is 6 units from the center, the limits of the x and y axes will be ±6. One way to change the size of the spirograph is to set the limits manually with the xlim
and ylim
parameters. Doing so often involves trial-and-error to obtain the desired results.
An alternative is to change the size of the image with the image_scale
function. This function does not alter the underlying data but transforms the .svg image directly.
spiro(
fixed_radius = 7,
cycling_radius = 1,
pen_radius = 3,
colors = "darkorchid",
file = "heptagon_petals.svg"
) %>%
image_scale(scale = 0.4)
This function is useful when you wish to merge images of different sizes.
c(
spiro(
fixed_radius = 7,
cycling_radius = -1,
pen_radius = 3,
colors = "darkorchid",
),
spiro(
fixed_radius = 7,
cycling_radius = 1,
pen_radius = 3,
colors = "white",
) %>%
image_scale(scale = 0.4)
) %>%
image_merge(
output = "heptagon_petals_merged.svg",
delete_input = TRUE)
The spiro
function has a rotation
parameter that determines the rotation of the image and alters the data points directly. However, sometimes you want to rotate the image after it has been created. The image_rotate
function inserts a transform rotate statement into the .svg file. All transforms (i.e., shifting, rotating, and rescaling) are cumulative in .svg, and thus the order in which they are performed will produce different results.
You can specify the rotation in the degrees
parameter by default, but if you specify the rotation in the radians
, any values in the degrees
parameter will be ignored.
hsv_s <- rep(c(1,0),10) + rep(c(-1,1),10) * seq(0.3,0.8,length.out = 20)
spiro(
fixed_radius = 6,
cycling_radius = -1,
pen_radius = 1,
rotation = pi / 6,
points_per_polygon = 500
) %>%
image_merge(
output = "hsv_stripes.svg",
copies = 20,
delete_input = TRUE
) %>%
add_fills(
colors = hsv(
h = rep(seq(0,.90,0.1), 2),
v = rep(c(0.4,.7),10),
s = hsv_s)
) %>%
image_scale(scale = seq(1, 0, length.out = 21)[-21]) %>%
image_rotate(degrees = 0:19 * 5) %>%
add_background(rounding = 1)
In this example, we create 9 copies of a spirograph, give each copy a different color, and then shift the x and y coordinates.
k <- 9
spiro(
fixed_radius = 8,
cycling_radius = 5,
pen_radius = 2,
draw_fills = F,
colors = "black",
line_width = 0.1
) %>%
image_scale(0.8) %>%
image_merge(
output = "rainbowsequence.svg",
copies = k,
delete_input = TRUE
) %>%
add_fills(colors = rainbow(k, alpha = .25)) %>%
image_shift(
x = seq(-1,1,length.out = k) * -60,
y = seq(-1,1,length.out = k) * 15)
One of the primary reasons I chose to make the spiro package was to be able to animate the images. Unfortunately, the animations produced with the spiro package are not viewable by all programs that display svg files. These animations work with the Google Chrome browser. They may work, with varying degrees of smoothness in other browsers (e.g., Mozilla Firefox). They do not work in Microsoft Edge or Microsoft Internet Explorer.
The image_spin
function takes file name of an svg file created by the spiro
function and adds an animateTransform
node to the .svg. To spin clockwise, set the rpm
parameter to a postie number. To spin counter-clockwise, set the rpm
parameter to a negative number.
This spirograph may not seem particularly interesting:
spiro(
colors = "purple",
fixed_radius = 16,
cycling_radius = 15,
pen_radius = 1.5,
draw_fills = FALSE,
line_width = 4,
file = "purple.svg"
)
This one looks almost identical to it:
spiro(
colors = "black",
fixed_radius = 15,
cycling_radius = 14,
pen_radius = 1.5,
draw_fills = FALSE,
line_width = 4,
file = "black.svg"
)
However, with a black background, spinning the two images produces this unexpected effect:
image_merge(
input = c("purple.svg", "black.svg"),
output = "purple_cycle.svg") %>%
image_spin(rpm = c(0.5,-0.5)) %>%
add_background(color = "black")
We can merge and spin images in a streamlined workflow. By default, the output of the spiro function is the file name. Thus, we can use the pipe operator %>%
from the magrittr
package to string commands together. If we merge the images first, the rotations can be specified with a vector.
Suppose we have a static image we want to spin:
spiro(
file = "Static.svg",
fixed_radius = pi,
cycling_radius = sqrt(8),
pen_radius = 0.5 * sqrt(8) / pi ,
windings = 81,
start_angle = 0,
points_per_polygon = 10000,
transparency = 0.7
)
This is a nice spirograph already, but it becomes especially groovy when 2 copies are merged and spun slowly in opposite directions.
spiro(
fixed_radius = pi,
cycling_radius = sqrt(8),
pen_radius = 0.5 * sqrt(8) / pi ,
windings = 81,
start_angle = 0,
points_per_polygon = 10000,
transparency = 0.7
) %>%
image_merge(
output = "SpinLeftRight.svg",
copies = 2,
delete_input = TRUE
) %>%
image_spin(rpm = c(-0.1,0.1))
The image_spin
function spins the image around its center by default, but off-center rotations are possible. Here we spin an image around a point not at the center. Imagine the image is a square of size 1. The top left corner is point (0,0) and the bottom right corner is point (1,1). The center is point (0.5,0.5). Here we spin the image around the point (0.25,0.38), which centered horizontally but a little above the vertical center. I have added a red dot to show the point around which the image is rotating.
spiro(
fixed_radius = 5,
cycling_radius = 3,
pen_radius = 3 ,
start_angle = 0,
rotation = pi / 2,
file = "off_center.svg"
) %>%
image_spin(
rpm = 5,
rotation_point = c(0.5, 0.385),
add_dot = TRUE
)
You can make dots trace your paths:
spiro(
fixed_radius = 5,
cycling_radius = 1,
pen_radius = 11 / 3,
draw_fills = FALSE,
rotation = pi / 10,
file = "pathdot1.svg"
) %>%
add_pathdot()
If your spirograph has multiple colors and multiple groups, a pathdot will be added to each one. You can also set the duration of the animation in seconds.
# Make Colors
pathcolors <- scales::viridis_pal()(5)[c(4, 5, 1, 2, 3)]
spiro(
fixed_radius = 5,
cycling_radius = 1,
pen_radius = 11 / 3,
start_angle = pi,
color_groups = 5,
draw_fills = FALSE,
line_width = 10,
rotation = pi / 10,
file = "pathdot5.svg"
) %>%
add_pathdot(
colors = pathcolors,
duration = 3)
You can add multiple layers of pathdots. Here I have made all the lines white to make it seem that the dots are whizzing about on their own.
spiro(
fixed_radius = 5,
cycling_radius = 1,
pen_radius = 11 / 3,
color_groups = 5,
colors = rep("white", 5),
start_angle = pi,
draw_fills = FALSE,
rotation = pi / 10,
file = "pathdot25.svg", points_per_polygon = 250
) %>%
add_pathdot(duration = 2) %>%
add_pathdot(duration = 3) %>%
add_pathdot(duration = 4) %>%
add_pathdot(duration = 5) %>%
add_pathdot(duration = 6)
ggplot2
The spiro
function normally returns the file name, but it is possible to return the raw data by setting savefile
to FALSE. Thus, it is easy to use the data with ggplot2
or any other plotting system you wish to use.
library(ggplot2)
library(dplyr)
d <- spiro(
fixed_radius = 37,
cycling_radius = 12 ,
pen_radius = 31,
color_groups = 6,
color_cycles = 6,
points_per_polygon = 1000,
savefile = FALSE
)
d %>%
mutate(color_id = factor(color_id),
color_cycle_id = factor(color_cycle_id)) %>%
ggplot(aes(x, y, color = color_cycle_id)) +
geom_path(lwd = 1) +
theme_void() +
theme(legend.position = "none") +
coord_equal()
Bezier strings are my name for the string art I did when I was in elementary school. I thought they were amazing. They are not spirographs, though.
By default, the string_bezier
function stars with strings on a right angle:
string_bezier(
file = "bezier.svg",
color = c("red","green", "blue"),
n = 75)
However, as long as there are at least 3 points on x and y, you can string these curves together. Here we have also added a background color.
string_bezier(
file = "bezier_sequence.svg",
x = c(-1, 1, 0,-1, 1,-1, 0, 1,-1, 1),
y = c( 0, 0, 1, 0, 0, 0,-1, 0, 0, 0),
color = c("#006B6B", "#FFFFFF", "black", "black", "black"),
n = 200,
lwd = 5,
ljoin = 1
) %>%
add_background(color = "#108998")
The spiro package could be developed in a many ways. It is possible to extend the idea of spirographs to arbitrary shapes cycling around arbitrary shapes. The animations and transformations are still primitive. The animations may be better handled with d3.
I look forward to seeing what people can create with spiro.