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.
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.
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
setSourceRGBA 0 0 0 1)
rectangle 0 0 500 500)
Draw instructions always use the current path in the Render monad.
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
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.
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
For example, with this matte:
And this source:
alphaMatte matte source looks like this:
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).
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.
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 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:
We can render animations in two ways:
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:
If you've decided to start writing art with Haskell, hurray! Here are some good readings for your journey.
If you make something, come share your work at /r/generative! We're friendly!
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.
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.