diff --git a/ChangeLog b/ChangeLog
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,6 +1,42 @@
-ChangeLog exported from Git by claude@zebimus at 2010-07-12T20:23:57+0100
+ChangeLog exported from Git by claude@zebimus at 2010-07-25T03:55:40+0100
 
- (HEAD, tag: v0.4, origin/master, origin/HEAD, master)
+ (HEAD, tag: v0.5, origin/master, origin/HEAD, master)
+
+7b5e311 2010-07-25 03:51:26 +0100 v0.5 neon in O(1) space
+69ec357 2010-07-25 03:21:02 +0100 fix annoying disappearing/appearing julias at edge of window
+cf6fb48 2010-07-25 02:42:00 +0100 fix aspect ratio to keep julias square
+ac1f7be 2010-07-25 02:41:18 +0100 fix displaySize for aspect ratios other than 16:9
+05a5e35 2010-07-25 02:23:22 +0100 fix screenshot
+099b264 2010-07-25 02:12:40 +0100 bump version number
+                                  add note about SSE4 flag
+98d09bb 2010-07-25 02:05:04 +0100 update to current code
+a94f0e7 2010-07-25 02:04:43 +0100 update to current code
+1baef1a 2010-07-25 02:00:51 +0100 fix screenshots/recording comments
+9c350a8 2010-07-25 02:00:10 +0100 prune dead code
+e59440e 2010-07-25 01:28:20 +0100 merge random and interactive configurations into one main configuration
+c874936 2010-07-25 01:26:00 +0100 reduce heap limit as we can run in constant space now
+c013ea2 2010-07-25 01:25:28 +0100 recompute tiling on instead of sharing to fix massive memory leak
+                                  get statistics for Lua interface
+56edf21 2010-07-25 01:23:33 +0100 strictify to avoid memory leak
+ebd2a33 2010-07-25 01:22:38 +0100 attempt to fix memory leak by pattern-matching on ()
+177423a 2010-07-25 01:21:48 +0100 attempt to store statistics more strictly
+b1f0293 2010-07-25 01:21:06 +0100 strict version of modifyIORef
+74d8184 2010-07-23 05:56:43 +0100 additional file to distribute
+2296642 2010-07-23 05:52:48 +0100 colour using Mandelbrot Set escape time in addition
+1631f5b 2010-07-23 05:51:53 +0100 larger field for Julia Set renders
+89e15fd 2010-07-23 05:50:13 +0100 improved escapetime to colour function
+39aed4f 2010-07-23 05:46:21 +0100 remove unused includes
+5f9aa1a 2010-07-23 05:40:33 +0100 background back to black
+01bca99 2010-07-23 05:38:22 +0100 larger display size
+8086318 2010-07-23 05:37:49 +0100 use filename for reinclusion defence macro
+ccaf263 2010-07-23 00:06:25 +0100 ignore binary
+bddc637 2010-07-23 00:05:52 +0100 avoid visiting near recent history to make attract mode more interesting
+88647a8 2010-07-20 22:59:16 +0100 attract mode improvements: self-centering random walk for zoom level
+                                  target border of Mandelbrot set as more interesting
+                                  random walk near previous target
+320e8ee 2010-07-20 22:57:04 +0100 exterior distance from Mandelbrot Set
+
+ (tag: v0.4)
 
 9d98a8c 2010-07-12 20:17:15 +0100 v0.4 joining the cabal
 3a4ccd5 2010-07-12 20:08:50 +0100 fix base dependency to allow successful compilation with ghc-6.10.4
diff --git a/HACKING b/HACKING
--- a/HACKING
+++ b/HACKING
@@ -29,6 +29,6 @@
 src/Vector.hs           - linear algebra ((2+1)D homegenous coordinates)
 
 config/defaults.lua     - default settings (to be used by configurations)
-config/interactive.lua  - configuration for interactive navigation
-config/random.lua       - configuration for random navigation
-config/transition.lua   - interpolate between viewports (used by 'random')
+config/distance.lua     - compute Mandelbrot exterior distance (used by 'main')
+config/main.lua         - 'main' configuration (interact and attract modes)
+config/transition.lua   - interpolate between viewports (used by 'main')
diff --git a/NEWS b/NEWS
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,11 @@
+v0.5 2010-07-25 neon in O(1) space
+
+    This version runs in asymptotic constant space.  Aesthetics tweaks
+    give more colourful 'neon' output, and the randomized attract mode
+    picks more interesting places to explore.
+
+    Source code statistics: 2072 lines, 10050 words, 58399 chars.
+
 v0.4 2010-07-12 joining the cabal
 
     This version is installable with 'cabal-install'.  Documentation has
diff --git a/README b/README
--- a/README
+++ b/README
@@ -7,23 +7,36 @@
 Invocation
 ----------
 
-Multi-core CPU with lots of RAM recommended.  For two cores run:
+Multi-core CPU with lots of RAM recommended:
 
-mandulia +RTS -N2 -M2G -RTS configuration
+    mandulia configuration
 
-Currently there are two preset configurations available:
-'interactive' -- keyboard navigation
-'random'      -- randomized navigation
+Currently there is one preset configurations available:
+'main' -- keyboard navigation with randomized attract mode.
 
-Both of these configurations extend 'defaults'.
+To render videos (see below):
 
+    mandulia configuration |
+    ppmtoy4m -S444 -F25:1 |
+    ffmpeg2theora -v 8 -f yuv4mpegpipe -o mandulia.ogv -
 
+
 Controls
 --------
 
-'interactive' defines the following key bindings:
+'main' defines the following key bindings:
 
 Escape   -- quit
+F11      -- toggle full screen
+a        -- enter attract mode immediately
+r        -- record for 10 minutes (assuming 25fps)
+s        -- record for 1 frame (aka screenshot)
+
+Note that recording is via PPM images on 'stdout', so to avoid flooding
+your terminal with binary data be sure to redirect it somewhere else.
+
+As a side-effect, the following count as 'activity':
+
 PageUp   -- zoom in
 PageDown -- zoom out
 cursors  -- move around
@@ -31,7 +44,12 @@
 Home     -- return to base
 [ ]      -- change speed
 #        -- reset speed
+{ }      -- change weight
+~        -- reset weight
 
+Attract mode is entered after 1 minute (assuming 25fps) of inactivity,
+and left on any activity.
+
 Other configurations may differ, check their sources for details.
 
 
@@ -41,14 +59,6 @@
 Mandulia can be reconfigured without recompilation, using Lua scripts.
 The first (non RTS) argument to 'mandulia' will be executed on startup.
 First it will try to load it as a file, then via Lua's 'require()'.
-See 'defaults.lua' to see the available interface, and 'interactive.lua'
-for an example of keyboard navigation.  Note that some variables only
-take effect on startup, and later modification will be ignored.
-
-
-Profiling
----------
-
-If you installed an executable with profiling, try (for example):
-
-mandulia +RTS -s -P -V0.001 -H512M -M2G -RTS
+See 'defaults.lua' to see the available interface, and 'main.lua' for an
+example of keyboard navigation.  Note that some variables only take
+effect on startup, and later modification will be ignored.
diff --git a/config/defaults.lua b/config/defaults.lua
--- a/config/defaults.lua
+++ b/config/defaults.lua
@@ -32,7 +32,11 @@
   m.height      =  576        -- (*) window height
   m.fps         =   25        --     frames per second
   m.detail      =   11        -- (*) detail level
-  m.displaysize = m.height/11 --     display size of each Julia set
+  if m.width > m.height then  --     display size of each Julia set
+    m.displaysize = m.width / 16
+  else
+    m.displaysize = m.height / 16
+  end
   m.juliasize   =  256        -- (*) pixel size of each Julia set
   m.jobs        =  512        -- (*) maximum number of jobs per frame
   m.images      =  512        -- (*) maximum number of images to cache
@@ -44,7 +48,11 @@
   function m.reshape(width, height)
     m.width  = width
     m.height = height
-    m.displaysize = height / 11
+    if width > height then
+      m.displaysize = width / 16
+    else
+      m.displaysize = height / 16
+    end
   end
 
   -- gets called at the start of each frame
diff --git a/config/distance.lua b/config/distance.lua
new file mode 100644
--- /dev/null
+++ b/config/distance.lua
@@ -0,0 +1,82 @@
+--[[--
+
+Mandulia -- Mandelbrot/Julia explorer
+Copyright (C) 2010  Claude Heiland-Allen <claudiusmaximus@goto10.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+--]]--
+
+do
+
+  --[[--
+
+  SYNOPSIS
+
+    s = exteriordistance(x, y, n, r)
+
+  INPUTS
+
+    x, y  :  point in the complex plane
+    n     :  iteration limit
+    r     :  escape radius
+
+  OUTPUTS
+
+    s     :  distance from Mandelbrot set (or nil if inside)
+
+  EXAMPLE
+
+    s = exteriordistance(x, y, 100, 100)
+    if s ~= nil and s < 0.01 then ... else ... end
+
+  --]]--
+  function exteriordistance(x, y, n, r)
+
+    local px = 0
+    local py = 0
+    local cx = x
+    local cy = y
+    local dx = 1
+    local dy = 0
+    local r2 = r * r
+    local escape = false
+    local px1, py1, dx1, dy1, px2, py2, pxy, d2, p, d
+
+    for i = 1,n do
+      -- p_n+1 := p_n * p_n + c
+      -- d_n+1 := 2 * p_n * d_n + 1
+      px2 = px * px
+      py2 = py * py
+      d2 = px2 + py2
+      if d2 > r2 then escape = true ; break end
+      pxy = px * py
+      px1 = px2 - py2 + cx
+      py1 = 2 * pxy + cy
+      dx1 = 2 * (px * dx - py * dy) + 1
+      dy1 = 2 * (dx * py + dy * px)
+      px = px1 ; py = py1 ; dx = dx1 ; dy = dy1
+    end
+
+    if escape then
+      p = math.sqrt(px * px + py * py)
+      d = math.sqrt(dx * dx + dy * dy)
+      return 2 * p * math.log(p) / d
+    else
+      return nil
+    end
+
+  end
+
+end
diff --git a/config/interactive.lua b/config/interactive.lua
deleted file mode 100644
--- a/config/interactive.lua
+++ /dev/null
@@ -1,57 +0,0 @@
---[[--
-
-Mandulia -- Mandelbrot/Julia explorer
-Copyright (C) 2010  Claude Heiland-Allen <claudiusmaximus@goto10.org>
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
---]]--
-
-require("defaults")
-
-do
-
-  local delta = { x = 0, y = 0, z = 0 }
-  local speed = 0.01
-  
-  local phi  = (math.sqrt(5) + 1) / 2
-  local phi1 = (math.sqrt(5) - 1) / 2
-  
-  local keys =
-    { Escape   = function() mandulia.quit() end
-    , F11      = function() mandulia.fullscreen = not mandulia.fullscreen end
-    , Right    = function() delta.x = delta.x + 1 end
-    , Left     = function() delta.x = delta.x - 1 end
-    , Up       = function() delta.y = delta.y + 1 end
-    , Down     = function() delta.y = delta.y - 1 end
-    , PageUp   = function() delta.z = delta.z + 1 end
-    , PageDown = function() delta.z = delta.z - 1 end
-    , End      = function() delta         = { x = 0, y = 0, z = 0 } end
-    , Home     = function() mandulia.view = { x = 0, y = 0, z = 0 } end
-    , ["["]    = function() speed = speed * 0.95 end
-    , ["]"]    = function() speed = speed / 0.95 end
-    , ["#"]    = function() speed = 0.01 end
-    }
-  
-  function mandulia.render()
-    mandulia.view.x = mandulia.view.x + speed * delta.x * phi1 ^ mandulia.view.z
-    mandulia.view.y = mandulia.view.y + speed * delta.y * phi1 ^ mandulia.view.z
-    mandulia.view.z = mandulia.view.z + speed * delta.z 
-  end
-  
-  function mandulia.keyboard(key)
-    if type(keys[key]) == "function" then keys[key]() end
-  end
-  
-end
diff --git a/config/main.lua b/config/main.lua
new file mode 100644
--- /dev/null
+++ b/config/main.lua
@@ -0,0 +1,244 @@
+--[[--
+
+Mandulia -- Mandelbrot/Julia explorer
+Copyright (C) 2010  Claude Heiland-Allen <claudiusmaximus@goto10.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+--]]--
+
+require("defaults")
+require("transition")
+require("distance")
+
+do
+
+  -- frame count without interaction
+  local mode = "interact"
+  local unattended = 0
+  local attentionSpan = 25 * 60
+
+  -- frame counts for recording
+  local frames = 0
+  local frameLimit = 0
+
+  -- magic numbers
+  math.randomseed(os.time())
+  local phi  = (math.sqrt(5) + 1) / 2
+  local phi1 = (math.sqrt(5) - 1) / 2
+
+  -- animation parameters
+  local defaultSpeed = 0.125
+  local defaultWeight = 8
+  local speed = defaultSpeed
+  local weight = defaultWeight
+
+  -- view bounds
+  local minimum = { x = -4, y = -4, z =  0 }
+  local maximum = { x =  4, y =  4, z = 42 }
+  local function clamp(x, mi, ma)
+    return math.min(math.max(x, mi), ma)
+  end
+
+  -- attract mode for interesting behaviour when unattended
+  local attract = { }
+  do
+
+    local function walk(x, mi, ma, d)
+      local p = (x - mi) / (ma - mi)
+      local s = math.random()
+      return x + d * (s - p)
+    end
+
+    -- default view
+    local function zero() return { x = 0, y = 0, z = minimum.z } end
+
+    -- animation variables
+    local source = zero()
+    local target = zero()
+    local t = 0
+    local s = 0
+    local f = function(t) return zero() end
+    local dz = minimum.z
+
+    local history = { }
+    for i = 1,48 do
+      history[i] = { x = 0, y = 0 }
+    end
+    local historyptr = 1
+
+    attract.enter = function()
+      mode = "attract"
+      target.x = mandulia.view.x
+      target.y = mandulia.view.y
+      target.z = mandulia.view.z
+      t = 0
+      s = 0
+    end
+
+    attract.render = function()
+      t = t + speed * (phi1 ^ dz)
+      if t > s then -- we reached the target, new target needed
+        t = t - s
+        source.x = target.x
+        source.y = target.y
+        source.z = target.z
+        local interesting
+        local x, y, e, v, d2, dx, dy
+        local r = 25 * phi1 ^ dz
+        e = exteriordistance(source.x, source.y, 1000, 1000)
+        interesting = e ~= nil and e < 0.00001
+        if interesting then
+          interesting = false
+          while not interesting do
+            x = source.x + r * (2 * math.random() - 1)
+            y = source.y + r * (2 * math.random() - 1)
+            e = exteriordistance(x, y, 1000, 1000)
+            if e ~= nil and e < 0.00001 then
+              v = false
+              for i,p in ipairs(history) do
+                dx = x - p.x
+                dy = y - p.y
+                d2 = dx * dx + dy * dy
+                v = v or d2 < 0.01
+              end
+              if v then r = r * 1.1 end
+              interesting = not v
+            end
+          end
+        else
+          interesting = false
+          while not interesting do
+            x = math.random() * (maximum.x - minimum.x) + minimum.x
+            y = math.random() * (maximum.y - minimum.y) + minimum.y
+            e = exteriordistance(x, y, 1000, 1000)
+            interesting = e ~= nil and e < 0.00001
+          end
+        end
+        target.x = clamp(x, minimum.x, maximum.x)
+        target.y = clamp(y, minimum.y, maximum.y)
+        target.z = walk(source.z, minimum.z, maximum.z, 3)
+        history[historyptr].x = target.x
+        history[historyptr].y = target.y
+        historyptr = historyptr + 1
+        if historyptr > #history then historyptr = 1 end
+        s, f = transition(source, target, weight, phi)
+      end
+      mandulia.view = f(t)
+      mandulia.view.z = clamp(mandulia.view.z, minimum.z, maximum.z)
+      dz = mandulia.view.z
+    end -- render
+
+  end -- attract
+
+  -- interact mode for engagement and control
+  local interact = { }
+  do
+
+    local delta = { x = 0, y = 0, z = 0 }
+
+    interact.enter = function()
+      mode = "interact"
+      unattended = 0
+    end
+
+    interact.render = function()
+      local x = mandulia.view.x + speed * delta.x * phi1 ^ mandulia.view.z
+      local y = mandulia.view.y + speed * delta.y * phi1 ^ mandulia.view.z
+      local z = mandulia.view.z + speed * delta.z
+      mandulia.view.x = clamp(x, minimum.x, maximum.x)
+      mandulia.view.y = clamp(y, minimum.y, maximum.y)
+      mandulia.view.z = clamp(z, minimum.z, maximum.z)
+    end
+
+    local keys =
+      { Right    = function() delta.x = delta.x + 1 end
+      , Left     = function() delta.x = delta.x - 1 end
+      , Up       = function() delta.y = delta.y + 1 end
+      , Down     = function() delta.y = delta.y - 1 end
+      , PageUp   = function() delta.z = delta.z + 1 end
+      , PageDown = function() delta.z = delta.z - 1 end
+      , End      = function() delta         = { x = 0, y = 0, z = 0 } end
+      , Home     = function() delta         = { x = 0, y = 0, z = 0 }
+                              mandulia.view = { x = 0, y = 0, z = 0 } end
+      , ["["]    = function() speed = speed * 0.95 end
+      , ["]"]    = function() speed = speed / 0.95 end
+      , ["#"]    = function() speed = defaultSpeed end
+      , ["{"]    = function() weight = weight * 0.95 end
+      , ["}"]    = function() weight = weight / 0.95 end
+      , ["~"]    = function() weight = defaultWeight end
+      }
+
+    interact.keyboard = function(key)
+      if type(keys[key]) == "function" then keys[key]() end
+    end
+
+  end -- interact
+
+  local screenshot = { }
+  local record = { }
+  do
+
+    local recording = false
+
+    screenshot.enter = function()
+      frames = 0
+      frameLimit = 1
+      frameStep = 1
+      recording = true
+    end
+
+    record.enter = function()
+      frames = 0
+      frameLimit = 25 * 60 * 10
+      frameStep = 1
+      recording = true
+    end
+
+    record.render = function()
+      if recording then
+        mandulia.record = (frames % frameStep) == 0 and frames < frameLimit
+        frames = frames + 1
+      end
+    end
+
+  end -- screenshot/record
+
+
+  function mandulia.render()
+    if mode == "interact" then
+      interact.render()
+      unattended = unattended + 1
+      if unattended >= attentionSpan then attract.enter() end
+    elseif mode == "attract" then
+      attract.render()
+    end
+    record.render()
+  end
+
+  function mandulia.keyboard(key)
+    if     key == "Escape" then mandulia.quit()
+    elseif key == "F11"    then mandulia.fullscreen = not mandulia.fullscreen
+    elseif key == "a"      then attract.enter()
+    elseif key == "r"      then record.enter()
+    elseif key == "s"      then screenshot.enter()
+    else
+      interact.enter()
+      interact.keyboard(key)
+    end
+  end
+
+end
+
+pcall(function() require("config") end)
diff --git a/config/random.lua b/config/random.lua
deleted file mode 100644
--- a/config/random.lua
+++ /dev/null
@@ -1,104 +0,0 @@
---[[--
-
-Mandulia -- Mandelbrot/Julia explorer
-Copyright (C) 2010  Claude Heiland-Allen <claudiusmaximus@goto10.org>
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
---]]--
-
-require("defaults")
-require("transition")
-
-do
-
-  --[[-- generate random views and transition between them --]]--
-
-  -- randomness
-  math.randomseed(os.time())
-
-  -- for time-limited recording
-  local frameLimit = 10 * 60 * 25
-  local frames = 0
-
-  -- magic numbers
-  local phi  = (math.sqrt(5) + 1) / 2
-  local phi1 = (math.sqrt(5) - 1) / 2
-
-  -- animation parameters
-  local defaultSpeed = 0.25
-  local defaultWeight = 25
-
-  -- view bounds
-  local minimum = { x = -1.5, y = -1.1, z =  1 }
-  local maximum = { x =  0.5, y =  1.1, z = 32 }
-
-  -- default view
-  local function zero() return { x = 0, y = 0, z = minimum.z } end
-
-  -- animation variables
-  local source = zero()
-  local target = zero()
-  local t = 0
-  local s = 0
-  local f = function(t) return zero() end
-  local dz = minimum.z
-  local speed = defaultSpeed
-  local weight = defaultWeight
-
-  -- keyboard bindings
-  local keys =
-    { Escape   = function() mandulia.quit() end
-    , F11      = function() mandulia.fullscreen = not mandulia.fullscreen end
-    , Home     = function()
-                   source = f(t) ; target = zero() ; t = 0
-                   s, f = transition(source, target, weight, phi)
-                 end
-    , PageUp   = function() weight = weight * 0.95 end
-    , PageDown = function() weight = weight / 0.95 end
-    , End      = function() weight = defaultWeight end
-    , ["["]    = function() speed = speed * 0.95 end
-    , ["]"]    = function() speed = speed / 0.95 end
-    , ["#"]    = function() speed = defaultSpeed end
-    }
-  function mandulia.keyboard(key)
-    if type(keys[key]) == "function" then keys[key]() end
-  end
-
-  -- every frame
-  function mandulia.render()
-    if mandulia.record then
-      if frames == frameLimit then
-        mandulia.quit()
-      else
-        frames = frames + 1
-      end
-    end
-    t = t + speed * (phi1 ^ dz)
-    if t > s then -- we reached the target, new target needed
-      t = t - s
-      source.x = target.x
-      source.y = target.y
-      source.z = target.z
-      target.x = math.random() * (maximum.x - minimum.x) + minimum.x
-      target.y = math.random() * (maximum.y - minimum.y) + minimum.y
-      target.z = math.random() * (maximum.z - minimum.z) + minimum.z
-      s, f = transition(source, target, weight, phi)
-    end
-    mandulia.view = f(t)
-    mandulia.view.z = math.max(mandulia.view.z, minimum.z)
-    dz = mandulia.view.z
-  end
-
-end
diff --git a/mandulia.cabal b/mandulia.cabal
--- a/mandulia.cabal
+++ b/mandulia.cabal
@@ -1,10 +1,14 @@
 Name:                mandulia
-Version:             0.4
+Version:             0.5
 Stability:           Provisional
 Synopsis:            A zooming visualisation of the Mandelbrot Set as many Julia Sets.
 Description:         Mandulia provides a zooming visualisation of the Mandelbrot Set
                      as many Julia Sets.  Featuring a profiled and optimized renderer,
                      and a Lua configuration and scripting interface.
+                     .
+                     By default Mandulia compiles using SSE4 instructions.  If your
+                     CPU does not support SSE4, you should add "-f-SSE4" to your
+                     cabal-install command line.
 Homepage:            http://gitorious.org/maximus/mandulia
 Cabal-version:       >=1.6
 License:             GPL-3
@@ -17,7 +21,7 @@
 Extra-source-files:  AUTHORS ChangeLog HACKING INSTALL NEWS README TODO
                      src/rjulia.h src/qsort.h
 Data-dir:            config
-Data-files:          defaults.lua interactive.lua random.lua transition.lua
+Data-files:          defaults.lua distance.lua main.lua transition.lua
 
 Flag SSE4
   Description:         Enable optimizations for SSE4 CPUs.
@@ -66,4 +70,4 @@
 Source-repository this
   type:                git
   location:            git://gitorious.org/maximus/mandulia.git
-  tag:                 v0.4
+  tag:                 v0.5
diff --git a/src/Bounds.hs b/src/Bounds.hs
--- a/src/Bounds.hs
+++ b/src/Bounds.hs
@@ -45,19 +45,6 @@
       V _ y _ = bottomLeft box
   in  V x y 1
 
-{-
-bounds :: [V] -> Bounds
-bounds [] = error "Bounds.bounds []"
-bounds vs =
-  let xs = map (\(V x _ _) -> x) vs
-      ys = map (\(V _ y _) -> y) vs
-      x0 = minimum xs
-      x1 = maximum xs
-      y0 = minimum ys
-      y1 = maximum ys
-  in  Bounds{ bottomLeft = V x0 y0 1, topRight = V x1 y1 1 }
--}
-
 bounds :: [V] -> Bounds
 bounds [] = error "Bounds.bounds []"
 bounds (V u v _ : vs) =
diff --git a/src/Interface.hs b/src/Interface.hs
--- a/src/Interface.hs
+++ b/src/Interface.hs
@@ -29,26 +29,30 @@
 
 import Paths_mandulia (getDataFileName)
 
+import Utils(modifyIORef')
+
 -- interface
 
 data Interface =
   Interface
-    { iLua         :: LuaState -- initialized in interface
-    , iQuit        :: Bool     -- can only be set by Lua 'quit'
+    { iLua         :: !LuaState -- initialized in interface
+    , iQuit        :: !Bool     -- can only be set by Lua 'quit'
     -- these are all updated from Lua globals in Haskell 'update'
-    , iRecord      :: Bool
-    , iWidth       :: Int
-    , iHeight      :: Int
-    , iFPS         :: Double
-    , iFullScreen  :: Bool
-    , iDetail      :: Double
-    , iDisplaySize :: Double
-    , iJuliaSize   :: Int
-    , iJobs        :: Int
-    , iImages      :: Int
-    , iTextures    :: Int
-    , iWorkers     :: Int
-    , iView        :: (Double, Double, Double)
+    , iRecord      :: !Bool
+    , iWidth       :: !Int
+    , iHeight      :: !Int
+    , iFPS         :: !Double
+    , iFullScreen  :: !Bool
+    , iDetail      :: !Double
+    , iDisplaySize :: !Double
+    , iJuliaSize   :: !Int
+    , iJobs        :: !Int
+    , iImages      :: !Int
+    , iTextures    :: !Int
+    , iWorkers     :: !Int
+    , iView        :: !(Double, Double, Double)
+    -- these are updated from the Haskell side, set in Lua in 'update'
+    , iStatistics  :: ![(String, (Double, Double, Double))]
     }
 
 -- constructor
@@ -72,6 +76,7 @@
     , iTextures = 2048
     , iWorkers = 2
     , iView = (0, 0, 0)
+    , iStatistics = []
     }
   Lua.openlibs l
   path <- getDataFileName "?.lua"
@@ -122,7 +127,7 @@
   l <- iLua `fmap` readIORef iR
   Lua.close l
 
--- update from Lua
+-- update from Lua   FIXME this should copy iStatistics to Lua-land
 
 update :: IORef Interface -> IO ()
 update iR = do
@@ -147,7 +152,7 @@
   viewX       <- g "view.x"
   viewY       <- g "view.y"
   viewZ       <- g "view.z"
-  modifyIORef iR $ \i ->
+  modifyIORef' iR $ \i ->
     let f k v = case v of
                   Nothing -> k i
                   Just x  -> x
@@ -171,7 +176,7 @@
 -- callbacks from Lua
 
 quit :: IORef Interface -> IO ()
-quit iR = modifyIORef iR $ \i -> i{ iQuit = True }
+quit iR = modifyIORef' iR $ \i -> i{ iQuit = True }
 
 -- callbacks into Lua
 
diff --git a/src/Julia.hs b/src/Julia.hs
--- a/src/Julia.hs
+++ b/src/Julia.hs
@@ -84,7 +84,7 @@
   forever $ do
     i <- acquire is
     withJob js $ \j -> do
-      (dtC,_) <- time $ c_julia c (iBuffer i) (realToFrac . jCX . jCoords $ j) (realToFrac . jCY . jCoords $ j)
+      (dtC,()) <- time $ c_julia c (iBuffer i) (realToFrac . jCX . jCoords $ j) (realToFrac . jCY . jCoords $ j)
       logStats "compute" dtC
       return j
         { jDoneAction = Just . Right $ (jCoords j, 
diff --git a/src/Mandulia.hs b/src/Mandulia.hs
--- a/src/Mandulia.hs
+++ b/src/Mandulia.hs
@@ -31,7 +31,6 @@
 import Graphics.UI.GLUT hiding (scale, translate, fullScreen)
 import qualified Graphics.UI.GLUT as G
 import Unsafe.Coerce (unsafeCoerce)
---import Numeric (showFFloat)
 
 import Paths_mandulia (version)
 
@@ -52,7 +51,7 @@
 
 data Mandulia =
   Mandulia
-    { tiling     :: AmmannA3
+    { tiling     :: Maybe AmmannA3
     , viewMax    :: Bounds
     , view       :: Bounds
     , width      :: Int
@@ -131,16 +130,14 @@
       winH   = 1 `max` I.iHeight i
       full   = I.iFullScreen i
       mspf   = 1 `max` (ceiling $ 1000 / (iFPS i `max` 0.01))
-      detail = 1 `max` (ceiling $ iDetail i)
       view0  = mkView winW winH 0 0 0
-      tiling0 = iterate stepIn (ammannA3 view0) !! detail
   (logStats', getStats') <- statsLogger
   jobq      <- jobQueue
   imgpool   <- resourcePool (image jsize jsize 4) imagen
   let texcache = textureCache texn
   wtids     <- replicateM workn
     (forkIO $ juliaWorker logStats' jsize jsize imgpool jobq)
-  manduliaR <- newIORef Mandulia { tiling = tiling0
+  manduliaR <- newIORef Mandulia { tiling = Nothing
                                  , viewMax = view0
                                  , view = view0
                                  , width = winW
@@ -183,24 +180,15 @@
 quit mR = do
   m <- readIORef mR
   mapM_ killThread (workers m)
---  hPutStrLn stderr "what\tcount\tmean\tstddev"
---  mapM_ (hPutStrLn stderr . uncurry printStat) =<< getStats m
   I.atexit (iface m)
   closeInterface (iface m)
   exitSuccess
 
-{-
-printStat :: String -> (Double, Double, Double) -> String
-printStat name (n, m, d) =
-  name ++ "\t" ++
-   show (floor n :: Integer) ++ "\t" ++
-    showFFloat (Just 8) m "" ++ "\t" ++
-     showFFloat (Just 8) d ""
--}
-
 update :: IORef Mandulia -> IO (Julia -> Double)
 update mR = do
   m0 <- readIORef mR
+  s <- getStats m0
+  modifyIORef' (iface m0) (\i -> i{ iStatistics = s })
   I.render (iface m0)
   i <- readIORef (iface m0)
   when (iQuit i) (quit mR)
@@ -210,8 +198,8 @@
       v = mkView (width m) (height m) x y z
   if v `insideOrEqual` viewMax m
    then do
-    case zoomTo v (tiling m) of
-      Just t  -> do
+    case zoomTo v (ammannA3 $ viewMax m) of
+      t@(Just _) -> do
         writeIORef mR m{ tiling = t, view = v }
         return $ score z x y
       Nothing -> return $ score z x y -- FIXME should never happen?
@@ -235,12 +223,14 @@
   i <- readIORef (iface m)
   let w = fromIntegral (width m)
       h = fromIntegral (height m)
-      window = bounds [ V 0 0 1, V w h 1]
+      s = iDisplaySize i
+      window = bounds [ V (-s) (-s) 1, V (w+s) (h+s) 1]
       viewT  = view m `into` window
-      ctiles = filter ((==) C . tTile) . tiles 0 . tiling $ m
+      ctiles = case tiling m of
+        Just t -> filter ((==) C . tTile) . tiles (ceiling d) $ t
+        Nothing -> []
       d = iDetail i
       (_, _, z) = iView i
-      s = iDisplaySize i
       rads = map ((s *) . radius d z) [0 ..]
   return $ partitionEithers
         [ case mt of
@@ -308,12 +298,12 @@
   reprioritise (jobs m) (const js')
   return $ filter visible qs
 
-color0 :: Color4 GLclampf
-color0 = Color4 1 (0.5 * cos 1 + 0.5) (0.5 * cos 2 + 0.5) 1
+black :: Color4 GLclampf
+black = Color4 0 0 0 1
 
 drawQuads :: [Quad] -> IO ()
 drawQuads qs = do
-  clearColor $= color0
+  clearColor $= black
   clear [ColorBuffer]
   blend $= Enabled
   blendFunc $= (SrcAlpha, OneMinusSrcAlpha)
@@ -355,12 +345,12 @@
   I.reshape (iface m) (fromIntegral w) (fromIntegral h)
   i <- readIORef (iface m)
   let (x, y, z) = iView i
-      s = ceiling $ iDisplaySize i
+      s = ceiling $ 2 * iDisplaySize i
   modifyIORef mR $ \m' -> m'{ view = mkView (width m') (height m') x y z }
   viewport $= (Position (-s) (-s), (Size (w + 2 * fromIntegral s) (h + 2 * fromIntegral s)))
   matrixMode $= Projection
   loadIdentity
-  ortho 0 (fromIntegral w) 0 (fromIntegral h) (-1) 1
+  ortho (-fromIntegral s) (fromIntegral w + fromIntegral s) (-fromIntegral s) (fromIntegral h + fromIntegral s) (-1) 1
   matrixMode $= Modelview 0
   loadIdentity
   postRedisplay Nothing
diff --git a/src/StatsLogger.hs b/src/StatsLogger.hs
--- a/src/StatsLogger.hs
+++ b/src/StatsLogger.hs
@@ -54,7 +54,7 @@
 statsLogger :: IO (Logger, Statistics)
 statsLogger = do
   v <- newMVar empty
-  let logStats name value = modifyMVar_ v $ return . insertWith' combine name (stat value)
+  let logStats name value = modifyMVar_ v $ \m -> return $! (insertWith' combine name (stat value) $! m)
       getStats = readMVar v >>= return . toAscList . map stats
   return (logStats, getStats)
 
diff --git a/src/Utils.hs b/src/Utils.hs
--- a/src/Utils.hs
+++ b/src/Utils.hs
@@ -16,24 +16,9 @@
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 -}
 
-module Utils (phi, phi', clamp) where
-
-{-
-import Data.List (sortBy)
-import Data.Ord (comparing)
-
-first :: (a -> c) -> (a, b) -> (c, b)
-first f ~(x,y) = (f x, y)
-
-second :: (b -> c) -> (a, b) -> (a, c)
-second f ~(x,y) = (x, f y)
-
-second' :: (b -> c) -> (a, b) -> (a, c)
-second' f ~(x,y) = let fy = f y in fy `seq` (x, fy)
+module Utils (phi, phi', clamp, modifyIORef') where
 
-sortOn :: Ord o => (a -> o) -> [a] -> [a]
-sortOn p = map fst . sortBy (comparing snd) . map (\x -> let px = p x in px `seq` (x, px))
--}
+import Data.IORef(IORef(), readIORef, writeIORef)
 
 phi  :: Double
 phi  = (sqrt 5 + 1) / 2
@@ -43,3 +28,6 @@
 
 clamp :: Ord r => r -> r -> r -> r
 clamp mi ma x = ma `min` x `max` mi
+
+modifyIORef' :: IORef a -> (a -> a) -> IO ()
+modifyIORef' r m = readIORef r >>= \v -> writeIORef r $! m v
diff --git a/src/Vector.hs b/src/Vector.hs
--- a/src/Vector.hs
+++ b/src/Vector.hs
@@ -60,34 +60,16 @@
 (^-^) :: V -> V -> V
 (V a b c)  ^-^  (V x y z) = V (a-x) (b-y) (c-z)
 
-{-
 dot :: V -> V -> R
-(V a b c) `dot` (V x y z) = sumV $ V (a*x) (b*y) (c*z)
--}
-
-dot :: V -> V -> R
 (V a b c) `dot` (V x y z) = a*x + b*y + c*z
 
-{-
 (^^*^) :: M -> V -> V
-(M a b c) ^^*^  v = V (a`dot`v) (b`dot`v) (c`dot`v)
--}
-
-(^^*^) :: M -> V -> V
 (M (V a b c) (V d e f) (V g h i)) ^^*^ (V r u x) =
   V (a * r + b * u + c * x)
     (d * r + e * u + f * x)
     (g * r + h * u + i * x)
 
-{-
 (^^*^^) :: M -> M -> M
-(M a b c) ^^*^^ m = let M x y z = transposeM m
-                    in M (V (a`dot`x) (a`dot`y) (a`dot`z))
-                         (V (b`dot`x) (b`dot`y) (b`dot`z))
-                         (V (c`dot`x) (c`dot`y) (c`dot`z))
--}
-
-(^^*^^) :: M -> M -> M
 (M (V a b c) (V d e f) (V g h i)) ^^*^^ (M (V r s t) (V u v w) (V x y z)) =
   M (V (a * r + b * u + c * x) (a * s + b * v + c * y) (a * t + b * w + c * z))
     (V (d * r + e * u + f * x) (d * s + e * v + f * y) (d * t + e * w + f * z))
@@ -95,26 +77,3 @@
 
 (^|-|^) :: V -> V -> R
 u ^|-|^ v = let d = u ^-^ v in sqrt $ d `dot` d
-
-{-
-import Numeric (showFFloat)
-
-sumV :: V -> R
-sumV (V a b c) = a + b + c
-
-identity :: M
-identity = M (V 1 0 0) (V 0 1 0) (V 0 0 1)
-
-transposeM :: M -> M
-transposeM (M (V a b c) (V d e f) (V g h i)) =
-            M (V a d g) (V b e h) (V c f i)
-
-prettyR :: R -> String
-prettyR x = (if x < 0 then id else ('+':)) $ showFFloat (Just 6) x ""
-
-prettyV :: V -> String
-prettyV (V a b c) = unwords . map prettyR $ [a,b,c]
-
-prettyM :: M -> String
-prettyM (M a b c) = unlines . map prettyV $ [a,b,c]
--}
diff --git a/src/rjulia.c b/src/rjulia.c
--- a/src/rjulia.c
+++ b/src/rjulia.c
@@ -16,12 +16,8 @@
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-#include <errno.h>
-#include <limits.h>
 #include <math.h>
-#include <stdio.h>
 #include <stdlib.h>
-#include <string.h>
 
 #include "rjulia.h"
 
@@ -99,11 +95,35 @@
   return x > y ? x : y;
 }
 
-static inline int colour(R a) {
-  int channel = 128 * cos(a) + 128;
-  return min(max(channel, 0), 255);
+static inline void colour(R v, unsigned char *r, unsigned char *g, unsigned char *b) {
+  int rr = 128 * cos(sqrt5 * v) + 128;
+  int gg = 128 * cos(sqrt6 * v) + 128;
+  int bb = 128 * cos(sqrt7 * v) + 128;
+  *r = min(max(rr, 0), 255);
+  *g = min(max(gg, 0), 255);
+  *b = min(max(bb, 0), 255);
 }
 
+static inline double mandelbrot1(double x, double y) {
+  R px = 0, py = 0, cx = x, cy = y, px1, py1, px2, py2, pxy, d2;
+  for (int i = 0; i < 16384; ++i) {
+    // p_n+1 := p_n * p_n + c
+    // d_n+1 := 2 * p_n * d_n + 1
+    px2 = px * px;
+    py2 = py * py;
+    d2 = px2 + py2;
+    if (d2 > escapeRadius2) {
+      return i - log2(log2(d2)/log2(escapeRadius2));
+    }
+    pxy = px * py;
+    px1 = px2 - py2 + cx;
+    py1 = 2 * pxy + cy;
+    px = px1;
+    py = py1;
+  }
+  return -1;
+}
+
 void julia(struct context *ctx, unsigned char *image, double dcx, double dcy) {
   if (! ctx || ! image) {
     return;
@@ -114,10 +134,11 @@
   const int stride = width * channels;
   const R cx = dcx;
   const R cy = dcy;
-  const R hw = height * 3.0 / width;
-  const R wh = width  * 3.0 / height;
-  const R sx = width < height ? 3 : wh;
-  const R sy = width < height ? hw : 3;
+  const R k = 4;
+  const R hw = height * k / width;
+  const R wh = width  * k / height;
+  const R sx = width < height ? k : wh;
+  const R sy = width < height ? hw : k;
   const int ntotal = width * height;
   int npoints;
   { // initialize
@@ -141,6 +162,8 @@
   int ndone = 0;
   int n = 0;
   int escapes;
+  R u = mandelbrot1(dcx, dcy);
+  u = u > 0 ? u : 0;
   do {
     escapes = 0;
     { // iterate
@@ -159,11 +182,10 @@
             R d = sqrt(zd2);
             // colourify pixel
             unsigned char *pixel = p->pixel;
-            R v = 0.025 * ((n+e) - log2(log2(d)/log2(escapeRadius)));
+            R v = (n+e) - log2(log2(d)/log2(escapeRadius));
+            v /= 32;
+            colour(u + v, &pixel[0], &pixel[1], &pixel[2]);
             R a = fmin(fmax(8 * (v - 0.1) * v, 0), 1);
-            pixel[0] = colour(sqrt5 * v    );
-            pixel[1] = colour(sqrt6 * v + 1);
-            pixel[2] = colour(sqrt7 * v + 2);
             pixel[3] = a * 255;
             p->pixel = 0;
             ++escapes;
diff --git a/src/rjulia.h b/src/rjulia.h
--- a/src/rjulia.h
+++ b/src/rjulia.h
@@ -16,8 +16,8 @@
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-#ifndef JULIA_H
-#define JULIA_H 1
+#ifndef RJULIA_H
+#define RJULIA_H 1
 
 struct context;
 
diff --git a/src/rts.c b/src/rts.c
--- a/src/rts.c
+++ b/src/rts.c
@@ -16,4 +16,4 @@
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-char *ghc_rts_opts = "-M1G"; /* limit heap size to something sane */
+char *ghc_rts_opts = "-M50M"; /* we can run in constant space */
