A demonstration of Pan
Beautiful images may be formed with beautuful programs - Conal ElliottIntroduction
If you wish to create visual effects, be they static images or animations, with your computer you have two alternatives; use some sort of interactive tool or program the visual effect, possibily using a graphics library.
For the most part however the process of creating images is a highly imperative task whether it is done, as is mostly the case, via an interactive tool, or programmed. So what exactly do I mean by imperative? What I mean is that images are generated in a sequential manner; you'll do something, then apply this or that filter, stretch the image, then change the palette of colours, and so on. It's a do-this-then-that approach to generating images. This is also the way most people program.
This is mostly to do with the fact that this style of constructing things seems natural to people. When you make something in real life, like a chest of drawers or a painting, be it with your hands or with the aid tools, there's really no other way to think about it; creating is an inherently time oriented process.
What is startling is that things don't have to be this way. Declarative programming languages, although not used heavily in industry (and oh, how they should be), have long allowed programmers to focus on what something should be rather than how it should be done. It has only been recently that people have thought of specifying images in a declarative style.But what exactly do I mean by declarative? "Can you give me an example", I hear you ask.
The image below depicts a blue circle that is 75% opaque overlapping a red circle (that is also 75% opaque), on a white background.
The imperative way of writing a program to display this image is to write something along the lines of the following pseudo-code:
1. draw red circle shifted slightly to right of centre
2. draw blue circle shifted slight to left of centre on top of
previous image.
The declarative code for this example goes something like this.
Define the final image as the composition of a shifted left blue circle on top of a shifted red circle.
In fact the declarative code for this particular example is so simple I can write it here directly.
circles = let blueCircle = colourRegion circle (0,0,1,0.75)
redCircle = colourRegion circle (1,0,0,0.75)
blueCircle' = translate (-5, 0) blueCircle
redCircle' = translate (5, 0) redCircle
in blueCircle' `over` redCircle'
Programming in this style separates the modelling from the
presentation of the image. The essential concept of the image is
expressed in the definition of images while the (often tedious)
details of the presentation are left to the computer to worry about.
Introduction to Pan
Is it of any harm that reading a program is so unlike viewing an artwork? After all, programs are meant to be understood and acted on by unfeeling, sequential machines, right? Not entirely. They also exist for humans to express ideas with clarity, to illuminate and inspire, and to be combined with other such idea expressions for forming richer results. To serve these human purposes, programs whould be expressed as close as possible to the essence of the underlying ideas, stripping away incidental artifacts of the machines on which they are intended to execute - Conal Elliott.
In 2001, Conal Elliott published a paper on a graphics library, Pan, for the synthesis of two-dimensional images and animations, which I urge you to read even if it's just for the pretty pictures. It is written in the declarative language, Haskell.
In Pan, images are functions. These functions map from continuous Cartesian co-ordinates to colours, which have red, green, blue and transparency components. For instance, a black circle with a radius of 30 would be defined as follows.
f (x,y) = let black = (0,0,0,1)
white = (1,1,1,0)
in if x*x + y*y < 30*30 then black else white
Understanding this bit of code relies on a couple of facts I haven't told you about just yet. First of all, the red, green, blue and transparency components of a colour can be between 0 and 1. So (0,0,0,1) represents a completely opaque black colour. Secondly, you'll have to think all the way back to high school mathematics and recall that a circle is defined as the region in which x*x + y*y < r*r, where r is the radius of the circle. We have Descartes to thank for this marriage of geometry and algebra.
The final image is displayed by sampling the image function repeatedly. We could, for instance, want to display the circle in the coordinate rectangle (-50,50) to (50, -50). But we can display this in a window of any size! I'll show with an example why this is the case.
Say we chose to display the portion of the image in the rectangle defined above in a window of physical size 640x480. The rectangle is 100 units wide. The real window is 640 pixels wide. We obtain a step size by dividing 100 by 640 to yield 0.15625. For the top-left pixel in the window we simply find out what the value of circle (-50, 50) is, which in this case is black. For the pixel immediately to its right we find out what the value of circle(-50 + 0.15625, 50) is. We repeat this process for the entire width until we get finally to circle (50, 50). This will happen after precisely 640 steps.
Resolution independence
The consequences of this method of defining and displaying images are really interesting! First of all, it is clear that the images are resolution independent. It does not matter what size window we display them in. Astoundingly, these windows do not even have to be rectangular. We also have full license to display any portion of the image we desire, so we can in effect pan left and right and also zoom to our hearts content. And here's the most beautiful thing. When we do zoom, more detail of the image is presented. We have not represented our images as mere bitmaps which, as most of us know, get chunkier as you zoom in upon them. Below I show a sequence of images that show the extra detail afforded as I zoom in upon the edge of a circle.
Infinite Images
The astute reader may have also noticed that the possibility exists for defining infinite images! Images are defined on continuous Cartesian co-ordinates; that is all we have specified. There is nothing stopping us from sampling the image function at co-ordinates such as (-12030405, 30000) or even (0.0000234, 0.015). We can define an infinite checkerboard by colouring points dark blue in which the sum of the truncated x and y co-ordinates are even and white if this sum is odd. (Truncation is the process of removing the fractional part of a number. e.g. 2.678 becomes the integer, 2.)
checkerboard (x,y) = if even (floor x + floor y) then darkBlue else white
Composability
But perhaps best of all, images are very naturally composable. That is, any image can have effects (called warps) applied to it, or be composed with another through the use of a combining form. I'm now going to show you the effect of warping the checkerboard using swirl.
swirl is an interesting warp that is defined in terms of polar co-ordinates. Polar co-ordinates are just an alternative way of defining the position of point. Instead of specifying the position in terms of an x and y co-ordinate, a radius and angle are used instead. I'm not going to show the definition of swirl lest I alienate some of my readers, but in general terms the swirl warp works by taking a point in the original image and then rotating it a distance that is proportional to its distance from the origin (i.e. the Cartesian co-ordinates (0,0)). Actually using swirl is very easy, and it yields the following image. (Again, I must stress; this is real code. The 30.0 affects the amount by which the points are rotated.)
swirledCheckerboard = swirl 30.0 checkerboard
Now, I don't blame you if you thought the image of the checkerboard didn't really give a sense of its infinite extent. (A very difficult thing to do in an inherently finite world.) So what we're going to do now is map an infinite checkerboard onto a finite area using a warp called circleLimit. The result looks like this.
circleLimitCheckerBoard = circleLimit checkerboard
A detailed example
I realise that towards the end of the previous section I was leaving out a lot of detail and just essentially showing you the pretty pictures. I'm trying to aim this page at a general audience that may include non-programmers. But I suspect that many of the visitors to this page will have the mathematical and computational literacy to understand an adequately explained example. So in this section I'm going to take you through the construction of a very simple effect that I saw on a close friends web site. As soon as I saw it I knew that I could recreate it in Pan in but a handful of lines of code. This section can be safely skipped by those who are overwhelmed by the details.
The effect we will be looking is this:
Look closely. You can see that this effect is the overlap of a pair of fans like this:
Imagine two of these fans slightly separated and superimposed on each other. Wherever we have red on red, or white on white the final image is coloured red. Conversely, wherever we have red on white or vice versa we colour it red.
Now I'm going to show you how to create the final effect. We'll begin with the fan effect. The key to composing two of these together in the manner I explained just above is defined the fan as a region not an image. A region is like an image only it is a function mapping from continuous Cartesian co-ordinates to boolean values. (Booleans can take one of two values - true or false and are named after the 19th century logician George Boole.) The fan is best described in polar co-ordinates. We're going to let it take another argument ang which describes how wide in radians (a unit for angles common in trigonometry) each fan element is.
fan ang (x,y) = let (r,a) = toPolar (x,y)
in even (floor (a / ang))
The part just after the let simply converts the Cartesian co-ordinates to polar co-ordinates. The body of the definition evaluates to true if the angle of the point divided by the width ang is even or not. So say ang = pi/6 and (r,a) = (34.23, pi/2) then floor (pi/2 / pi/6) = 3 which is clearly odd, so the resulting value would be false.
It is a simple matter to write a function which takes a region and turns it into an image by colouring all points which evaluate to true to a colour supplied by the programmer. But we won't be using this function just yet. What we want to do now is overlap them. The way we do this is with a function called xorR which takes the exclusive or of two regions. This is done according to the following truth table.
| A | B | A xor B |
| true | true | false |
| true | false | true |
| false | true | true |
| false | false | false |
The function is defined as follows:
xor True True = False xor True False = True xor False True = True xor False False = False xorR reg1 reg2 (x,y) = reg1 (x,y) `xor` reg2 (x,y)
The final function diamonds ties it all together. It takes an argument ang just like fan did but it also takes d, a distance which specifies the distance between the origins of the two fans. (By origin, I mean the point at which the elements of the fan project from.)
diamonds d a =
colourRegion
(translate (-(d/2), 0) (fan a)
`xorR` translate (d/2, 0) (fan a)) darkRed
And voila, we have the effect displayed above. This particular picture was produced with ang = pi/6 and d = 200.