This article is an introduction to writing generative art with Haskell, using a stack inspired by Ben Kovach's similar post I came across in March. If you plan to follow along see the deps list. I assume familiarity with Haskell and an interest in generative art.
![Seed 347593857234897520](/assets/fpN2AGO.gif)
![Seed 99924583058](/assets/xWFQL0Z.gif)
![Seed 98239276903311](/assets/atuesRr.gif)
Some images generated using the methods described below. Details.
Table of Contents
Vector Graphics with Cairo
Axel Simon and Duncan Coutts wrote a wonderful Cairo binding for Haskell. We can use it to describe a 2D vector graphics composition and raster it. It may be helpful to keep the haddock open aside this introduction.
Render Monad
At a high level, Cairo paints source patterns onto surfaces using instructions contained in a Render monad.
Source patterns may be a solid color, another surface, or as we'll cover in the matte section, another Render monad.
Surfaces are usually memory buffers or image files.
In this simple example code we create an image surface and realize two Render monads to create the image that follows.
Both monads in the example code above follow the typical structure we'll use moving forward. Each drawing action is made of
- A source (
setSourceRGBA 0 0 0 1
) - A path (
rectangle 0 0 500 500
) - A draw instruction (
fill
)
Draw instructions always use the current path in the Render monad.
Paths
There are some shortcuts for making paths, such as rectangle
, but usually we will need to use some combination of the path building Render monads moveTo
, lineTo
, curveto
, and closePath
.
Since Cairo is low level, you may want to build some types over these things. I personally use a Contour type which holds a vector of points, each represented by the V2 type from the linear package. V2 defines many useful class instances for relevant math and many other haskell libraries expect V2, so using it will make life easy.
Here is an example program that draws a triangle with the Contour type.
Mattes
Cairo supports using a stack of Render monads for compositing. This means you can push a Render monad, bind some instructions, then pop it as a source for drawing your next paths. Here's an alpha matte function as an example:
This function creates a Render monad which draws src
, multiplying (in 0-1 space) the alpha channel of src
with the alpha channel of matte
.
For example, with this matte:
And this source:
alphaMatte matte source
looks like this:
Generate Monad
Originally described by Ben Kovach, the Generate monad helps us abstract the render instructions from the output. Ours will be different from Ben's, to remove IO effects (Render is an instance of MonadIO).
Dimension Agnosticism
First, we can create a Reader monad to hold our width, height, and a scale factor. Then we can make our sketches agnostic to the final render dimensions using Cairo's scale
function which will transform our coordinate space. For instance, you can work in a 500x500 grid and if you like the result, sink the time to render an image with identical content at scale factor 10 for a 5000x5000 output.
Running the Generate monad will yield a Render monad we can use. I recommend this over making Render part of a monad transformer stack in Generate. As you build up abstractions over different pieces of Cairo you may want to return other types in a Generate monad, at which point it will be nice to know those functions don't have side effects on the drawing or perform IO.
Both of the following images were rendered with the above code; I only changed the arguments to World
between them.
Random Variables
We can add random variable support to our generate Monad using rvar, random-source, and random-fu, a comprehensive random variable library family written by James Cook and maintained by Dominic Steinitz. It implements most distributions you might want, and many you never will.
We'll put our Reader monad in a State transformer, then we can sample random variables when building our Render monads.
Now the box we render will have a random color each invocation:
Colour
Colour in Cairo is interesting because it's sometimes important to work with it as colour in particular and other times it should just be treated as a source--identical to patterns and images you might paint with.
I settle on using colour when working with colours themselves, and using a class for CairoColour that supports whatever I might use as a source:
It may seem like unnecessary indirection, but it allows some convenient instances to be passed around such as for radial ramps:
Animations
We can render animations in two ways:
- If everything is derivable from the time or frame number, just slap a frame field in your World record and you're done!
- If you need to carry state between frames (e.g. for a walker), wrap the Generate monad in a State monad.
This code renders frames to show the second method. I use convert *.png out.gif
to stitch them together.
Here's an animation I wrote using this technique:
Further Reading
If you've decided to start writing art with Haskell, hurray! Here are some good readings for your journey.
- Working With Color in Generative Art by Tyler Hobbs
- On Generative Algorithms by Anders Hoff
- Probability Distributions for Algorithmic Artists by Tyler Hobbs
- A Story of Iteration by Ben Kovach
If you make something, come share your work at /r/generative! We're friendly!
Raster Graphics with Accelerate
WARNING: This is an esoteric thing you're about to try. No one supports it or maintains any code to help you do it. You may waste lots of time. Here I share my unsupported undocumented minimally tested code I use to do it, with token commentary.
At some point you may want to manipulate pixel values, or generate something per-pixel as in a shader (e.g. a noise texture). This is difficult to do with Cairo, but with a little black magic and by leveraging some careful work done by people who understand color spaces well, we can build a GPU accelerated per-pixel function to modify our Cairo surface.
If you use Cairo's 32 bit surface format (you should use Cairo's 32 bit surface format), the surfaces contain 8 bits per channel and the colors are premultiplied by the alpha. The colors are in sRGB space.
You can acheive a reasonable compositing flow by unpacking this data into something you can work with, doing your shading work, and packing it back down, though getting the colors and data format right can be a bit tricky. See imageSurfaceGetPixels for a start.
Below is my personal solution with some parts stripped out. Basically this file provides a function which accepts an accelerate metaprogram which computes a color for each pixel, given some uniforms and random access to what's already been drawn. Then I run it on GPU and put the results back in the Cairo surface.
Dependencies to Follow Along
Slap these in your hpack list and stack solver
away!
- cairo
- colour
- vector
- random-source
- random-fu
- mtl
- rvar
- transformers
- linear
If you plan to do raster graphics on your GPU as described, follow accelerate's getting started.