by Danielle Navarro, 06 Oct 2019
[Author note: This post was written in the very early stages of writing the jasmines package, before the API started to stabilise. I’ve updated the code so that it should all work, but I haven’t updated the text, so there’s a jarring mismatch between the text and code in some places]
I’ve long admired generative artwork. There’s something appealing to me about writing a function that doesn’t merely generate a single image, but instead defines a family of images that you can then explore to discover artwork that surprises even you, the person who wrote the function. I’ve never been particularly skilled at it, but so what? The point of artwork isn’t really to be the “best” or even particularly “good” at doing a thing, it’s to take joy in the act of creation and discovery.
Inspired by Thomas Lin Pederson and Will Chase who have been making beautiful things in R based around curl noise, I wanted to see what I could do. So I wrote the
jasmines::tempest() function, the source code for which is up on GitHub. The idea behind the function is to iteratively apply a curl noise transformation to a seed object, and visualise the results. At the moment I’m not really trying to understand the low level mechanics of how curl noise works, I just want to play around and see what I can make with it.
Here’s a simple example. The seed object here is a set of six horizontal “lines”, where each line is represented by a discrete set of points (i.e.,
seed_rows(n=6)). Each point is peturbed once (
iterations = 1) according to a curl noise operation, and thin line (
line_width = 1) is drawn connecting the original point to the peturbed one. The seed object is highlighted by by setting
seed_col = "white":
seed_rows(n = 6) %>% time_tempest(iterations = 1) %>% style_ribbon(seed_col = "white")
The wavy shapes that this produces are the basic unit from which fancier calls to
tempest() are built. The colouring of segments uses the viridis palette by default, though this can be customised. The specific mapping from segment to colour is a deterministic function of its length, but as you can see looking at the plot it’s not a particularly systematic one!
We can make the images more complicated by increasing the number of iterations. In the code below we start with three horizontal rows, apply the curl noise operation as before to output a set of peturbed points (and segments), and then repeat the process a second time, using the output from the first iteration as the input to the second iteration:
seed_rows(n = 3) %>% time_tempest(iterations = 2) %>% style_ribbon()
At the moment the images aren’t all that pretty, but this “waves over waves” mechanic has a certain appeal, and it doesn’t take a lot of tinkering to make the output more interesting. Instead of using a set of parallel lines as a seed, the way I was doing with
seed_rows(), I’ll use a set of lines with random lengths and orientations as the seed using the
seed_sticks(n = 10) %>% tempest(iterations = 2) %>% style_ribbon()
Depending on how the sticks fall, you can get some surprisingly evocative images with the code above. I had a lot of fun just running that code over and over to see what happens! There is one irritating limitation to this code, however: every image must include the straight lines that made up the original seed. Aesthetically I don’t find that at all unpleasant, to be honest, but sometimes you might want to create images that have more of a “ribbony” look to them. To do that, you can set a “burn in” period. By default the
tempest() function sets
burnin = 0 meaning that every iteration of the curl noise operation gets drawn, but if this is increased then it will only draw segments for later iterations, after the burn in period expires. For example, in this code we run the curl noise operation three times but only plot the last one:
seed_sticks(n = 10) %>% time_tempest(iterations = 3) %>% style_ribbon(burnin = 2)
You can play around with the seed type to get interesting variations. For example, the
seed_bubbles() function generates a random set of circles that you can use as the seed, and when combined with a modest
burnin value the result kind of looks like floating smoke rings:
seed_bubbles(n = 3) %>% time_tempest(iterations = 5) %>% style_ribbon(burnin = 4)
I really like this look, but I’m starting to get tired of seeing the viridis colour scheme over and over again. To allow some flexibility here, the
tempest() function includes a
palette argument that takes a palette-generating function (or a list of such functions) as its value. I wrote it that way so that
tempest() can repeatedly call the palette function as it iterates, modifying the parameters (in particular the transparency) as it goes.
The jasmines package has a simple function factory called
palette_scico() that allows you to specify one of the scico palettes for the image. The three palettes I’ll use here:
bilbao <- palette_scico(palette = "bilbao") lajolla <- palette_scico(palette = "lajolla") berlin <- palette_scico(palette = "berlin")
So now we can go for it:
seed_bubbles(n = 3) %>% time_tempest(iterations = 5) %>% style_ribbon(burnin = 4, palette = bilbao)
After playing around with this for a while I started to get curious about other mechanisms for displaying the seed besides the
seed_col argument. Another variation I introduced was the
seed_fill argument, which converts the seed elements to polygons and fills them. As a simple example, here’s a version with circles used as the seed:
seed_bubbles(n = 1) %>% time_tempest(iterations = 10) %>% style_ribbon( seed_col = "white", seed_fill = "black", palette = bilbao )
This “occlusion” effect is kind of neat, so I played around with it using a single bubble as the seed, running the algorithm for 2000 iterations and setting the
alpha_decay parameter so that the intensity of the colour fades away gradually. The resulting image feels to me like a kind of “eclipse”:
seed_bubbles(n = 1) %>% time_tempest(iterations = 2000, scale = 0.0075) %>% style_ribbon( seed_fill = "black", alpha_decay = .0025, burnin = 500, palette = lajolla )
One of the things I love most about the idea of taking up generative art as a hobby, though, is the opportunities for unexpected delight. When I wrote the
tempest() function I didn’t really have any clear goal in mind. I just wanted to explore. Sometimes I added function arguments because I was trying to understand what I’d just created. Other times I added them because there was a specific effect I was trying to produce (not always successfully). The “final” system is itself the result of an iterative, quasi-random walk over space of possible generative art functions. I’m a part of that stochastic system in a curious way. Often I’d see a surprising output and then be prompted to tweak the function this way and not that way. If I’d seen a different output, maybe I’d have followed a different trajectory and the function could have looked quite different.
tempest() still surprises me. I’ve explored only a small part of its functionality, and I don’t have very good intuitions about the output when I feed it different parameters. My favourite example of that is this code snippet, the output of which came as an utter surprise to me…
seed_bubbles(n = 3, grain = 100) %>% time_tempest(iterations = 500, scale = 0.005) %>% style_ribbon( seed_fill = "#ffffff33", alpha_init = 1, alpha_decay = .0015, burnin = 300, palette = berlin )
I think this is truly gorgeous, but it’s nothing like what I had in mind when I started the project. It just sort of happened, and I’m utterly delighted.