library(arttools)
options(
arttools.repos.local = file.path(tempdir(), "temp_repos"),
arttools.bucket.local = file.path(tempdir(), "temp_bucket")
)
When you initialise a new art repository using
repo_create()
, it builds a skeleton that you can work from
when you want to get to work on the fun artistic parts. In this article
I’ll walk you through the process of what that entails in practice. I’ll
start by creating a “fake” art repository:
repo_create(series = "series-fake", license = "ccby")
#> ✔ repository folder created at /tmp/RtmpjCAWti/temp_repos/series-fake
#> ✔ writing README.md
#> ✔ writing LICENSE.md
#> ✔ writing .gitignore
#> ✔ writing .here
#> ✔ creating folder 'source'
#> ✔ creating folder 'input'
#> ✔ creating folder 'output'
#> ✔ creating folder 'build'
#> ✔ writing example file 'source/common.R'
#> ✔ writing example file 'source/art-system_01.R'
#> ✔ writing example file 'source/art-system_02.R'
When you create the repository, these are the files and folders that you start off with:
#> /tmp/RtmpjCAWti/temp_repos/series-fake
#> ├── .gitignore
#> ├── .here
#> ├── LICENSE.md
#> ├── README.md
#> ├── build
#> ├── input
#> ├── output
#> └── source
#> ├── art-system_01.R
#> ├── art-system_02.R
#> └── common.R
Notice that there are three files that are automatically added to the
source
folder. I’ve added them primarily for illustrative
purposes, and you can delete them if you like, but they’re written
specifically to highlight key aspects to the generative art workflow,
and it’s worth taking a look at what each of them does.
Assumptions about art workflow
One thing I’ve learned from years of creating generative art systems is that my “artistic workflow” is subtly different from the “analytic workflow” I adopt when analysing a data set, and it’s different from the “developer workflow” I adopt when writing packages and other software.
To understand why, it’s helpful to understand the ways in which art, analysis, and packages differ in their goals.
When writing a package, you’re aiming to create one piece of software. There will be different versions of the software, of course, as you add features, fix bugs, etc, but you don’t expect multiple versions of the same package to coexist within the repository at once! The history of the development process is only relevant to the extent that folks may be working from older versions of the package, or might need to install an older version for one reason or another.
Data analysis is similar to package development, but not quite the same. As with package development, you’re still aiming to create one analysis (or, very often, a collection of analyses). The history is a little more important in this case because you often want to trace your path through the so-called “garden of forking paths” to make sure you’re not deluding yourself about the validity of your analysis. Because of this you might want to keep copies of the interim analyses that you conducted – especially when you’re doing exploratory analysis – but ultimately the hope is that you’ll end up with a single authoritative version of the data analysis.
Art is… not like that at all. It’s a little bit like exploratory data analysis in that it’s an iterative process, but – at least in my own experience – your goal is almost never to create one “authoritative” version of the art system. It’s often true that later versions of the system are the ones that you like best, and the art you want to publish at the end are usually outputs from the later versions of the system, but the artistic process is pure exploration all the way. You don’t want to lose old versions of the system: you might want to use them later!
Because this workflow is grossly typical when creating art, it’s
actually a good thing to end up with files in your
source
folder that have names like this:
art-system_01.R
art-system_02.R
art-system_03.R
etc
Each of these defines a subtly different variant of the art system,
and I’ve found that in practice you want them all to live together
side-by-side in the source
folder.
The trick, when iteratively creating many versions of the system side by side, is to have a few helper functions that will help you maintain discipline and ensure that the different versions don’t interfere with each other and you can always figure out which version of the system generated any specific image!
That’s where the common.R
file comes in handy: the
purpose of having a common.R
file in the
source
folder is make it easy to maintain discipline
without interrupting you in the middle of the fun artistic parts.
The common functions
There are six functions defined in the common.R
file.
Two of them are used to make sure that different versions of the art
system don’t introduce inconsistencies:
-
assert_version_consistency()
is a function designed to help you avoid mistakes when creating a new version of the art system -
system_versions()
is a function you can call if you do make a mistake when creating a new version and need to figure out where the version inconsistency happened
There are also two functions that are helpful to make sure images are written to the correct location, and with the correct file name:
-
output_file()
constructs the file name -
output_path()
constructs the file path
Finally there are two functions – tidy_int_string()
and
tidy_name_string()
– that you don’t actually need to call
within your art code, but are used internally by other
common.R
functions. To get a sense of how I use the
common.R
functions within a system it’s helpful to walk
through the code of the example system.
Versioning an art system
The structure of almost every art system I’ve written – at least when I’ve done it in a sensible way – is pretty much the same every time:
- At the top of the file there’s a few lines of code that take care of “administrative” tasks.
- The bulk of the file is devoted to the fun stuff: writing functions that create generative art when called.
- (Optionally) At the bottom of the file there’s some code that actually calls the generative art functions and creates the art.
Let’s have a look at the code that forms the “administrative” section
of the art-system_01.R
script. It begins with these three
lines:
name <- "art-system"
version <- 1
format <- "png"
Here I’ve defined the name
of the generative art system,
the version
of the system, and the image
format
used when writing image files. Notice that the
name
and version
defined in the code are both
consistent with the filename of the script: it’s called
art-system_01.R
, and indeed the name
of the
system is defined to be "art-system"
and the
version
number is set to 1.
Whenever I want to create a new version of the system, I copy
art-system_01.R
to art-system_02.R
– almost
always by using the “save as” menu option in RStudio or whatever IDE I’m
using, because this is not the kind of thing worth doing at the terminal
– and then update the version
number within the code. As a
consequence, the first three lines in art-system_02.R
are
as follows:
name <- "art-system"
version <- 2
format <- "png"
The name
and format
don’t change, but I’ve
incremented the version
number appropriately.
Now that I’ve got a new version of the system, I can get to the fun part and start tinkering with the generative art code!
Unfortunately, there’s a trap here. When I’m in the midst of an
artistic process, I’m almost always thinking about the art, and
it’s very easy to forget to increment the value of version
.
That’s a problem because (as we’ll see later), the version
number is important. It is used to make sure that outputs from different
versions of the same system are given the correct file names. Chaos can
ensue if the version
number is wrong.
This is where the common.R
script comes in handy,
because it has helper functions that are designed to catch this mistake.
So the fourth line in every art script imports the functions from
common.R
:
To avoid introducing unwanted dependencies, the functions in
common.R
have been written using base R, almost
exclusively. It’s completely independent of “arttools”, and the
only package that is required for the common.R
functions to work is the “here” package.
One of the functions supplied by common.R
is
assert_version_consistency()
, and what it does is scan the
file names and source code to make sure that all the version numbers are
consistent. To put it crudely, this function exists because I
know that I’m likely to forget to update the
version
number, and it’s really helpful if the art script
throws an error if I make that mistake. Because of this, the final line
in the “administrative” section of the code looks like this:
assert_version_consistency(name)
If all the version numbers are fine, nothing happens. But if there’s a problem, it throws an error.
To understand what this is doing, you can call the
system_versions()
function – also supplied by
common.R
– to work out what is going on:
system_versions("art-system")
#> file filename_version source_version
#> 1 art-system_01.R 1 1
#> 2 art-system_02.R 2 2
The output here is a data frame with three columns. The
file
column specifies the name of the source file, the
filename_version
column contains the version number
extracted by inspecting the file name, and the
source_version
column contains the version number extracted
by inspecting the source code. In this case there are no
inconsistencies, which is why assert_version_consistency()
doesn’t throw an error. If, on the other hand, the source and file name
versions don’t match, or if there are duplicate entries, it will
error.
It’s a boring thing to do, but in practice I find that it’s extremely
useful to call assert_version_consistency()
at the start of
every art script, because otherwise I will eventually mess
something up.
Naming output files
When writing generative art code, most of your time is spent on the
artistic side. However, at some point in the process the code has to
write an image file, and – speaking from experience – it’s easy to get
lazy at this point. Image files with names "image1.png"
,
"image2.png"
aren’t very informative, and if you return to
the system later on you won’t know how to recreate those images. What
you really want is a consistent system for naming image files in a way
that allows you to look at the file name and immediately know:
- What art system created the image?
- What version of the art system created it?
- What unique information (e.g. RNG seed) was used to create it?
If that’s the goal, you want to name your files in a way that follows a consistent scheme that supplies this information. Here’s the naming convention I use:
[name]_[version]_[id].[format]
Notice that three of these four parts are already supplied in the
“administrative header” of our art script: name
,
version
, and format
are variables that are
defined at the very beginning of the art-system_01.R
and
art-system_02.R
scripts, so when the time comes to create a
file name for any specific output from the generative art system those
variables are already available to us!
What about the id
variable? This part is intentionally a
little vague, because it might very well be different for every system,
but the intention here is that it should be a unique identifier string
that tells you what parameters were used to create the image. In most
generative art systems I create, there’s only one input parameter,
namely the RNG seed, so the id
part of the filename is
simply an integer value specifying the RNG seed.
To make life easier, the common.R
file supplies the
output_file()
function that you can call within your
generative art script and create a file name that adheres to these
rules. Here’s an example:
output_file(
name = "art-system",
version = 1,
id = 109,
format = "png"
)
#> [1] "art-system_01_0109.png"
Notice that the numeric parts of the name are all left-padded with
zeros. By default output_file()
will create a two-digit
version number, on the assumption that you very likely might make more
than 10 versions of a specific system but aren’t likely to make more
than 100 versions. Similarly, if the id
input is a single
number (presumably the RNG seed), it’s padded with zeros so that it is
always a four digit number on the assumption that you’re probably not
going to create more than 10000 outputs. It’s a little bit of syntactic
sugar designed to ensure that the file names sort into the correct
order, which is often helpful later on if you want to create a website
showcasing your work.
Along the same lines, it’s worth noting that there are some
constraints on file names that output_file()
attempts to
enforce:
- It uses
_
as the separator between the different parts of the name - It uses
-
as the separator between “words” within any specific part - It allows
.
to appear only as the separator between the file name and the file extension - It replaces white space with
-
In principle this shouldn’t be necessary because “of course” you’re
going to try to choose names wisely, but in practice I’ve messed this up
so many times that I’ve found it valuable to write
output_file()
in a way that tries to enforce good names. To
give a sense of this, here’s what happens if we pass some very bad
names:
output_file(
name = "this... is a _very_ bad name",
version = 1,
id = 109,
format = ".png"
)
#> [1] "this-is-a-very-bad-name_01_0109.png"
Obviously, we don’t really want to be providing input like this, but… look, this is art. The artistic process almost always involves fucking around and finding out, so it’s valuable to have something in place to protect us against the worst of our own excesses, yes?
Writing output files
While the output_file()
function is a handy thing to
have available to you, in practice you’re more likely to call
output_path()
, which goes a little further and creates the
complete path to the output file:
output_path("art-system", 1, 109, "png")
#> [1] "/tmp/RtmpjCAWti/temp_repos/system-fake/output/01/art-system_01_0109.png"
In most cases this is the function you want to call within your
generative art system, because it provides a complete path to the
location that the output file should be written. Writing files in this
fashion ensures that within the output
folder there will be
a separate image folder for every version of the system, and every image
within that system will be named in a way that allows you to accurately
reconstruct where it came from.
Putting it together
This discussion has felt a little abstract. I’ve talked entirely
about the administrative aspects to the art process, and that’s really
boring. With that in mind, it might be helpful – in light of this
discussion – to take a look at the complete source code for
art-system_01.R
:
# set up ------------------------------------------------------------------
name <- "art-system" # only change this for an entirely new system
version <- 1 # increment to 2 for "my-art-system_02.R", etc
format <- "png" # png is usually a good choice!
# define common helper functions
source(here::here("source", "common.R"), echo = FALSE)
# make sure we haven't accidentally messed up the versions
assert_version_consistency(name)
# define the art system ---------------------------------------------------
art_generator <- function(seed) {
set.seed(seed)
# specify the output path and message the user
output <- output_path(name, version, seed, format)
message("generating art at ", output)
# generate high-level parameters for this piece
n <- sample(50:100, size = 1)
palette <- sample(colours(distinct = TRUE), 10)
# generate low-level details for this piece
x_coord <- sample(1:10, size = n, replace = TRUE)
y_coord <- sample(1:10, size = n, replace = TRUE)
size <- sample(2 * (1:5), size = n, replace = TRUE)
color <- sample(palette, size = n, replace = TRUE)
thickness <- sample(2 * (3:6), size = n, replace = TRUE)
# write the image
op <- par(mar = c(0, 0, 0, 0))
png(
filename = output,
width = 1000,
height = 1000,
units = "px",
bg = "black"
)
plot.new()
plot.window(xlim = c(0, 11), ylim = c(0, 11))
points(x = x_coord, y = y_coord, cex = size, col = color, lwd = thickness)
dev.off()
par(op)
}
# make the art ------------------------------------------------------------
for(seed in 1:5) art_generator(seed)
As you can see, there are a few places where the “administrative”
aspects intrude, but not many. In practice, the functions from
common.R
don’t actually show up much in the code. They
appear in the administrative header at the top of the script, and in
those parts of the code that are devoted to writing the output file.
Otherwise, they stay out of your way and let you focus on making
art.
Which is of course the point.