diff --git a/gruff.cabal b/gruff.cabal
--- a/gruff.cabal
+++ b/gruff.cabal
@@ -1,5 +1,5 @@
 Name:                gruff
-Version:             0.2
+Version:             0.2.1
 Synopsis:            fractal explorer GUI using the ruff library
 Description:
     Mandelbrot Set fractal explorer using the ruff library.
@@ -10,15 +10,25 @@
     with integer-simple instead of the default integer-gmp.  To install
     without MPFR support, use @cabal install gruff -f-mpfr@.
     .
+    This version is a bugfix release.  Changes since gruff-0.2:
+    .
+      * Supersampling bugs are fixed (no more undocumented quantization
+        leading to inconsistent appearance at different zoom levels).
+    .
+      * Race condition bugs in cache management are fixed (no more
+        incompletely rendered images).
+    .
+      * Internal changes/refactoring to make future additions easier.
+    .
     Features in this version include:
     .
       * Interactive fractal browser display (left click to zoom in,
         right click to zoom out, middle-click to center).
     .
-      * Session persistance (stored in @~/.gruff/state.gruff@ - states can
+      * Session persistance (stored in @~\/.gruff\/state.gruff@ - states can
         also be loaded from and saved to other files).
     .
-      * Tile cache (by default in @~/.gruff/cache@ - symlink it somewhere
+      * Tile cache (by default in @~\/.gruff\/cache@ - symlink it somewhere
         with a few GB of space if you plan on exploring a lot).
     .
       * High-level feature finding using angled internal addresses
@@ -37,7 +47,7 @@
       * Limited amount of customizable colouring (colours for interior,
         border, and exterior points).
     .
-      * Supersampling for more detailed images (useful range is 1 to 4).
+      * Supersampling for more detailed images (useful range is 1 to 16).
     .
     Future features might include image saving, external ray and feature
     information overlays, more feature finding and identification
diff --git a/src/Browser.hs b/src/Browser.hs
--- a/src/Browser.hs
+++ b/src/Browser.hs
@@ -2,7 +2,7 @@
 
 import Prelude hiding (log)
 import Control.Concurrent
-  ( forkIO, MVar, newMVar, takeMVar, putMVar, tryTakeMVar, threadDelay )
+  ( forkIO, MVar, newEmptyMVar, newMVar, takeMVar, putMVar, tryTakeMVar, threadDelay )
 import Control.Monad (foldM, forever, forM_, replicateM, unless, when)
 import Data.IORef (IORef, newIORef, readIORef, atomicModifyIORef)
 import Data.List (sortBy)
@@ -71,6 +71,9 @@
   , combineProg :: Program
   , tsheet0 :: TextureObject
   , tsheet1 :: TextureObject
+  , sheetCount0 :: Int
+  , sheetCount1 :: Int
+  , sheetCountTarget :: Int
   , fbo :: GLuint
   , cacheSizeMin :: Int
   , cacheSizeMax :: Int
@@ -83,8 +86,8 @@
   , doClear :: Bool
   }
 
-sheetSize :: GruffImage -> Size
-sheetSize g = let s = texels . bufferSize . window $ g in Size s s
+sheetSize :: GruffImage -> BufferSize
+sheetSize g = bufferSize (sheetOffset g) (window g)
 
 zoomPhase :: GruffImage -> Double
 zoomPhase = delta . location
@@ -92,9 +95,23 @@
 rotationA :: GruffImage -> Double
 rotationA = orient . viewport
 
+sheetBlend :: GruffImage -> (Int, Double)
+sheetBlend g =
+  let z = zoomPhase g
+      s = supersamples (window g)
+      b = logBase 4 (4 ** z * s)
+      h = floor b
+      d = fromIntegral h - b
+  in  (h, 1 - (1 - 4**d) / (4**(d+1) - 4**d))
+
+sheetOffset :: GruffImage -> Int
+sheetOffset = fst . sheetBlend
+
+blendFactor :: GruffImage -> Double
+blendFactor = snd . sheetBlend
+
 iDisplay :: IORef GruffImage -> IO ()
 iDisplay iR = do
-  prune iR
   s0 <- readIORef iR
   mtls <- tryTakeMVar (queue s0)
   case mtls of
@@ -116,11 +133,11 @@
   when c $ do
     clearSheet s False
     clearSheet s True
-  todo0 <- renderSheet s False
-  todo1 <- renderSheet s True
-  atomicModifyIORef iR $ \s' -> (s'{ viewQuads = (todo0, todo1) }, ())
-  let complete = null todo0 && null todo1
-      w = width (window s)
+    atomicModifyIORef iR $ \s' -> (s'{ sheetCount0 = 0, sheetCount1 = 0 }, ())
+  (todo0, upped0) <- renderSheet s False
+  (todo1, upped1) <- renderSheet s True
+  atomicModifyIORef iR $ \s' -> (s'{ viewQuads = (todo0, todo1), sheetCount0 = sheetCount0 s' + upped0 , sheetCount1 = sheetCount1 s' + upped1 }, ())
+  let w = width (window s)
       h = height (window s)
   GL.viewport $=
     (GL.Position 0 0, GL.Size (fromIntegral w) (fromIntegral h))
@@ -134,10 +151,7 @@
   lblend  <- get $ uniformLocation (combineProg s) "blend"
   uniform lsheet0 $= TexCoord1 (0 :: GLint)
   uniform lsheet1 $= TexCoord1 (1 :: GLint)
-  uniform lblend $= TexCoord1 (realToFrac (
-    let p = negate $ zoomPhase s
-        q = 4 -- realToFrac $ supersamples (window s) -- FIXME figure out correct blend
-    in  1 - (1 - q**p) / (q**(p + 1) - q**p) ) :: GLfloat)
+  uniform lblend $= TexCoord1 (realToFrac (blendFactor s) :: GLfloat)
   activeTexture $= TextureUnit 0
   textureBinding Texture2D $= Just (tsheet0 s)
   activeTexture $= TextureUnit 1
@@ -155,7 +169,7 @@
       v :: Int -> Int -> IO ()
       v x y = vertex $ Vertex2
         (fromIntegral x :: GLdouble) (fromIntegral y :: GLdouble)
-      k = 0.25 * realToFrac (0.5 ** zoomPhase s) :: GLdouble
+      k = 0.125 * realToFrac (0.5 ** zoomPhase s) :: GLdouble
   renderPrimitive Quads $ do
     t (-1) 1 >> v 0 h
     t (-1) (-1) >> v 0 0
@@ -165,17 +179,21 @@
   activeTexture $= TextureUnit 0
   textureBinding Texture2D $= Nothing
   currentProgram $= Nothing
-  when complete $ case doneCallback s of
-    Nothing -> return ()
-    Just act -> do
-      atomicModifyIORef iR $ \s' -> (s'{ doneCallback = Nothing, abortCallback = Nothing }, ())
-      act
+  s'' <- readIORef iR
+  when (sheetCount0 s'' + sheetCount1 s'' >= sheetCountTarget s'') $ do
+    case doneCallback s of
+      Nothing -> return ()
+      Just act -> do
+        atomicModifyIORef iR $ \s' -> (s'{ doneCallback = Nothing, abortCallback = Nothing }, ())
+        act
+  prune iR
   err <- glGetError
   when (err /= 0) $ print ("error: " ++ show err)
 
 clearSheet :: GruffImage -> Bool -> IO ()
 clearSheet s b = do
-  let Size tw' th' = sheetSize s
+  let tw' = texels $ sheetSize s
+      th' = tw'
       tw = if b then tw' * 2 else tw'
       th = if b then th' * 2 else th'
       tsheet = (if b then tsheet1 else tsheet0) s
@@ -190,14 +208,15 @@
   glGenerateMipmap gl_TEXTURE_2D
   textureBinding Texture2D $= Nothing
 
-renderSheet :: GruffImage -> Bool -> IO [(Complex Int, Quad)]
+renderSheet :: GruffImage -> Bool -> IO ([(Complex Int, Quad)], Int)
 renderSheet s b = do
-  let Size tw' th' = sheetSize s
+  let tw' = texels $ sheetSize s
+      th' = tw'
       tw = if b then tw' * 2 else tw'
       th = if b then th' * 2 else th'
       tsheet = (if b then tsheet1 else tsheet0) s
       vquads = (if b then snd else fst) (viewQuads s)
-  if null vquads then return [] else do
+  if null vquads then return ([], 0) else do
     bindFBO (fbo s) tsheet
     GL.viewport $=
       (GL.Position 0 0, GL.Size (fromIntegral tw) (fromIntegral th))
@@ -220,11 +239,12 @@
     todo <- foldM (\a t -> do a' <- drawQuad (tiles s) t ; return (a' ++ a)) [] vquads
     currentProgram $= Nothing
     unbindFBO
-    when (length todo < length vquads) $ do
+    let upped = length vquads - length todo
+    when (upped > 0) $ do
       textureBinding Texture2D $= Just tsheet
       glGenerateMipmap gl_TEXTURE_2D
       textureBinding Texture2D $= Nothing
-    return todo
+    return (todo, upped)
 
 drawQuad :: Map Quad TextureObject3 -> (Complex Int, Quad) -> IO [(Complex Int, Quad)]
 drawQuad m ijq@(i :+ j, q) = case q `M.lookup` m of
@@ -331,7 +351,7 @@
       , " --> ", show (cacheSizeMin s0)
       ]
     bad <- atomicModifyIORef sR $ \s -> 
-      let Just q0 = originQuad (location s) (bufferSize (window s))
+      let Just q0 = originQuad (location s) (sheetSize s)
           score = quadDistance q0
           o = comparing (score . fst)
           (good, bad)
@@ -345,17 +365,17 @@
   s' <- readIORef sR
   log s' Debug $ "updateCallback "
   todo' <- atomicModifyIORef sR $ \s ->
-    let vq@(qs0, qs1) = fromMaybe ([],[]) $ visibleQuads (window s) (viewport s) (location s)
+    let vq@(qs0, qs1) = fromMaybe ([],[]) $ visibleQuads (window s) (viewport s) (location s) (sheetOffset s)
         qs = qs0 ++ qs1
         todo = S.filter (`M.notMember` tiles s) (S.fromList (map snd qs))
-    in  (s{ viewQuads = vq }, todo)
+    in  (s{ viewQuads = vq, sheetCountTarget = length qs }, todo)
   -- cancel in-progress jobs that aren't still needed
   _ <- takeMVar (jobs s')
   p <- atomicModifyIORef sR $ \s -> (s{ progress = M.filter (`S.member` todo') (progress s) }, progress s)
   mapM_ (`poke` 1) . filter (\w -> case M.lookup w p of Nothing -> False ; Just q -> q `S.notMember` todo') . workers $ s'
   -- set new jobs
   s <- readIORef sR
-  let Just q0 = originQuad (location s) (bufferSize (window s))
+  let Just q0 = originQuad (location s) (sheetSize s)
       score = quadDistance q0
   putMVar (jobs s') . sortBy (comparing score) . S.toList $ todo' \\ S.fromList (M.elems p)
 
@@ -426,20 +446,27 @@
 
 data Browser = Browser
   { browserWindow :: GTK.Window
+  , browserGL :: GLUTGtk
   , browserRender :: Image -> IO () -> IO () -> IO ()
   , browserAbort  :: IO ()
   , browserSaveImage :: FilePath -> IO ()
   , browserSetExitCallback :: IO () -> IO ()
   , browserSetReshapeCallback :: (Int -> Int -> IO ()) -> IO ()
   , browserSetMouseCallback :: (Complex R -> Double -> IO ()) -> IO ()
-  , browserResize :: Int -> Int -> Double -> IO ()
+--  , browserResize :: Int -> Int -> Double -> IO ()
   }
 
 browserRenders :: Browser -> [(Image, FilePath)] -> IO ()
 browserRenders _ [] = print "done"
-browserRenders b ((i, f):ifs) = browserRender b i
-  (browserSaveImage b f >> browserRenders b ifs)
-  (print "aborted")
+browserRenders b ((i, f):ifs) = do
+  result <- newEmptyMVar
+  postGUISync $ do
+          browserRender b i
+            (browserSaveImage b f >> putMVar result True)
+            (putMVar result False)
+          postRedisplay (browserGL b)
+  r <- takeMVar result
+  if r then browserRenders b ifs else print "aborted"
 
 browserNew :: GLUTGtk -> Pixbuf -> Logger -> FilePath -> IO Browser
 browserNew gl' icon lg cacheDir' = do
@@ -484,6 +511,9 @@
     , combineProg = error "combineProg"
     , tsheet0 = TextureObject 0
     , tsheet1 = TextureObject 0
+    , sheetCount0 = 0
+    , sheetCount1 = 0
+    , sheetCountTarget = maxBound
     , cacheSizeMin = 1000
     , cacheSizeMax = 1500
     , doClear = False
@@ -495,7 +525,11 @@
         i <- readIORef iR
         putJobs iR []
         mapM_ (`poke` 1) . workers $ i
-        act <- atomicModifyIORef iR $ \s -> (s{ progress = M.empty, doneCallback = Nothing, abortCallback = Nothing }, abortCallback s)
+        act <- atomicModifyIORef iR $ \s -> (s
+          { progress = M.empty
+          , doneCallback = Nothing
+          , abortCallback = Nothing
+          }, abortCallback s)
         fromMaybe (return ()) act
       atExit = do
         i <- readIORef iR
@@ -503,13 +537,22 @@
           Nothing -> return ()
           Just act -> act
       browserRender' img done aborted = do
+        s <- readIORef iR
+        let wr = width  (imageWindow img)
+            hr = height (imageWindow img)
+        unless (width (window s) == wr && height (window s) == hr) $ do
+          windowResize iw wr hr
+        atomicModifyIORef iR $ \s' -> (s'
+          { window = imageWindow img
+          , viewport = imageViewport img
+          , location = imageLocation img
+          , colours = imageColours img
+          }, ())
+        s' <- readIORef iR
+        unless (sheetSize s == sheetSize s') (reallocateBuffers iR)
         atomicModifyIORef iR $ \i -> (i
           { doneCallback = Just done
           , abortCallback = Just aborted
-          , location = imageLocation img
-          , viewport = imageViewport img
-          , window = imageWindow img
-          , colours = imageColours img
           , doClear = True
           }, ())
         update iR
@@ -522,19 +565,20 @@
         ( i{ Browser.reshapeCallback = Just act }, () )
       browserSetMouseCallback' act = atomicModifyIORef iR $ \i ->
         ( i{ mouseCallback = Just act }, () )
-      browserResize' w h s = windowResize iw w h >> iReshape iR (Just s) (Size w h)
+--      browserResize' w h s = windowResize iw w h >> iReshape iR (Just s) (Size w h)
   keyboardMouseCallback gl' $= iMouse iR
   _ <- timeoutAdd (timer iR >> return True) msPerFrame
   _ <- iw `onDestroy` atExit
   return Browser
     { browserWindow = iw
+    , browserGL = gl'
     , browserRender = browserRender'
     , browserAbort = browserAbort'
     , browserSaveImage = browserSaveImage'
     , browserSetExitCallback = browserSetExitCallback'
     , browserSetReshapeCallback = browserSetReshapeCallback'
     , browserSetMouseCallback = browserSetMouseCallback'
-    , browserResize = browserResize'
+--    , browserResize = browserResize'
     }
 
 fromColours :: Colours -> (Color3 GLfloat, Color3 GLfloat, Color3 GLfloat)
@@ -572,13 +616,17 @@
     , combineProg = combineProg'
     , tsheet0 = tsheet0'
     , tsheet1 = tsheet1'
+    , sheetCount0 = 0
+    , sheetCount1 = 0
+    , sheetCountTarget = maxBound
     }, ())
   reallocateBuffers iR
 
 reallocateBuffers :: IORef GruffImage -> IO ()
 reallocateBuffers iR = do
   s <- readIORef iR
-  let Size tw' th' = sheetSize s
+  let tw' = texels $ sheetSize s
+      th' = tw'
       ts = [tsheet0 s, tsheet1 s]
   forM_ (ts `zip` [(tw', th'), (tw' * 2, th' * 2)]) $ \(t, (tw, th)) -> do
     texture Texture2D $= Enabled
@@ -591,6 +639,7 @@
     textureWrapMode Texture2D T $= (Repeated, ClampToEdge)
     textureBinding Texture2D $= Nothing
     texture Texture2D $= Disabled
+  atomicModifyIORef iR $ \s' -> (s'{ sheetCount0 = 0, sheetCount1 = 0, sheetCountTarget = maxBound }, ())
 
 newFBO :: IO GLuint
 newFBO = alloca $ \p -> glGenFramebuffers 1 p >> peek p
diff --git a/src/View.hs b/src/View.hs
--- a/src/View.hs
+++ b/src/View.hs
@@ -51,8 +51,10 @@
 diagonal2 :: Window -> Int
 diagonal2 w = width w * width w + height w * height w
 
+{-
 resolution :: Window -> Double
-resolution w = supersamples w * fromIntegral (windowSize w)
+resolution w = sqrt $ supersamples w * fromIntegral (diagonal2 w)
+-}
 
 pixelLocation :: Window -> Viewport -> Location -> Double -> Double -> Complex Rational
 pixelLocation w v l x y =
@@ -71,7 +73,7 @@
       si = sin a
       x2 =  co * x1 + si * y1
       y2 = -si * x1 + co * y1
-      r = 2 * radius l
+      r = radius l
   in  (r * x2) :+ (r * y2)
 
 data BufferSize = BufferSize
@@ -79,11 +81,11 @@
   }
   deriving (Read, Show, Eq)
 
-bufferSize :: Window -> BufferSize
-bufferSize w = BufferSize{ texels = max tileSize . roundUp2 . ceiling . resolution $ w }
+bufferSize :: Int -> Window -> BufferSize
+bufferSize o w = BufferSize{ texels = roundUp2 . ceiling . ((2::Double) ^^ o *) . sqrt . fromIntegral . diagonal2 $ w }
 
 roundUp2 :: Int -> Int -- fails for too small and too large inputs
-roundUp2 x = last . takeWhile (x >) . iterate (2 *) $ 1
+roundUp2 x = head . dropWhile (x >=) . iterate (2 *) $ 1
 
 level :: Location -> Int
 level = floor . negate . logBase 2 . radius
@@ -148,13 +150,13 @@
       , (i1 :+ j1, SouthEast `child` q)
       ]
 
-visibleQuads :: Window -> Viewport -> Location -> Maybe ([(Complex Int, Quad)], [(Complex Int, Quad)])
-visibleQuads w v l = do
-  let b = bufferSize w
+visibleQuads :: Window -> Viewport -> Location -> Int -> Maybe ([(Complex Int, Quad)], [(Complex Int, Quad)])
+visibleQuads w v l o = do
+  let b = bufferSize o w
       a = orient v
       co = cos a
       si = sin a
-      k = 0.5 ** delta l * supersamples w
+      k = 0.5 ** (delta l - fromIntegral o)
       x1 = k * fromIntegral (width  w)
       y1 = k * fromIntegral (height w)
       x0 = - x1
diff --git a/src/gruff.hs b/src/gruff.hs
--- a/src/gruff.hs
+++ b/src/gruff.hs
@@ -2,9 +2,10 @@
 module Main (main) where
 
 import Prelude hiding (catch, log)
+--import Control.Concurrent (forkIO)
 import Control.Monad (forM_, liftM3, when)
 import Data.IORef (IORef, newIORef, readIORef, atomicModifyIORef, writeIORef)
-import Data.Maybe (isJust, fromMaybe)
+import Data.Maybe (isJust, fromMaybe) --  , mapMaybe)
 import Graphics.UI.Gtk hiding (get, Region, Size, Window, Viewport)
 import qualified Graphics.UI.Gtk as GTK
 import Graphics.UI.Gtk.OpenGL (initGL)
@@ -16,9 +17,9 @@
 
 import Paths_gruff (getDataFileName)
 import Number (R)
-import Browser (Browser(..), browserNew)
+import Browser (Browser(..), browserNew) --  , browserRenders)
 import MuAtom (MuAtom(..), MuProgress(..), muFromAddress, MuProgress'(..), muToAddress, MuProgress''(..), muLocate)
-import View (Image(..), Location(..), Viewport(..), Window(..), Colours(..), Colour(..), defWindow, defViewport)
+import View (Image(..), Location(..), Viewport(..), Window(..), Colours(..), Colour(..), defWindow, defViewport) --  , defColours)
 import GLUTGtk (glut, Size(Size), postRedisplay)
 import Logger (logger, LogLevel(Debug))
 import qualified Logger as Log
@@ -206,6 +207,7 @@
       entI e a = do
         _ <- e g0 `onEntryActivate` (entryGetText (e g0) >>= wrapE g0 gR a >> upI)
         return ()
+{-
       entI' e a = do
         _ <- e g0 `onEntryActivate` (do
           entryGetText (e g0) >>= wrapE g0 gR a
@@ -213,6 +215,7 @@
           browserResize browser (width (gWindow g)) (height (gWindow g)) (supersamples (gWindow g))
           upI)
         return ()
+-}
       colI cbi cbb cbe a = forM_ [cbi, cbb, cbe] $ \c -> do
         _ <- c g0 `onColorSet` (do
           ci <- colorButtonGetColor (cbi g0)
@@ -260,7 +263,7 @@
           , () )
         g <- readIORef gR
         uEverything g0 g
-        upI
+--        upI
   butI bHome aHome
   butI bLoad aLoad
   but  bSave aSave
@@ -273,9 +276,14 @@
   entME eImagM eImagE aImag
   entME eSizeM eSizeE aSize
   entI eRota aRota
+  entI eWidth aWidth
+  entI eHeight aHeight
+  entI eSamples aSamples
+{-
   entI' eWidth aWidth
   entI' eHeight aHeight
   entI' eSamples aSamples
+-}
   colI cInterior cBorder cExterior aColours
   let aExit' = (exit (Log.log lg) stateFile =<< readIORef gR)
   browserSetExitCallback browser aExit'
@@ -288,6 +296,7 @@
   refreshGUI g0 g
   widgetShowAll iw
   widgetShowAll ww
+--  _ <- forkIO $ script browser
   mainGUI
 
 data Gruff = Gruff
@@ -317,7 +326,7 @@
   { gAddress    = parseAngledInternalAddress "1"
   , gReal       = Just 0
   , gImag       = Just 0
-  , gSize       = Just 1
+  , gSize       = Just 2
   , gRota       = Just 0
   , gColours    = (red, black, white)
   , gWindow     = defWindow
@@ -421,7 +430,7 @@
         MuSuccess'' mu -> do
           let g' = g{ gReal = Just . realPart . muNucleus $ mu
                     , gImag = Just . imagPart . muNucleus $ mu
-                    , gSize = Just . (* 4) . muSize $ mu
+                    , gSize = Just . (* 16) . muSize $ mu
                     , gRota = Just . subtract (pi/2) . muOrient $ mu
                     }
           progress "Found!"
@@ -478,7 +487,7 @@
           MuSuccess mu     -> do
             let g' = g{ gReal = Just . realPart . muNucleus $ mu
                       , gImag = Just . imagPart . muNucleus $ mu
-                      , gSize = Just . (* 4) . muSize $ mu
+                      , gSize = Just . (* 16) . muSize $ mu
                       , gRota = Just . subtract (pi/2) . muOrient $ mu
                       }
             progress "Done!"
@@ -556,3 +565,95 @@
 
 fromColor :: Color -> Colour
 fromColor (Color r g b) = Colour (fromIntegral r / 65535) (fromIntegral g / 65535) (fromIntegral b / 65535)
+
+{-
+strings :: [String]
+strings =  [ "1 1/3 " ++ (unwords . map show . scanl (+) 3) (replicate m 1 ++ replicate (n - m) 2) | m <- [0 .. n - 1] ]
+        ++ [ "1 1/3 " ++ (unwords . map show . scanl (+) 3) (replicate m 2 ++ replicate (n - m) 1) | m <- [0 .. n - 1] ]
+  where n = 21 -- 15
+
+script :: Browser -> IO ()
+script b = browserRenders b images'
+
+filename :: String -> Int -> String
+filename s n = (reverse . take 4 . (++ "0000") . reverse . show) n ++ "__" ++ map filechar s ++ ".ppm"
+  where filechar ' ' = '_'
+        filechar '/' = '-'
+        filechar  c  =  c
+
+findMu :: AngledInternalAddress -> Maybe MuAtom
+findMu a = case last (muFromAddress a) of
+  MuSuccess m -> Just m
+  _ -> Nothing
+
+scene :: (String, Int) -> Maybe (View.Image, String)
+scene (s, n) = do
+  a <- parseAngledInternalAddress s
+  m <- findMu a
+  let cx :+ cy = muNucleus m
+      f = filename s n
+      i = Image
+            { imageLocation = Location{ center = toRational cx :+ toRational cy, radius = muSize m * 8 } -- 6
+            , imageViewport = viewportDVD{ orient = muOrient m - pi / 2 }
+            , imageWindow = windowDVD
+            , imageColours = defColours
+            }
+  return (i, f)
+
+windowDVD :: View.Window
+windowDVD = Window{ width = 1080, height = 576, supersamples = 2 }
+
+viewportDVD :: View.Viewport
+viewportDVD = Viewport{ aspect = 1080/576, orient = 0 }
+
+images :: [(View.Image, String)]
+images = mapMaybe scene (score `zip` [0..])
+
+kick, snare :: Int -> [String]
+kick n = [ "1 2 " ++ (unwords . map show . take m . scanl (+) (3 :: Int) . repeat) 1 | m <- [n, n - 1 .. 1] ]
+snare n = [ "1 2 " ++ (unwords . map show . take (2 * m) . filter (\x -> x `mod` 3 /= 0)) [(3 :: Int) ..] | m <- [n, n - 1 .. 1] ]
+
+score :: [String]
+score = concat
+  [ kick 21
+  , snare 32
+  , kick 20
+  , snare 11
+  , kick 8
+  , kick 8
+  , kick 5
+  , snare 32
+  , kick 20
+  , snare 11
+  ]
+
+images' :: [(View.Image, String)]
+images' = {-drop 49 . -} map frame . take 1200 $ [0 ..]
+
+frame :: Int -> (View.Image, String)
+frame n = (img, (reverse . take 4 . (++ "0000") . reverse . show) n ++ ".ppm")
+  where
+    t = fromIntegral n / 25
+    r = 4 * 2 ** negate t
+    a = c0a * t / 48
+    img = Image
+      { imageLocation = Location{ center = c0x :+ c0y, radius = r }
+      , imageViewport = viewportHD{ orient = a }
+      , imageWindow = windowHD
+      , imageColours = defColours
+      }
+
+windowHD :: View.Window
+windowHD = Window{ width = 1920, height = 1080, supersamples = 5 }
+
+viewportHD :: View.Viewport
+viewportHD = Viewport{ aspect = 1920/1080, orient = 0 }
+
+c0x, c0y :: Rational
+c0x = -1.249654501250026535305564290406342641357245044812024384076696041016544263255410210573644817965421364058466642283047810126900443791060999959661828661014819271180654290498245641835647321878524004347784493127325654028742057010462138345395182421783064045422740334072967042829523926305969086019115635907387177
+c0y =  2.62964442060409470886060596680625049122647005543488791701840256388323764840498489578760240122036234473238581692886963706321452429283848754125169118310897168288049653283033547961422702057918968158064636527854942947851448063883186797623535011066501329732840334929404268157442692714476422929470502521447535e-2
+
+c0a :: Double
+c0a = -2.651738023427396
+
+-}
