art with code

2008-12-07

How hex.hs works

I posted this small Haskell + Gtk2Hs graphics demo yesterday. Let me explain how it works.

Hex.hs is written in a pretty imperative fashion, it mostly happens inside Gtk2Hs's Cairo Render monad. The core of the top-level drawing loop works like this:

mapM_ (\y -> do
mapM_ (drawHexagons rotation cylinderRadius rowCount col) [col*2 .. col*2+rowCount/6-4])
[0..columns-1]

It draws a row of hexagons around a cylinder at y-offset col, for x-offsets from col*2 to col*2 + rowCount/6 - 4. As the x-offset grows with the y-offset, the rows are offset from each other, forming a diagonal stripe moving down and to the right. But because we are drawing the hexagons on a cylinder, the stripe moves down and around the cylinder.

The rotation parameter gives the initial rotation of the cylinder coordinate system, and is based on the current time. As time changes, the rotation does too. And as we draw a new frame with a new time after having shown the previous one, we get an animation.

drawHexagons calls drawHexagon twice, drawing a \-segment of a hexagon row. The drawHexagon call does all the actual drawing and goes as follows:

drawHexagon rotation cylinderRadius rowCount col row = do
-- offset odd rows down (remember that we draw like \\\\)
let y = if (floor row) `mod` 2 == 0 then 0 else 1.732

-- transform the hexagon from [-1..1] coordinates to the cylinder coordinate system
-- read from bottom up
let rhex = map (
scaleP (2*pi*r/rowCount) . -- scale up so that rowCount hexagons go around the cylinder
translateP (rowCount*rot/(2*pi) + row) (y+col*1.732*2) . -- move it to the wanted position
rotateP (pi/2) -- rotate the hexagon 90 degrees
) hexagon

-- project the hexagon from cylinder coordinates over to screen coordinates
let hex = map (cylinderProjection r) rhex

-- and draw the hexagon
save
newPath
uncurry moveTo $ head hex -- move to the first point of the hexagon
mapM_ (uncurry lineTo) $ tail hex -- apply lineTo to the rest of the points
closePath -- and close the path
setLineWidth 1

-- fill some of the hexagons and stroke the rest
if (floor (row+col)) `mod` 4 == 0
then fill
else stroke
restore

Then we need the definition of a hexagon:

-- ngon creates a regular polygon as a list of (x,y)-tuples in [-1..1] coordinate space.
ngon n =
map nrot [0..n-1]
where nrot i = let a = 2*pi*i/n in
(cos a, sin a)

hexagon = ngon 6

The function to project cylinder coordinates to display space:

-- maps the x-coordinate around a cylinder, growing right so that
-- 0 => 0, 0.5*pi*r => r, pi*r => 0, 1.5*pi*r => -r and 2*pi*r => 0
cylinderProjection r (x, y) = (r * sin (x/r), y)

And the 2D point transformations:
 
scaleP f (x,y) = (x*f, y*f)
translateP u v (x,y) = (x+u, y+v)
rotateP a (x,y) = (cos a * x - sin a * y, sin a * x + cos a * y)

And there we have it. Create coordinates for the objects you want to draw, project them to the screen space, and draw them. Simple as pie.

1 comment:

Jared said...

Beautiful. Thanks for sharing.

Blog Archive