diff --git a/test/tutorial/tut03.cpp b/test/tutorial/tut03.cpp
index 4d1aa58d..0de4eb34 100644
--- a/test/tutorial/tut03.cpp
+++ b/test/tutorial/tut03.cpp
@@ -38,78 +38,117 @@ public:
         m_size = size;
         m_pixels = new u8vec4[size.x * size.y];
         m_frame = -1;
-        m_center = 0;
-        //m_target = f64cmplx(0.001643721971153, 0.822467633298876);
-        m_target = f64cmplx(-1.207205434596, 0.315432814901);
-        //m_target = f64cmplx(-0.79192956889854, -0.14632423080102);
-        //m_target = f64cmplx(0.3245046418497685, 0.04855101129280834);
-        //m_target = f64cmplx(0.28693186889504513, 0.014286693904085048);
-        m_angle = 0.0;
-        m_radius = 8.0;
+        m_dirty = 8;
+        m_center = -0.75;
+        //f64cmplx(0.001643721971153, 0.822467633298876);
+        //f64cmplx(-1.207205434596, 0.315432814901);
+        //f64cmplx(-0.79192956889854, -0.14632423080102);
+        //f64cmplx(0.3245046418497685, 0.04855101129280834);
+        //f64cmplx(0.28693186889504513, 0.014286693904085048);
+        m_radius = 1.5;
+        m_screenradius = 0.5 * (m_size.x < m_size.y ? m_size.x : m_size.y);
         m_ready = false;
+
+        m_centertext = new Text(NULL, "gfx/font/ascii.png");
+        m_centertext->SetPos(ivec3(5, m_size.y - 15, 1));
+        Ticker::Ref(m_centertext);
+
+        m_mousetext = new Text(NULL, "gfx/font/ascii.png");
+        m_mousetext->SetPos(ivec3(5, m_size.y - 29, 1));
+        Ticker::Ref(m_mousetext);
+
+        position = ivec3(0, 0, 0);
+        bbox[0] = position;
+        bbox[1] = ivec3(size, 0);
+        Input::TrackMouse(this);
     }
 
     ~Fractal()
     {
+        Input::UntrackMouse(this);
+        Ticker::Unref(m_centertext);
+        Ticker::Unref(m_mousetext);
         delete m_pixels;
     }
 
+    inline f64cmplx ScreenToWorldOffset(ivec2 pixel)
+    {
+        f64cmplx tmp = f64cmplx(pixel.x - m_size.x / 2,
+                                m_size.y / 2 - pixel.y);
+        return tmp * (m_radius / m_screenradius);
+    }
+
     virtual void TickGame(float deltams)
     {
         WorldEntity::TickGame(deltams);
 
         m_frame = (m_frame + 1) % 4;
 
-        double zoom = pow(2.0, -deltams * 0.0005);
-        m_radius *= zoom;
-        m_center = (m_center - m_target) * zoom * zoom + m_target;
+        f64cmplx worldmouse = m_center + ScreenToWorldOffset(mousepos);
 
-        double step = m_radius / (m_size.x > m_size.y ? m_size.x : m_size.y);
-//        m_angle -= deltams * 0.00015;
-        f64cmplx transform = step * f64cmplx(cos(m_angle), sin(m_angle));
+        ivec3 buttons = Input::GetMouseButtons();
+        if ((buttons[0] || buttons[2]) && mousepos.x != -1)
+        {
+            double zoom = pow(2.0, (buttons[0] ? -deltams : deltams) * 0.0015);
+            if (m_radius * zoom > 1.5)
+                zoom = 1.0;
+            m_radius *= zoom;
+            m_center = (m_center - worldmouse) * zoom + worldmouse;
+            worldmouse = m_center + ScreenToWorldOffset(mousepos);
+            m_dirty = 8;
+        }
+
+        char buf[128];
+        sprintf(buf, "center: %+13.11f%+13.11fi", m_center.x, m_center.y);
+        m_centertext->SetText(buf);
+        sprintf(buf, " mouse: %+13.11f%+13.11fi", worldmouse.x, worldmouse.y);
+        m_mousetext->SetText(buf);
 
         u8vec4 *m_pixelstart = m_pixels + m_size.x * m_size.y / 4 * m_frame;
 
-        for (int j = ((m_frame + 1) % 4) / 2; j < m_size.y; j += 2)
-        for (int i = m_frame % 2; i < m_size.x; i += 2)
+        if (m_dirty)
         {
-            double const maxlen = 32;
-            int const maxiter = 170;
-
-            f64cmplx delta(i - m_size.x / 2, j - m_size.y / 2);
-
-            f64cmplx z0 = m_center + transform * delta;
-            f64cmplx r0 = z0;
-            //f64cmplx r0(0.28693186889504513, 0.014286693904085048);
-            //f64cmplx r0(0.001643721971153, 0.822467633298876);
-            f64cmplx z;
-            int iter = maxiter;
-            for (z = z0; iter && z.sqlen() < maxlen * maxlen; z = z * z + r0)
-                --iter;
-
-            double f = iter;
-            double n = z.sqlen();
-
-            double k = log(n) * 0.5f / log(maxlen);
-            /* Approximate log2(k) in [1,2]. */
-            f += (- 0.344847817623168308695977510213252644185 * k
-                  + 2.024664188044341212602376988171727038739) * k
-                  - 1.674876738008591047163498125918330313237;
-
-            if (iter)
-            {
-                double r = 0.5 * sin(f * 0.27 - 2.0) + 0.5;
-                double g = 0.5 * sin(f * 0.13 + 1.0) + 0.5;
-                double b = 0.5 * sin(f * 0.21) + 0.5;
-
-                uint8_t red = r * 255.0f;
-                uint8_t green = g * 255.0f;
-                uint8_t blue = b * 255.0f;
-                *m_pixelstart++ = u8vec4(red, green, blue, 0);
-            }
-            else
+            m_dirty--;
+
+            for (int j = ((m_frame + 1) % 4) / 2; j < m_size.y; j += 2)
+            for (int i = m_frame % 2; i < m_size.x; i += 2)
             {
-                *m_pixelstart++ = u8vec4(0, 0, 0, 0);
+                double const maxlen = 32;
+                int const maxiter = 170;
+
+                f64cmplx z0 = m_center + ScreenToWorldOffset(ivec2(i, j));
+                f64cmplx r0 = z0;
+                //f64cmplx r0(0.28693186889504513, 0.014286693904085048);
+                //f64cmplx r0(0.001643721971153, 0.822467633298876);
+                f64cmplx z;
+                int iter = maxiter;
+                for (z = z0; iter && z.sqlen() < maxlen * maxlen; z = z * z + r0)
+                    --iter;
+
+                double f = iter;
+                double n = z.sqlen();
+
+                double k = log(n) * 0.5f / log(maxlen);
+                /* Approximate log2(k) in [1,2]. */
+                f += (- 0.344847817623168308695977510213252644185 * k
+                      + 2.024664188044341212602376988171727038739) * k
+                      - 1.674876738008591047163498125918330313237;
+
+                if (iter)
+                {
+                    double r = 0.5 * sin(f * 0.27 - 1.5) + 0.5;
+                    double g = 0.5 * sin(f * 0.13 + 1.3) + 0.5;
+                    double b = 0.5 * sin(f * 0.21 + 0.4) + 0.5;
+
+                    uint8_t red = r * 255.0f;
+                    uint8_t green = g * 255.0f;
+                    uint8_t blue = b * 255.0f;
+                    *m_pixelstart++ = u8vec4(red, green, blue, 0);
+                }
+                else
+                {
+                    *m_pixelstart++ = u8vec4(0, 0, 0, 0);
+                }
             }
         }
     }
@@ -130,12 +169,12 @@ public:
 
         static float const texcoords[] =
         {
-             1.0f,  0.0f,
-             0.0f,  0.0f,
-             0.0f,  1.0f,
-             0.0f,  1.0f,
              1.0f,  1.0f,
+             0.0f,  1.0f,
+             0.0f,  0.0f,
+             0.0f,  0.0f,
              1.0f,  0.0f,
+             1.0f,  1.0f,
         };
 
         if (!m_ready)
@@ -166,9 +205,11 @@ public:
                 /* gl_FragCoord is centered inside the pixel, so we remove
                  * 0.5 from gl_FragCoord.x. Also, (0,0) is at the bottom
                  * left whereas our images have (0,0) at the top left, so we
-                 * _add_ 0.5 to gl_FragCoord.y. */
+                 * _add_ 0.5 to gl_FragCoord.y. (XXX: this is no longer true
+                 * but will be again when mouse coordinates are back to
+                 * being top-left again). */
                 "    float i = mod(gl_FragCoord.x - 0.5, 2.0);"
-                "    float j = mod(gl_FragCoord.y + 0.5 + i, 2.0);"
+                "    float j = mod(gl_FragCoord.y - 0.5 + i, 2.0);"
                 "    coord.y += i + j * 2;"
                 "    coord.y *= 0.25;"
                 "    vec4 p = texture2D(in_Texture, coord);"
@@ -216,15 +257,21 @@ public:
 
         glEnable(GL_TEXTURE_2D);
         glBindTexture(GL_TEXTURE_2D, m_texid);
-        glTexSubImage2D(GL_TEXTURE_2D, 0,
-                        0, m_frame * m_size.y / 2, m_size.x / 2, m_size.y / 2,
+
+        if (m_dirty)
+        {
+            m_dirty--;
+
+            glTexSubImage2D(GL_TEXTURE_2D, 0, 0, m_frame * m_size.y / 2,
+                            m_size.x / 2, m_size.y / 2,
 #if !defined __CELLOS_LV2__
-                        GL_RGBA, GL_UNSIGNED_BYTE,
+                            GL_RGBA, GL_UNSIGNED_BYTE,
 #else
-                        /* The PS3 is big-endian */
-                        GL_RGBA, GL_UNSIGNED_INT_8_8_8_8,
+                            /* The PS3 is big-endian */
+                            GL_RGBA, GL_UNSIGNED_INT_8_8_8_8,
 #endif
-                        m_pixels + m_size.x * m_size.y / 4 * m_frame);
+                            m_pixels + m_size.x * m_size.y / 4 * m_frame);
+        }
 
         m_shader->Bind();
 #if !defined __CELLOS_LV2__ && !defined __ANDROID__ && !defined __APPLE__
@@ -272,11 +319,14 @@ private:
     GLuint m_tco;
 #endif
     int m_vertexattrib, m_texattrib;
-    int m_frame;
+    int m_frame, m_dirty;
     bool m_ready;
 
-    f64cmplx m_center, m_target;
-    double m_radius, m_angle;
+    f64cmplx m_center;
+    double m_radius, m_screenradius;
+
+    /* Debug information */
+    Text *m_centertext, *m_mousetext;
 };
 
 int main()