In 1965, programmer George Nees wrote his first pioneering works of generative art in ALGOL, printing his programs on punch cards and loading them into a Zuse Graphomat to plot them as physical drawings, including his well known Shotter.

Shotter, by George Nees

The modern programmer has access to a vastly more convenient and powerful toolset, writing in a modern language, and drawing from the expansive ecosystem of open-source libraries available online.

Our tools

In this series of posts, I’ll be using Python to write programs that can generative svg images.

Python provides a great workspace for iterative creative work:

  • It is a consise language with simple syntax in which you can achieve a lot with relatively little code
  • It has a great ecosystem of open-source packages which can save you from reinventing the wheel by solving a problem that already has a good solution (for example, using a package that can generate SVG files, like svgwrite)
  • As a scripting language you don’t have to worry about compiling your code as you make iterative changes to your program in order to tweak your work, accelerating your working process

SVG files provides a great file format for our generated images, for a few reasons:

  • It’s a human-readable format (based on XML), so it’s easy to examine the output of your work and make manual changes
  • It’s widely supported on browsers, making it easy to share your work
  • It has all the advantages of vector images: it can be scaled without losing resolution and it can be used to generate physical drawings with a plotter

Getting started

A good practice for your Python projects is setting up a virtual environment for your packages. This will provide a self-contained environment where you can install specific versions of your packages in order to manage their dependencies and ensure intercompatibility between them, rather than using an operating system-level version of each package. There are many well known packages related to this subject (e.g. pipenv, virtualenv, venv, pyenv).

I’ve gone for virtualenv here. If you haven’t got it installed, run pip install virtualenv. Then, in your project folder, run virtualenv venv. This will create a directory called ‘venv’ which will contain installed packages.

We’ll be using the svgwrite package; running pip install svgwrite will install the package in that ‘venv’ directory, then we’ll create a .py file in which to write our code.

Examining a piece of generative art

Let’s take a look again at Shotter.

It’s essentially a grid of squares where their position and rotation becomes gradually more chaotic as their distance from the origin increases.

This should be simple to achieve with little more than a two-dimensional for loop and some random number generation.

In order to add progressive ‘chaos’, each square will have it’s own ‘origin’ position, modified by a random number whose possible range is larger for squares further from the origin. The same can be applied to its rotation.

Creating our own Shotter

First, let’s create a grid using svgwrite.

We can start by importing svgwrite and creating a single square of size 10 * 10 at co-ordinate (0,0) in the svg, and applying some global settings to the entire drawing. This handles most of our boilerplate.

import svgwrite

# Initialise the drawing
dwg = svgwrite.Drawing(
  'test.svg', 
  size=(100, 100), 
  profile='tiny', 
  stroke_width=0.5,
  stroke='black',
  stroke_opacity=1.0
)

# Add the square 
dwg.add(dwg.rect(
  (0, 0),
  (10, 10)
))

dwg.saveas("shotterOneSquare.svg")

Creating a grid of squares can be achieved with two nested for loops, running from 0 to 9. Their positions can be derived from the values ‘x’ and ‘y’, which increase by one with each loop. Bear in mind that higher ‘y’ values here will move the square further down the image, rather than up, because the origin (0, 0) is in the top left hand corner of the image.

import svgwrite

dwg = svgwrite.Drawing(
  'test.svg', 
  size=(100, 100), 
  profile='tiny', 
  stroke_width=0.5,
  stroke='black',
  stroke_opacity=1.0,
  fill='none'
)

width = 10
height = 10

for x in range(0, width):
    for y in range(0, height):
        dwg.add(dwg.rect(
        (x * 10,y * 10),
        (10,10)
        ))

dwg.saveas("shotterGrid.svg")

Adding chaos

We now have a plain 10 * 10 grid of squares. Python’s random package from the standard library can help us introduce some chaos.

First, we import random, then we:

  1. Create x and y position modifiers for each square during our loop using random.random() (which generates a pseudo-random floating point number between 0.0 and 1.0)
  2. Minus 0.5 from the randomly generated number, so that we have a number between -0.5 and 0.5
  3. Multiply this modifier by the ‘number’ of the current square, for example 1 for the first square in the first row, 11 for the first square in the second row (given by x + (y * width)) so that squares further from the origin can be moved a greater distance.
import svgwrite
import random

dwg = svgwrite.Drawing(
  'test.svg', 
  size=(100, 100), 
  profile='tiny', 
  stroke_width=0.5,
  stroke='black',
  stroke_opacity=1.0,
  fill='none'
)

width = 10
height = 10

for x in range(0, width):
    for y in range(0, height):
        position_modifier_x = (random.random() - 0.5) * (x + (y * width))
        position_modifier_y = (random.random() - 0.5) * (x + (y * width))
        dwg.add(dwg.rect(
            (
                x * 10 + position_modifier_x,
                y * 10 + position_modifier_y
            ),
            (10,10)
        ))

dwg.saveas("shotterGridPosition.svg")

The result is overly chaotic, but with some minor numerical tweaking, we can achieve something more similar to Nees’ Shotter.

import svgwrite
import random

dwg = svgwrite.Drawing(
  'test.svg', 
  size=(100, 100), 
  profile='tiny', 
  stroke_width=0.5,
  stroke='black',
  stroke_opacity=1.0,
  fill='none'
)

width = 10
height = 10
dampener = 100

for x in range(0, width):
    for y in range(0, height):
        position_modifier_x = (random.random() - 0.5) * (x + (y * width)) * y / dampener
        position_modifier_y = (random.random() - 0.5) * (x + (y * width)) * y / dampener
        dwg.add(dwg.rect(
            (
                x * 10 + position_modifier_x,
                y * 10 + position_modifier_y
            ),
            (10,10)
        ))

dwg.saveas("shotterGridPositionTweaked.svg")

In order to add rotation to the squares, we can add a rotate transformation to each square. We will subtitute the rotation degree and centre co-ordinate values into a transformation using an f-string along the following lines: transform=f"rotate({degrees}, {centre_x_coord}, {centre_y_coord})"

import svgwrite
import random

dwg = svgwrite.Drawing(
  'test.svg', 
  size=(100, 100), 
  profile='tiny', 
  stroke_width=0.5,
  stroke='black',
  stroke_opacity=1.0,
  fill='none'
)

width = 10
height = 10
position_dampener = 100
rotation_dampener = 10

for x in range(0, width):
    for y in range(0, height):
        position_modifier_x = (random.random() - 0.5) * (x + (y * width)) * y / position_dampener
        x_position = x * 10 + position_modifier_x

        position_modifier_y = (random.random() - 0.5) * (x + (y * width)) * y / position_dampener
        y_position = y * 10 + position_modifier_y

        rotation = (random.random() - 0.5) * (x + (y * width)) * y / rotation_dampener

        dwg.add(dwg.rect(
            (x_position,y_position
            ),
            (10,10),
            transform = f"rotate({rotation},{x_position + 5},{y_position +5})"
        ))

dwg.saveas("shotterGridPositionWithRotation.svg")

Which generates this:

Finishing up

We really have everything we need. A few minor changes to the program can create something very similar to what George Nees produced, and we can tidy our code using functions to reduce repetition (I’ve used lambda functions but standard functions would work just as well).

import svgwrite
import random

dwg = svgwrite.Drawing(
  'test.svg', 
  size=(100, 100), 
  profile='tiny', 
  stroke_width=0.5,
  stroke='black',
  stroke_opacity=1.0,
  fill='none'
)

width = 10
height = 20
position_dampener = 400
rotation_dampener = 15

random_magnitude = lambda x,y : (random.random() - 0.5) * (x + (y * width)) * y
random_position_magnitude = lambda x,y : random_magnitude(x,y) / position_dampener

for x in range(0, width):
    for y in range(0, height):
        position_modifier_x = random_position_magnitude(x,y)
        x_position = x * 10 + position_modifier_x

        position_modifier_y = random_position_magnitude(x,y)
        y_position = y * 10 + position_modifier_y

        rotation = "{:.1f}".format(random_magnitude(x,y) / rotation_dampener)

        dwg.add(dwg.rect(
            (x_position,y_position
            ),
            (10,10),
            transform = f"rotate({rotation},{x_position + 5},{y_position + 5})"
        ))

dwg.saveas("shotter.svg")

It’s worth bearing in mind that we haven’t produce a single artwork here - what we have made is a program that can produce myriad artworks following the algorithm, or set of rules, that we have written.

Here are a set of images produced by our program: