From http://www.jwz.org/xscreensaver/xscreensaver-5.38.tar.gz
[xscreensaver] / hacks / glx / esper.c
diff --git a/hacks/glx/esper.c b/hacks/glx/esper.c
new file mode 100644 (file)
index 0000000..20efa32
--- /dev/null
@@ -0,0 +1,2408 @@
+/* esper, Copyright (c) 2017 Jamie Zawinski <jwz@jwz.org>
+ * Enhance 224 to 176. Pull out track right. Center in pull back.
+ * Pull back. Wait a minute. Go right. Stop. Enhance 57 19. Track 45 left.
+ * Gimme a hardcopy right there.
+ *
+ * Permission to use, copy, modify, distribute, and sell this software and its
+ * documentation for any purpose is hereby granted without fee, provided that
+ * the above copyright notice appear in all copies and that both that
+ * copyright notice and this permission notice appear in supporting
+ * documentation.  No representations are made about the suitability of this
+ * software for any purpose.  It is provided "as is" without express or
+ * implied warranty.
+ */
+
+/*
+  The Esper machine has a 4:3 display, about 12" diagonal.
+  The display is overlayed with a 10x7 grid of blue lines.
+  The scene goes approximately like this:
+
+      "Enhance 224 To 176."
+
+          ZM 0000  NS 0000  EW 0000
+
+      The reticle is displayed centered.
+      It moves in 8 steps with 3 frame blur to move to around to grid 1,4.
+
+          ZM 0000  NS 0000  EW 0000
+          ZM 0000  NS 0001  EW 0001
+          ZM 0000  NS 0001  EW 0002
+          ZM 0000  NS 0002  EW 0003
+          ZM 0000  NS 0003  EW 0005
+          ZM 0000  NS 0004  EW 0008
+          ZM 0000  NS 0015  EW 0011
+
+      These numbers appear to have little relation to what we are
+      actually seeing on the screen.  Also the same numbers are
+      repeated later when looking at totally different parts of
+      the photograph.
+
+          ZM 0000  NS 0117  EW 0334
+
+      The box appears: 8 steps, final box is 1.5x2.25 at -0.5,4.0.
+
+          ZM 4086  NS 0117  EW 0334
+
+      The box blinks yellow 5x.
+      The image's zoom-and-pan takes 8 steps, with no text on the screen.
+      The zoom is in discreet steps, with flashes.
+      The grid stays the same size the whole time.
+      The flashes look like solarization to blue.
+      When the zoom is finished, there is still no text.
+
+      "Enhance."  Goes 4 more ticks down the same hole?
+      "Stop."  Moves up a little bit at the end.
+
+      Then with no instructions, it goes 20 ticks by itself, off camera.
+
+      "Move in."  10 ticks.
+      "Stop."  (We are looking at a fist in the picture.)
+      "Pull out track right."
+      "Stop."  (We are looking at a newspaper.)
+      "Center and pull back."
+      "Stop."  (We just passed the round mirror.)
+      "Track 45 right."
+      "Stop."
+      "Center and stop."
+
+      This time there was no grid until it stopped, then the grid showed up.
+      There is video tearing at the bottom.
+
+      "Enhance 34 to 36."
+
+          ZM 0000  NS 0063  EW 0185
+          ZM 0000  NS 0197  EW 0334
+          ZM 3841  NS 0197  EW 0334
+
+      It kind of zooms in to the center wobbly and willy-nilly.
+      We are now looking at a glass.
+
+      "Pan right and pull back."  (There is no grid while moving again.)
+      "Stop."
+
+      Ok, at this point, we enter fantasy-land.  From here on, the images
+      shown are very high resolution with no noise.  And suddenly the 
+      UI on the Esper is *way* higher resolution.  My theory is that from
+      this point on in the scene, we are not looking at the literal Esper
+      machine, but instead the movie is presenting Decard's perception of
+      it.  We're seeing the room, not the photo of the room.  The map has
+      become the territory. 
+
+      "Enhance 34 to 46."
+
+          ZM 0000  NS 0197  EW 0334
+
+      This has the reticle and box only, no grid, ends with no grid.
+
+      "Pull back."
+      "Wait a minute. Go right."
+      "Stop."
+      Now it's going around the corner or something.
+
+      "Enhance 57 19."
+      This has a reticle then box, but the image started zooming early.
+
+      "Track 45 left."
+      zooms out and moves left
+
+      "Stop."  (O hai Zhora.)
+      "Enhance 15 to 23."
+
+          ZM 3852  NS 0197  EW 0334
+
+      "Gimme a hardcopy right there."
+
+      The printer polaroid is WAY lower resolution than the image we see on
+      the "screen" -- in keeping with my theory that we were not seeing the
+      screen.
+
+
+  TODO:
+
+  * There's a glitch at the top/bottom of the texfont textures.
+  * "Pull back" isn't quite symmetric: zoom origin is slightly off.
+  * Maybe display text like "Pull right" and "Stop".
+*/
+
+
+/* Use a small point size to keep it nice and grainy. */
+#if defined(HAVE_COCOA) || defined(HAVE_ANDROID)
+# define TITLE_FONT "OCR A Std 10, Lucida Console 10, Monaco 10"
+#else  /* real X11 */
+# define TITLE_FONT "-*-courier-bold-r-*-*-*-100-*-*-m-*-*-*"
+#endif
+
+#define DEFAULTS  "*delay:           20000                \n" \
+                 "*wireframe:       False                \n" \
+                  "*showFPS:         False                \n" \
+                  "*fpsTop:          True                 \n" \
+                 "*useSHM:          True                 \n" \
+                  "*titleFont: "     TITLE_FONT          "\n" \
+                  "*desktopGrabber:  xscreensaver-getimage -no-desktop %s\n" \
+                 "*grabDesktopImages:   False \n" \
+                 "*chooseRandomImages:  True  \n" \
+                 "*gridColor:    #4444FF\n" \
+                 "*reticleColor: #FFFF77\n" \
+                 "*textColor:    #FFFFBB\n" \
+
+# define free_esper 0
+# define refresh_esper 0
+# define release_esper 0
+# include "xlockmore.h"
+
+#undef countof
+#define countof(x) (sizeof((x))/sizeof((*x)))
+
+#undef RANDSIGN
+#define RANDSIGN() ((random() & 1) ? 1 : -1)
+#undef BELLRAND
+#define BELLRAND(n) ((frand((n)) + frand((n)) + frand((n))) / 3)
+
+#ifdef USE_GL
+
+#undef SMOOTH
+
+# define DEF_GRID_SIZE      "11"
+# define DEF_GRID_THICKNESS "15"
+# define DEF_TITLES         "True"
+# define DEF_SPEED          "1.0"
+# define DEF_DEBUG          "False"
+
+#include "grab-ximage.h"
+#include "texfont.h"
+
+#ifdef HAVE_XSHM_EXTENSION
+# include "xshm.h"  /* to get <sys/shm.h> */
+#endif
+
+
+typedef struct {
+  double x, y, w, h;
+} rect;
+
+typedef struct {
+  ModeInfo *mi;
+  unsigned long id;               /* unique */
+  char *title;                    /* the filename of this image */
+  int w, h;                       /* size in pixels of the image */
+  int tw, th;                     /* size in pixels of the texture */
+  XRectangle geom;                /* where in the image the bits are */
+  Bool loaded_p;                  /* whether the image has finished loading */
+  Bool used_p;                    /* whether the image has yet appeared
+                                      on screen */
+  GLuint texid;                           /* which texture contains the image */
+  int refcount;                           /* how many sprites refer to this image */
+} image;
+
+
+typedef enum {
+  BLANK,
+  GRID_ON,
+  IMAGE_LOAD,
+  IMAGE_UNLOAD,
+  IMAGE_FORCE_UNLOAD,
+  REPOSITION,
+  RETICLE_ON,
+  RETICLE_MOVE,
+  BOX_MOVE,
+  IMAGE_ZOOM,
+  MANUAL_RETICLE_ON,
+  MANUAL_RETICLE,
+  MANUAL_BOX_ON,
+  MANUAL_BOX,
+} anim_state;
+
+typedef enum { NEW, IN, FULL, OUT, DEAD } sprite_state;
+typedef enum { IMAGE, RETICLE, BOX, GRID, FLASH, TEXT } sprite_type;
+
+typedef struct {
+  unsigned long id;               /* unique */
+  sprite_type type;
+  image *img;                     /* type = IMAGE */
+  unsigned long text_id;          /* type = TEXT */
+  char *text;
+  GLfloat opacity;
+  GLfloat thickness_scale;        /* line and image types */
+  Bool throb_p;
+  double start_time;              /* when this animation began */
+  double duration;                /* lifetime of sprite in seconds; 0 = inf */
+  double fade_duration;                   /* speed of fade in and fade out */
+  double pause_duration;          /* delay before fade-in starts */
+  Bool remain_p;                  /* pause forever before fade-out */
+  rect from, to, current;         /* the journey this image is taking */
+  sprite_state state;             /* the state we're in right now */
+  double state_time;              /* time of last state change */
+  int frame_count;                /* frames since last state change */
+  Bool fatbits_p;                 /* For image texture rendering */    
+  Bool back_p;                    /* If BOX, zooming out, not in */
+} sprite;
+
+
+typedef struct {
+  GLXContext *glx_context;
+  int nimages;                 /* how many images are loaded or loading now */
+  image *images[10];           /* pointers to the images */
+
+  int nsprites;                        /* how many sprites are animating right now */
+  sprite *sprites[100];                /* pointers to the live sprites */
+
+  double now;                  /* current time in seconds */
+  double dawn_of_time;         /* when the program launched */
+  double image_load_time;      /* time when we last loaded a new image */
+
+  texture_font_data *font_data;
+
+  int sprite_id, image_id;      /* debugging id counters */
+
+  GLfloat grid_color[4], reticle_color[4], text_color[4];
+
+  anim_state anim_state;       /* Counters for global animation state, */
+  double anim_start, anim_duration;
+
+  Bool button_down_p;
+
+} esper_state;
+
+static esper_state *sss = NULL;
+
+
+/* Command-line arguments
+ */
+static int grid_size;
+static int grid_thickness;
+
+static Bool do_titles;     /* Display image titles. */
+static GLfloat speed;
+static Bool debug_p;       /* Be loud and do weird things. */
+
+
+static XrmOptionDescRec opts[] = {
+  { "-speed",      ".speed",     XrmoptionSepArg, 0 },
+  { "-titles",     ".titles",    XrmoptionNoArg, "True"  },
+  { "-no-titles",  ".titles",    XrmoptionNoArg, "False" },
+  { "-debug",      ".debug",     XrmoptionNoArg, "True"  },
+};
+
+static argtype vars[] = {
+  { &grid_size,     "gridSize",     "GridSize",     DEF_GRID_SIZE,      t_Int},
+  { &grid_thickness,"gridThickness","GridThickness",DEF_GRID_THICKNESS, t_Int},
+  { &do_titles,     "titles",       "Titles",       DEF_TITLES,        t_Bool},
+  { &speed,         "speed",        "Speed",        DEF_SPEED,        t_Float},
+  { &debug_p,       "debug",        "Debug",        DEF_DEBUG,         t_Bool},
+};
+
+ENTRYPOINT ModeSpecOpt esper_opts = {countof(opts), opts, countof(vars), vars, NULL};
+
+
+/* Returns the current time in seconds as a double.
+ */
+static double
+double_time (void)
+{
+  struct timeval now;
+# ifdef GETTIMEOFDAY_TWO_ARGS
+  struct timezone tzp;
+  gettimeofday(&now, &tzp);
+# else
+  gettimeofday(&now);
+# endif
+
+  return (now.tv_sec + ((double) now.tv_usec * 0.000001));
+}
+
+static const char *
+state_name (anim_state s)
+{
+  switch (s) {
+  case BLANK:         return "BLANK";
+  case GRID_ON:       return "GRID_ON";
+  case IMAGE_LOAD:    return "IMAGE_LOAD";
+  case IMAGE_UNLOAD:  return "IMAGE_UNLOAD";
+  case IMAGE_FORCE_UNLOAD: return "IMAGE_FORCE_UNLOAD";
+  case REPOSITION:    return "REPOSITION";
+  case RETICLE_ON:    return "RETICLE_ON";
+  case RETICLE_MOVE:  return "RETICLE_MOVE";
+  case BOX_MOVE:      return "BOX_MOVE";
+  case IMAGE_ZOOM:    return "IMAGE_ZOOM";
+  case MANUAL_BOX_ON: return "MANUAL_BOX_ON";
+  case MANUAL_BOX:    return "MANUAL_BOX";
+  case MANUAL_RETICLE_ON: return "MANUAL_RETICLE_ON";
+  case MANUAL_RETICLE:    return "MANUAL_RETICLE";
+  default:            return "UNKNOWN";
+  }
+}
+
+
+static void image_loaded_cb (const char *filename, XRectangle *geom,
+                             int image_width, int image_height,
+                             int texture_width, int texture_height,
+                             void *closure);
+
+
+/* Allocate an image structure and start a file loading in the background.
+ */
+static image *
+alloc_image (ModeInfo *mi)
+{
+  esper_state *ss = &sss[MI_SCREEN(mi)];
+  int wire = MI_IS_WIREFRAME(mi);
+  image *img = (image *) calloc (1, sizeof (*img));
+
+  img->id = ++ss->image_id;
+  img->loaded_p = False;
+  img->used_p = False;
+  img->mi = mi;
+
+  glGenTextures (1, &img->texid);
+  if (img->texid <= 0) abort();
+
+  ss->image_load_time = ss->now;
+
+  if (wire)
+    image_loaded_cb (0, 0, 0, 0, 0, 0, img);
+  else
+    {
+      /* If possible, load images at much higher resolution than the window,
+         to facilitate deep zooms.
+       */
+      int max_max = 4096;  /* ~12 megapixels */
+      int max = 0;
+
+# if defined(HAVE_XSHM_EXTENSION) && \
+     !defined(HAVE_MOBILE) && \
+     !defined(HAVE_COCOA)
+
+      /* Try not to ask for an image larger than the SHM segment size.
+         If XSHM fails in a real-X11 world, it can take a staggeringly long
+         time to transfer the image bits from the server over Xproto -- like,
+         *18 seconds* for 4096 px and 8 seconds for 3072 px on MacOS XQuartz.
+         What madness is this?
+       */
+      unsigned long shmmax = 0;
+
+#  if defined(SHMMAX)
+      /* Linux 2.6 defines this to be 0x2000000, but on CentOS 6.9,
+         "sysctl kernel.shmmax" reports a luxurious 0x1000000000. */
+      shmmax = SHMMAX;
+#  elif defined(__APPLE__)
+      /* MacOS 10.13 "sysctl kern.sysv.shmmax" is paltry: */
+      shmmax = 0x400000;
+#  endif /* !SHMMAX */
+
+      if (shmmax)
+        {
+          /* Roughly, bytes => NxN. b = (n/8)*4n = n*n*4, so n^2 = 2b, so: */
+          unsigned long n = sqrt(shmmax)/2;
+          if (n < max_max)
+            max_max = n;
+        }
+# endif /* HAVE_XSHM_EXTENSION and real X11 */
+
+      glGetIntegerv (GL_MAX_TEXTURE_SIZE, &max);
+      if (max > max_max) max = max_max;
+
+      /* Never ask for an image smaller than the window, even if that
+         will make XSHM fall back to Xproto. */
+      if (max < MI_WIDTH(mi) || max < MI_HEIGHT(mi))
+        max = 0;
+
+      load_texture_async (mi->xgwa.screen, mi->window, *ss->glx_context,
+                          max, max, False, img->texid, image_loaded_cb, img);
+    }
+
+  ss->images[ss->nimages++] = img;
+  if (ss->nimages >= countof(ss->images)) abort();
+
+  return img;
+}
+
+
+/* Callback that tells us that the texture has been loaded.
+ */
+static void
+image_loaded_cb (const char *filename, XRectangle *geom,
+                 int image_width, int image_height,
+                 int texture_width, int texture_height,
+                 void *closure)
+{
+  image *img = (image *) closure;
+  ModeInfo *mi = img->mi;
+  int ow, oh;
+  /* esper_state *ss = &sss[MI_SCREEN(mi)]; */
+
+  int wire = MI_IS_WIREFRAME(mi);
+
+  if (wire)
+    {
+      img->w = MI_WIDTH (mi) * (0.5 + frand (1.0));
+      img->h = MI_HEIGHT (mi);
+      img->geom.width  = img->w;
+      img->geom.height = img->h;
+      goto DONE;
+    }
+
+  if (image_width == 0 || image_height == 0)
+    exit (1);
+
+  img->w  = image_width;
+  img->h  = image_height;
+  img->tw = texture_width;
+  img->th = texture_height;
+  img->geom = *geom;
+  img->title = (filename ? strdup (filename) : 0);
+
+  ow = img->geom.width;
+  oh = img->geom.height;
+
+  /* If the image's width doesn't come back as the width of the screen,
+     then the image must have been scaled down (due to insufficient
+     texture memory.)  Scale up the coordinates to stretch the image
+     to fill the window.
+   */
+  if (img->w != MI_WIDTH(mi))
+    {
+      double scale = (double) MI_WIDTH(mi) / img->w;
+      img->w  *= scale;
+      img->h  *= scale;
+      img->tw *= scale;
+      img->th *= scale;
+      img->geom.x      *= scale;
+      img->geom.y      *= scale;
+      img->geom.width  *= scale;
+      img->geom.height *= scale;
+    }
+
+  /* xscreensaver-getimage returns paths relative to the image directory
+     now, so leave the sub-directory part in.  Unless it's an absolute path.
+  */
+  if (img->title && img->title[0] == '/')
+    {
+      /* strip filename to part between last "/" and last ".". */
+      char *s = strrchr (img->title, '/');
+      if (s) strcpy (img->title, s+1);
+      s = strrchr (img->title, '.');
+      if (s) *s = 0;
+    }
+
+# if !(__APPLE__ && TARGET_IPHONE_SIMULATOR || !defined(__OPTIMIZE__))
+  if (debug_p)
+# endif
+    fprintf (stderr, "%s: loaded %lu \"%s\" %dx%d\n",
+             progname, img->id, (img->title ? img->title : "(null)"),
+             ow, oh);
+ DONE:
+
+  img->loaded_p = True;
+}
+
+
+
+/* Free the image and texture, after nobody is referencing it.
+ */
+static void
+destroy_image (ModeInfo *mi, image *img)
+{
+  esper_state *ss = &sss[MI_SCREEN(mi)];
+  Bool freed_p = False;
+  int i;
+
+  if (!img) abort();
+  if (!img->loaded_p) abort();
+  if (!img->used_p) abort();
+  if (img->texid <= 0) abort();
+  if (img->refcount != 0) abort();
+
+  for (i = 0; i < ss->nimages; i++)            /* unlink it from the list */
+    if (ss->images[i] == img)
+      {
+        int j;
+        for (j = i; j < ss->nimages-1; j++)    /* pull remainder forward */
+          ss->images[j] = ss->images[j+1];
+        ss->images[j] = 0;
+        ss->nimages--;
+        freed_p = True;
+        break;
+      }
+
+  if (!freed_p) abort();
+
+  if (debug_p)
+    fprintf (stderr, "%s: unloaded img %2lu: \"%s\"\n",
+             progname, img->id, (img->title ? img->title : "(null)"));
+
+  if (img->title) free (img->title);
+  glDeleteTextures (1, &img->texid);
+  free (img);
+}
+
+
+/* Return an image to use for a sprite.
+   If it's time for a new one, get a new one.
+   Otherwise, use an old one.
+   Might return 0 if the machine is really slow.
+ */
+static image *
+get_image (ModeInfo *mi)
+{
+  esper_state *ss = &sss[MI_SCREEN(mi)];
+  image *img = 0;
+  image *loading_img = 0;
+  int i;
+
+  for (i = 0; i < ss->nimages; i++)
+    {
+      image *img2 = ss->images[i];
+      if (!img2) abort();
+      if (!img2->loaded_p)
+        loading_img = img2;
+      else
+        img = img2;
+    }
+
+  /* Make sure that there is always one unused image in the pipe.
+   */
+  if (!img && !loading_img)
+    alloc_image (mi);
+
+  return img;
+}
+
+
+/* Allocate a new sprite and start its animation going.
+ */
+static sprite *
+new_sprite (ModeInfo *mi, sprite_type type)
+{
+  esper_state *ss = &sss[MI_SCREEN(mi)];
+  image *img = (type == IMAGE ? get_image (mi) : 0);
+  sprite *sp;
+
+  if (type == IMAGE && !img)
+    {
+      /* Oops, no images yet!  The machine is probably hurting bad.
+         Let's give it some time before thrashing again. */
+      usleep (250000);
+      return 0;
+    }
+
+  sp = (sprite *) calloc (1, sizeof (*sp));
+  sp->id = ++ss->sprite_id;
+  sp->type = type;
+  sp->start_time = ss->now;
+  sp->state_time = sp->start_time;
+  sp->thickness_scale = 1;
+  sp->throb_p = True;
+  sp->to.x = 0.5;
+  sp->to.y = 0.5;
+  sp->to.w = 1.0;
+  sp->to.h = 1.0;
+
+  if (img)
+    {
+      sp->img = img;
+      sp->img->refcount++;
+      sp->img->used_p = True;
+      sp->duration = 0;   /* forever, until further notice */
+      sp->fade_duration = 0.5;
+
+      /* Scale the sprite so that the image bits fill the window. */
+      {
+        double w = MI_WIDTH(mi);
+        double h = MI_HEIGHT(mi);
+        double r;
+        r = ((img->geom.height / (double) img->geom.width) * (w / h));
+        if (r > 1)
+          sp->to.h *= r;
+        else
+          sp->to.w /= r;
+      }
+
+      /* Pan to a random spot */
+      if (sp->to.h > 1)
+        sp->to.y += frand ((sp->to.h - 1) / 2) * RANDSIGN();
+      if (sp->to.w > 1)
+        sp->to.x += frand ((sp->to.w - 1) / 2) * RANDSIGN();
+    }
+
+  sp->from = sp->current = sp->to;
+
+  ss->sprites[ss->nsprites++] = sp;
+  if (ss->nsprites >= countof(ss->sprites)) abort();
+
+  return sp;
+}
+
+
+static sprite *
+copy_sprite (ModeInfo *mi, sprite *old)
+{
+  sprite *sp = new_sprite (mi, (sprite_type) ~0L);
+  int id;
+  double tt = sp->start_time;
+  if (!sp) abort();
+  id = sp->id;
+  memcpy (sp, old, sizeof(*sp));
+  sp->id = id;
+  sp->state = NEW;
+  sp->state_time = sp->start_time = tt;
+  if (sp->img)
+    sp->img->refcount++;
+  return sp;
+}
+
+
+/* Free the given sprite, and decrement the reference count on its image.
+ */
+static void
+destroy_sprite (ModeInfo *mi, sprite *sp)
+{
+  esper_state *ss = &sss[MI_SCREEN(mi)];
+  Bool freed_p = False;
+  image *img;
+  int i;
+
+  if (!sp) abort();
+  if (sp->state != DEAD) abort();
+  img = sp->img;
+
+  if (sp->type != IMAGE)
+    {
+      if (img) abort();
+    }
+  else
+    {
+      if (!img) abort();
+      if (!img->loaded_p) abort();
+      if (!img->used_p) abort();
+      if (img->refcount <= 0) abort();
+    }
+
+  for (i = 0; i < ss->nsprites; i++)           /* unlink it from the list */
+    if (ss->sprites[i] == sp)
+      {
+        int j;
+        for (j = i; j < ss->nsprites-1; j++)   /* pull remainder forward */
+          ss->sprites[j] = ss->sprites[j+1];
+        ss->sprites[j] = 0;
+        ss->nsprites--;
+        freed_p = True;
+        break;
+      }
+
+  if (!freed_p) abort();
+  if (sp->text) free (sp->text);
+  free (sp);
+  sp = 0;
+
+  if (img)
+    {
+      img->refcount--;
+      if (img->refcount < 0) abort();
+      if (img->refcount == 0)
+        destroy_image (mi, img);
+    }
+}
+
+
+/* Updates the sprite for the current frame of the animation based on
+   its creation time compared to the current wall clock.
+ */
+static void
+tick_sprite (ModeInfo *mi, sprite *sp)
+{
+  esper_state *ss = &sss[MI_SCREEN(mi)];
+  image *img = sp->img;
+  double now = ss->now;
+  double secs;
+  double ratio;
+  GLfloat visible = sp->duration + sp->fade_duration * 2;
+  GLfloat total = sp->pause_duration + visible;
+
+  if (sp->type != IMAGE)
+    {
+      if (sp->img) abort();
+    }
+  else
+    {
+      if (! sp->img) abort();
+      if (! img->loaded_p) abort();
+    }
+
+  /*          pause        fade  duration        fade
+     |------------|------------|---------|-----------|
+                   ....----====##########====----....
+               from             current            to
+   */
+
+  secs = now - sp->start_time;
+  ratio = (visible <= 0 ? 1 : ((secs - sp->pause_duration) / visible));
+  if (ratio < 0) ratio = 0;
+  else if (ratio > 1) ratio = 1;
+
+  sp->current.x = sp->from.x + ratio * (sp->to.x - sp->from.x);
+  sp->current.y = sp->from.y + ratio * (sp->to.y - sp->from.y);
+  sp->current.w = sp->from.w + ratio * (sp->to.w - sp->from.w);
+  sp->current.h = sp->from.h + ratio * (sp->to.h - sp->from.h);
+
+  sp->thickness_scale = 1;
+
+  if (secs < sp->pause_duration)
+    {
+      sp->state = IN;
+      sp->opacity = 0;
+    }
+  else if (secs < sp->pause_duration + sp->fade_duration)
+    {
+      sp->state = IN;
+      sp->opacity = (secs - sp->pause_duration) / (GLfloat) sp->fade_duration;
+    }
+  else if (sp->duration == 0 ||  /* 0 means infinite lifetime */
+           sp->remain_p ||
+           secs < sp->pause_duration + sp->fade_duration + sp->duration)
+    {
+      sp->state = FULL;
+      sp->opacity = 1;
+
+      /* Just after reaching full opacity, pulse the width up and down. */
+      if (sp->fade_duration > 0 &&
+          secs < sp->pause_duration + sp->fade_duration * 2)
+        {
+          GLfloat f = ((secs - (sp->pause_duration + sp->fade_duration)) /
+                       sp->fade_duration);
+          if (sp->throb_p)
+            sp->thickness_scale = 1 + 3 * (f > 0.5 ? 1-f : f);
+        }
+    }
+  else if (secs < total)
+    {
+      sp->state = OUT;
+      sp->opacity = (total - secs) / sp->fade_duration;
+    }
+  else
+    {
+      sp->state = DEAD;
+      sp->opacity = 0;
+    }
+
+  sp->frame_count++;
+}
+
+
+/* Draw the given sprite at the phase of its animation dictated by
+   its creation time compared to the current wall clock.
+ */
+static void
+draw_image_sprite (ModeInfo *mi, sprite *sp)
+{
+  /* esper_state *ss = &sss[MI_SCREEN(mi)]; */
+  int wire = MI_IS_WIREFRAME(mi);
+  image *img = sp->img;
+
+  if (! sp->img) abort();
+  if (! img->loaded_p) abort();
+
+  glPushMatrix();
+  {
+    GLfloat s = 1 + (sp->thickness_scale - 1) / 40.0;
+    glTranslatef (0.5, 0.5, 0);
+    glScalef (s, s, 1);
+    glTranslatef (-0.5, -0.5, 0);
+
+    glTranslatef (sp->current.x, sp->current.y, 0);
+    glScalef (sp->current.w, sp->current.h, 1);
+
+    glTranslatef (-0.5, -0.5, 0);
+
+    if (wire)                  /* Draw a grid inside the box */
+      {
+        GLfloat dy = 0.1;
+        GLfloat dx = dy * img->w / img->h;
+        GLfloat x, y;
+
+        if (sp->id & 1)
+          glColor4f (sp->opacity, 0, 0, 1);
+        else
+          glColor4f (0, 0, sp->opacity, 1);
+
+        glBegin(GL_LINES);
+        glVertex3f (0, 0, 0); glVertex3f (1, 1, 0);
+        glVertex3f (1, 0, 0); glVertex3f (0, 1, 0);
+
+        for (y = 0; y < 1+dy; y += dy)
+          {
+            GLfloat yy = (y > 1 ? 1 : y);
+            for (x = 0.5; x < 1+dx; x += dx)
+              {
+                GLfloat xx = (x > 1 ? 1 : x);
+                glVertex3f (0, xx, 0); glVertex3f (1, xx, 0);
+                glVertex3f (yy, 0, 0); glVertex3f (yy, 1, 0);
+              }
+            for (x = 0.5; x > -dx; x -= dx)
+              {
+                GLfloat xx = (x < 0 ? 0 : x);
+                glVertex3f (0, xx, 0); glVertex3f (1, xx, 0);
+                glVertex3f (yy, 0, 0); glVertex3f (yy, 1, 0);
+              }
+          }
+        glEnd();
+      }
+    else                       /* Draw the texture quad */
+      {
+        GLfloat texw  = img->geom.width  / (GLfloat) img->tw;
+        GLfloat texh  = img->geom.height / (GLfloat) img->th;
+        GLfloat texx1 = img->geom.x / (GLfloat) img->tw;
+        GLfloat texy1 = img->geom.y / (GLfloat) img->th;
+        GLfloat texx2 = texx1 + texw;
+        GLfloat texy2 = texy1 + texh;
+        GLfloat o = sp->opacity;
+        GLint mag = (sp->fatbits_p ? GL_NEAREST : GL_LINEAR);
+
+        glBindTexture (GL_TEXTURE_2D, img->texid);
+
+        glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
+        glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
+        glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag);
+        glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, mag);
+
+        /* o = 1 - sin ((1 - o*o*o) * M_PI/2); */
+        glColor4f (1, 1, 1, o);
+
+        glNormal3f (0, 0, 1);
+        glBegin (GL_QUADS);
+        glTexCoord2f (texx1, texy2); glVertex3f (0, 0, 0);
+        glTexCoord2f (texx2, texy2); glVertex3f (1, 0, 0);
+        glTexCoord2f (texx2, texy1); glVertex3f (1, 1, 0);
+        glTexCoord2f (texx1, texy1); glVertex3f (0, 1, 0);
+        glEnd();
+
+        if (debug_p)           /* Draw a border around the image */
+          {
+            if (!wire) glDisable (GL_TEXTURE_2D);
+            glColor4f (sp->opacity, 0, 0, 1);
+            glBegin (GL_LINE_LOOP);
+            glVertex3f (0, 0, 0);
+            glVertex3f (0, 1, 0);
+            glVertex3f (1, 1, 0);
+            glVertex3f (1, 0, 0);
+            glEnd();
+            if (!wire) glEnable (GL_TEXTURE_2D);
+          }
+      }
+  }
+  glPopMatrix();
+}
+
+
+static void
+draw_line_sprite (ModeInfo *mi, sprite *sp)
+{
+  esper_state *ss = &sss[MI_SCREEN(mi)];
+  int wire = MI_IS_WIREFRAME(mi);
+  int w = MI_WIDTH(mi);
+  int h = MI_HEIGHT(mi);
+  int wh = (w > h ? w : h);
+  int gs = (sp->type == RETICLE ? grid_size+1 : grid_size);
+  int sx = wh / (gs + 1);
+  int sy;
+  int k;
+  GLfloat t = grid_thickness * sp->thickness_scale;
+  int fade;
+  GLfloat color[4];
+
+  GLfloat x  = w * sp->current.x;
+  GLfloat y  = h * sp->current.y;
+  GLfloat bw = w * sp->current.w;
+  GLfloat bh = h * sp->current.h;
+
+  if (sx < 10) sx = 10;
+  sy = sx;
+
+  if (t > sx/3) t = sx/3;
+  if (t < 1) t = 1;
+  fade = t;
+  if (fade < 1) fade = 1;
+
+  if (t <= 0 || sp->opacity <= 0) return;
+
+  glPushMatrix();
+  glLoadIdentity();
+
+  if (debug_p)
+    {
+      GLfloat s = 0.75;
+      glScalef (s, s, s);
+    }
+
+  glOrtho (0, w, 0, h, -1, 1);
+
+  switch (sp->type) {
+  case GRID:    memcpy (color, ss->grid_color,    sizeof(color)); break;
+  case RETICLE: memcpy (color, ss->reticle_color, sizeof(color)); break;
+  case BOX:     memcpy (color, ss->reticle_color, sizeof(color)); break;
+  default: abort();
+  }
+
+  if (sp->type == GRID)
+    {
+      GLfloat s = 1 + (sp->thickness_scale - 1) / 120.0;
+      glTranslatef (w/2, h/2, 0);
+      glScalef (s, s, 1);
+      glTranslatef (-w/2, -h/2, 0);
+    }
+
+  glColor4fv (color);
+
+  if (!wire) glDisable (GL_TEXTURE_2D);
+
+  for (k = 0; k < fade; k++)
+    {
+      GLfloat t2 = t * (1 - (k / (fade * 1.0)));
+      if (t2 <= 0) break;
+      color[3] = sp->opacity / fade;
+      glColor4fv (color);
+
+      glBegin (wire ? GL_LINES : GL_QUADS);
+
+      switch (sp->type) {
+      case GRID:
+        {
+          GLfloat xoff = (w - sx * (w / sx)) / 2.0;
+          GLfloat yoff = (h - sy * (h / sy)) / 2.0;
+          for (y = -sy/2+t2/2; y < h; y += sy)
+            for (x = -sx/2-t2/2; x < w; x += sx)
+              {
+                glVertex3f (xoff+x+t2, yoff+y,       0);
+                glVertex3f (xoff+x+t2, yoff+y+sy-t2, 0);
+                glVertex3f (xoff+x,    yoff+y+sy-t2, 0);
+                glVertex3f (xoff+x,    yoff+y,       0);
+                mi->polygon_count++;
+
+                glVertex3f (xoff+x,    yoff+y-t2, 0);
+                glVertex3f (xoff+x+sx, yoff+y-t2, 0);
+                glVertex3f (xoff+x+sx, yoff+y,    0);
+                glVertex3f (xoff+x,    yoff+y,    0);
+                mi->polygon_count++;
+              }
+        }
+        break;
+
+      case BOX:
+        glVertex3f (x-bw/2-t2/2, y-bh/2-t2/2, 0);
+        glVertex3f (x+bw/2+t2/2, y-bh/2-t2/2, 0);
+        glVertex3f (x+bw/2+t2/2, y-bh/2+t2/2, 0);
+        glVertex3f (x-bw/2-t2/2, y-bh/2+t2/2, 0);
+        mi->polygon_count++;
+
+        glVertex3f (x-bw/2-t2/2, y+bh/2-t2/2, 0);
+        glVertex3f (x+bw/2+t2/2, y+bh/2-t2/2, 0);
+        glVertex3f (x+bw/2+t2/2, y+bh/2+t2/2, 0);
+        glVertex3f (x-bw/2-t2/2, y+bh/2+t2/2, 0);
+        mi->polygon_count++;
+
+        glVertex3f (x-bw/2+t2/2, y-bh/2+t2/2, 0);
+        glVertex3f (x-bw/2+t2/2, y+bh/2-t2/2, 0);
+        glVertex3f (x-bw/2-t2/2, y+bh/2-t2/2, 0);
+        glVertex3f (x-bw/2-t2/2, y-bh/2+t2/2, 0);
+        mi->polygon_count++;
+
+        glVertex3f (x+bw/2+t2/2, y-bh/2+t2/2, 0);
+        glVertex3f (x+bw/2+t2/2, y+bh/2-t2/2, 0);
+        glVertex3f (x+bw/2-t2/2, y+bh/2-t2/2, 0);
+        glVertex3f (x+bw/2-t2/2, y-bh/2+t2/2, 0);
+        mi->polygon_count++;
+        break;
+
+      case RETICLE:
+        glVertex3f (x+t2/2, y+sy/2-t2/2, 0);
+        glVertex3f (x+t2/2, h,           0);
+        glVertex3f (x-t2/2, h,           0);
+        glVertex3f (x-t2/2, y+sy/2-t2/2, 0);
+        mi->polygon_count++;
+
+        glVertex3f (x-t2/2, y-sy/2+t2/2, 0);
+        glVertex3f (x-t2/2, 0,           0);
+        glVertex3f (x+t2/2, 0,           0);
+        glVertex3f (x+t2/2, y-sy/2+t2/2, 0);
+        mi->polygon_count++;
+
+        glVertex3f (x-sx/2+t2/2, y+t2/2, 0);
+        glVertex3f (0,           y+t2/2, 0);
+        glVertex3f (0,           y-t2/2, 0);
+        glVertex3f (x-sx/2+t2/2, y-t2/2, 0);
+        mi->polygon_count++;
+
+        glVertex3f (x+sx/2-t2/2, y-t2/2, 0);
+        glVertex3f (w,           y-t2/2, 0);
+        glVertex3f (w,           y+t2/2, 0);
+        glVertex3f (x+sx/2-t2/2, y+t2/2, 0);
+        mi->polygon_count++;
+        break;
+
+      default: abort();
+      }
+      glEnd();
+    }
+
+  glPopMatrix();
+
+  if (!wire) glEnable (GL_TEXTURE_2D);
+}
+
+
+static sprite * find_newest_sprite (ModeInfo *, sprite_type);
+static void compute_image_rect (rect *, sprite *, Bool);
+
+static void
+draw_text_sprite (ModeInfo *mi, sprite *sp)
+{
+  esper_state *ss = &sss[MI_SCREEN(mi)];
+  int wire = MI_IS_WIREFRAME(mi);
+  GLfloat w = MI_WIDTH(mi);
+  GLfloat h = MI_HEIGHT(mi);
+  GLfloat s;
+  int x, y, z;
+  XCharStruct e;
+  sprite *target = 0;
+  char text[255];
+  GLfloat color[4];
+  int i;
+
+  if (sp->opacity <= 0)
+    return;
+
+  for (i = 0; i < ss->nsprites; i++)
+    {
+      sprite *sp2 = ss->sprites[i];
+      if (sp2->id == sp->text_id && sp2->state != DEAD)
+        {
+          target = sp2;
+          break;
+        }
+    }
+
+  if (target)
+    {
+      rect r;
+      sprite *img;
+
+      if (target->opacity <= 0 && 
+          (target->state == NEW || target->state == IN))
+        return;
+
+      r = target->current;
+
+      img = find_newest_sprite (mi, IMAGE);
+      if (img)
+        compute_image_rect (&r, img, target->back_p);
+
+      mi->recursion_depth = (img
+                             ? MIN (img->current.w, img->current.h)
+                             : 0);
+
+      x = abs ((int) (r.x * 10000)) % 10000;
+      y = abs ((int) (r.y * 10000)) % 10000;
+      z = abs ((int) (r.w * 10000)) % 10000;
+
+      sprintf (text, "ZM %04d  NS %04d  EW %04d", z, y, x);
+
+      if ((x == 0 || x == 5000) &&             /* startup */
+          (y == 0 || y == 5000) &&
+          (z == 0 || z == 5000))
+        *text = 0;
+
+      if (do_titles && 
+          target->type == IMAGE &&
+          target->remain_p)  /* The initial background image */
+        {
+          char *s = (target->img &&
+                     target->img->title && *target->img->title
+                     ? target->img->title
+                     : "Loading");
+          int L = strlen (s);
+          int i = (L > 23 ? L-23 : 0);
+          sprintf (text, ">>%-23s", target->img->title + i);
+          for (s = text; *s; s++)
+            if (*s >= 'a' && *s <= 'z') *s += ('A'-'a');
+            else if (*s == '/' || *s == '-' || *s == '.') *s = '_';
+        }
+
+      if (!*text) return;
+
+      if (sp->text) free (sp->text);
+      sp->text = strdup (text);
+    }
+  else if (sp->text && *sp->text)
+    /* The target sprite might be dead, but we saved our last text. */
+    strcpy (text, sp->text);
+  else
+    /* No target, no saved text. */
+    return;
+
+  texture_string_metrics (ss->font_data, text, &e, 0, 0);
+
+  glPushMatrix();
+  glLoadIdentity();
+  glOrtho (0, 1, 0, 1, -1, 1);
+
+  /* Scale the text to fit N characters horizontally. */
+  {
+# ifdef HAVE_MOBILE
+    GLfloat c = 25;
+# else /* desktop */
+    GLfloat c = (MI_WIDTH(mi) <= 640  ? 25 :
+                 MI_WIDTH(mi) <= 1280 ? 32 : 64);
+# endif
+    s = w / (e.ascent * c);
+  }
+  w /= s;
+  h /= s;
+  x = (w - e.width) / 2;
+  y = e.ascent + e.descent * 2;
+
+  glScalef (1.0/w, 1.0/h, 1);
+  glTranslatef (x, y, 0);
+
+  memcpy (color, ss->text_color, sizeof(color));
+  color[3] = sp->opacity;
+  glColor4fv (color);
+
+  if (wire)
+    glEnable (GL_TEXTURE_2D);
+
+  print_texture_string (ss->font_data, text);
+  mi->polygon_count++;
+
+  if (wire)
+    glDisable (GL_TEXTURE_2D);
+  glPopMatrix();
+}
+
+
+static void
+draw_flash_sprite (ModeInfo *mi, sprite *sp)
+{
+  /* esper_state *ss = &sss[MI_SCREEN(mi)]; */
+  GLfloat o = sp->opacity;
+
+  if (o <= 0) return;
+  o = 0.7;  /* Too fast to see, so keep it consistent */
+
+  glPushMatrix();
+  int wire = MI_IS_WIREFRAME(mi);
+  if (!wire)
+    glDisable (GL_TEXTURE_2D);
+  glColor4f (0, 0, 1, o);
+  glColorMask (0, 0, 1, 1); /* write only into blue and alpha channels */
+  glBegin (GL_QUADS);
+  glVertex3f (0, 0, 0);
+  glVertex3f (1, 0, 0);
+  glVertex3f (1, 1, 0);
+  glVertex3f (0, 1, 0);
+  glEnd();
+  glColorMask (1, 1, 1, 1);
+  if (!wire)
+    glEnable (GL_TEXTURE_2D);
+  glPopMatrix();
+}
+
+
+static void
+draw_sprite (ModeInfo *mi, sprite *sp)
+{
+  switch (sp->type) {
+  case IMAGE:
+    draw_image_sprite (mi, sp);
+    break;
+  case RETICLE:
+  case BOX:
+  case GRID:
+    draw_line_sprite (mi, sp);
+    break;
+  case TEXT:
+    draw_text_sprite (mi, sp);
+    break;
+  case FLASH:
+    draw_flash_sprite (mi, sp);
+    break;
+  default:
+    abort();
+  }
+}
+
+
+static void
+tick_sprites (ModeInfo *mi)
+{
+  esper_state *ss = &sss[MI_SCREEN(mi)];
+  int i;
+  for (i = 0; i < ss->nsprites; i++)
+    tick_sprite (mi, ss->sprites[i]);
+
+  for (i = 0; i < ss->nsprites; i++)
+    {
+      sprite *sp = ss->sprites[i];
+      if (sp->state == DEAD)
+        {
+          destroy_sprite (mi, sp);
+          i--;
+        }
+    }
+}
+
+
+static void
+draw_sprites (ModeInfo *mi)
+{
+  esper_state *ss = &sss[MI_SCREEN(mi)];
+  int i;
+
+  glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+  glPushMatrix();
+
+/*
+  {
+    GLfloat rot = current_device_rotation();
+    glTranslatef (0.5, 0.5, 0);
+    glRotatef(rot, 0, 0, 1);
+    if ((rot >  45 && rot <  135) ||
+        (rot < -45 && rot > -135))
+      {
+        GLfloat s = MI_WIDTH(mi) / (GLfloat) MI_HEIGHT(mi);
+        glScalef (s, 1/s, 1);
+      }
+    glTranslatef (-0.5, -0.5, 0);
+  }
+*/
+
+  /* Draw the images first, then the overlays. */
+  for (i = 0; i < ss->nsprites; i++)
+    if (ss->sprites[i]->type == IMAGE)
+      draw_sprite (mi, ss->sprites[i]);
+  for (i = 0; i < ss->nsprites; i++)
+    if (ss->sprites[i]->type != IMAGE)
+      draw_sprite (mi, ss->sprites[i]);
+
+  glPopMatrix();
+
+  if (debug_p)                         /* draw a white box (the "screen") */
+    {
+      int wire = MI_IS_WIREFRAME(mi);
+
+      if (!wire) glDisable (GL_TEXTURE_2D);
+
+      glColor4f (1, 1, 1, 1);
+      glBegin (GL_LINE_LOOP);
+      glVertex3f (0, 0, 0);
+      glVertex3f (0, 1, 0);
+      glVertex3f (1, 1, 0);
+      glVertex3f (1, 0, 0);
+      glEnd();
+
+      if (!wire) glEnable (GL_TEXTURE_2D);
+    }
+}
+
+
+static void
+fadeout_sprite (ModeInfo *mi, sprite *sp)
+{
+  esper_state *ss = &sss[MI_SCREEN(mi)];
+
+  /* If it hasn't faded in yet, don't fade out. */
+  if (ss->now <= sp->start_time + sp->pause_duration)
+    sp->fade_duration = 0;
+
+  /* Pretend it's at the point where it should fade out. */
+  sp->pause_duration = 0;
+  sp->duration = 9999;
+  sp->remain_p = False;
+  sp->start_time = ss->now - sp->duration;
+}
+
+static void
+fadeout_sprites (ModeInfo *mi, sprite_type type)
+{
+  esper_state *ss = &sss[MI_SCREEN(mi)];
+  int i;
+  for (i = 0; i < ss->nsprites; i++)
+    {
+      sprite *sp = ss->sprites[i];
+      if (sp->type == type)
+        fadeout_sprite (mi, sp);
+    }
+}
+
+
+static sprite *
+find_newest_sprite (ModeInfo *mi, sprite_type type)
+{
+  esper_state *ss = &sss[MI_SCREEN(mi)];
+  sprite *sp = 0;
+  int i;
+  for (i = 0; i < ss->nsprites; i++)
+    {
+      sprite *sp2 = ss->sprites[i];
+      if (sp2->type == type &&
+          (!sp || 
+           (sp->start_time < sp2->start_time &&
+            ss->now >= sp2->start_time + sp2->pause_duration)))
+        sp = sp2;
+    }
+  return sp;
+}
+
+
+/* Enqueue a text sprite describing the given sprite that runs at the 
+   same time.
+ */
+static sprite *
+push_text_sprite (ModeInfo *mi, sprite *sp)
+{
+  /* esper_state *ss = &sss[MI_SCREEN(mi)]; */
+  sprite *sp2 = new_sprite (mi, TEXT);
+  if (!sp2) abort();
+  sp2->text_id = sp->id;
+  sp2->fade_duration  = sp->fade_duration;
+  sp2->duration       = sp->duration;
+  sp2->pause_duration = sp->pause_duration;
+  return sp2;
+}
+
+
+/* Enqueue a flash sprite that fires at the same time.
+ */
+#ifndef SMOOTH
+static sprite *
+push_flash_sprite (ModeInfo *mi, sprite *sp)
+{
+  /* esper_state *ss = &sss[MI_SCREEN(mi)]; */
+  sprite *sp2 = new_sprite (mi, FLASH);
+  if (!sp2) abort();
+  if (sp->type != IMAGE) abort();
+  sp2->text_id = sp->id;
+  sp2->duration       = MAX (0.07 / speed, 0.07);
+  sp2->fade_duration  = 0;  /* Fading these is too fast to see */
+  sp2->pause_duration = sp->pause_duration + (sp->fade_duration * 0.3);
+  return sp2;
+}
+#endif /* !SMOOTH */
+
+
+/* Set the sprite's duration based on distance travelled.
+ */
+static void
+compute_sprite_duration (ModeInfo *mi, sprite *sp, Bool blink_p)
+{
+  /* Compute max distance traveled by any point (corners or center). */
+  /* (cpp is the devil) */
+# define L(F)    (sp->F.x - sp->F.w/2)   /* delta of left edge, from/to */
+# define R(F) (1-(sp->F.x + sp->F.w/2))  /* right */
+# define B(F)    (sp->F.y - sp->F.h/2)   /* top */
+# define T(F) (1-(sp->F.y + sp->F.h/2))  /* bottom */
+# define D(F,G) sqrt(F(from)*F(from) + G(to)*G(to))  /* corner traveled */
+  double BL = D(B,L);
+  double BR = D(B,R);
+  double TL = D(T,L);
+  double TR = D(T,R);
+  double cx = sp->to.x - sp->from.x;
+  double cy = sp->to.y - sp->from.y;
+  double C  = sqrt(cx*cx + cy*cy);
+  double dist = MAX (BL, MAX (BR, MAX (TL, MAX (TR, C))));
+# undef L
+# undef R
+# undef B
+# undef T
+# undef D
+
+  int steps = 1 + dist * 28;
+  if (steps > 10) steps = 10;
+
+  sp->duration = steps * 0.2 / speed;
+
+# ifndef SMOOTH
+  sp->duration += 1.5 / speed;  /* For linger added by animate_sprite_path() */
+  if (blink_p) sp->duration += 0.6 / speed;
+# endif
+}
+
+
+/* Convert the sprite to a jerky transition.
+   Instead of smoothly animating, move in discrete steps,
+   using multiple staggered sprites.
+ */
+static void
+animate_sprite_path (ModeInfo *mi, sprite *sp, Bool blink_p)
+{
+# ifndef SMOOTH
+  /* esper_state *ss = &sss[MI_SCREEN(mi)]; */
+  double dx = sp->to.x - sp->from.x;
+  double dy = sp->to.y - sp->from.y;
+  double dw = sp->to.w - sp->from.w;
+  double dh = sp->to.h - sp->from.h;
+  double linger  = 1.5 / speed;
+  double blinger = 0.6 / speed;
+  double dur = sp->duration - linger - (blink_p ? blinger : 0);
+  int steps = dur / 0.3 * speed;  /* step duration in seconds */
+  int i;
+
+  if (sp->type == IMAGE)
+    steps *= 0.8;
+
+  if (steps < 2)  steps = 2;
+  if (steps > 10) steps = 10;
+
+  /* if (dur <= 0.01) abort(); */
+  if (dur < 0.01)
+    linger = blinger = 0;
+
+  for (i = 0; i <= steps; i++)
+    {
+      sprite *sp2 = copy_sprite (mi, sp);
+      if (!sp2) abort();
+
+      sp2->to.x = (sp->current.x + i * dx / steps);
+      sp2->to.y = (sp->current.y + i * dy / steps);
+      sp2->to.w = (sp->current.w + i * dw / steps);
+      sp2->to.h = (sp->current.h + i * dh / steps);
+      sp2->current = sp2->from = sp2->to;
+      sp2->duration = dur / steps;
+      sp2->pause_duration += i * sp2->duration;
+      sp2->remain_p = False;
+      sp2->fatbits_p = True;
+
+      if (i == steps)
+        sp2->duration += linger;       /* last one lingers for a bit */
+
+      if (i == steps && !blink_p)
+        {
+          sp2->remain_p = sp->remain_p;
+          sp2->fatbits_p = False;
+        }
+
+      if (sp2->type == IMAGE && i > 0)
+        push_flash_sprite (mi, sp2);
+
+      if (sp2->type == RETICLE || sp2->type == BOX)
+        {
+          sp2 = push_text_sprite (mi, sp2);
+          if (i == steps)
+            sp2->duration += linger * 2;
+        }
+    }
+
+  if (blink_p && blinger)              /* last one blinks before vanishing */
+    {
+      int blinkers = 3;
+      for (i = 1; i <= blinkers; i++)
+        {
+          sprite *sp2 = copy_sprite (mi, sp);
+          if (!sp2) abort();
+
+          sp2->current = sp2->from = sp->to;
+          sp2->duration = blinger / blinkers;
+          sp2->pause_duration += dur + linger + i * sp2->duration;
+          sp2->remain_p = False;
+          if (i == blinkers)
+            {
+              sp2->remain_p = sp->remain_p;
+              sp2->fatbits_p = False;
+            }
+        }
+    }
+
+  /* Fade out the template sprite. It might not have even appeared yet. */
+  fadeout_sprite (mi, sp);
+# endif
+}
+
+
+/* Input rect is of a reticle or box.
+   Output rect is what the image's rect should be so that the only part
+   visible is the part indicated by the input rect.
+ */
+static void
+compute_image_rect (rect *r, sprite *img, Bool inverse_p)
+{
+  double scale = (inverse_p ? 1/r->w : r->w);
+  double dx = r->x - 0.5;
+  double dy = r->y - 0.5;
+
+  /* Adjust size and center by zoom factor */
+  r->w = img->current.w / scale;
+  r->h = img->current.h / scale;
+  r->x = 0.5 + (img->current.x - 0.5) / scale;
+  r->y = 0.5 + (img->current.y - 0.5) / scale;
+
+  /* Move center */
+
+  if (inverse_p)
+    {
+      dx = -dx;                /* #### Close but not quite right */
+      dy = -dy;
+    }
+
+  r->x -= dx / scale;
+  r->y -= dy / scale;
+}
+
+
+/* Sets 'to' such that the image zooms out so that the only part visible
+   is the part indicated by the box.
+ */
+static void
+track_box_with_image (ModeInfo *mi, sprite *sp, sprite *img)
+{
+  rect r = sp->current;
+  compute_image_rect (&r, img, sp->back_p);
+  img->to = r;
+
+  /* Never zoom out too far. */
+  if (img->to.w < 1 && img->to.h < 1)
+    {
+      if (img->to.w > img->to.h)
+        {
+          img->to.w = img->to.w / img->to.h;
+          img->to.h = 1;
+        }
+      else
+        {
+          img->to.h = img->to.h / img->to.w;
+          img->to.w = 1;
+        }
+    }
+
+  /* Never pan beyond the bounds of the image. */
+  if (img->to.x < -img->to.w/2+1) img->to.x = -img->to.w/2+1;
+  if (img->to.x >  img->to.w/2)   img->to.x =  img->to.w/2;
+  if (img->to.y < -img->to.h/2+1) img->to.y = -img->to.h/2+1;
+  if (img->to.y >  img->to.h/2)   img->to.y =  img->to.h/2;
+}
+
+
+static void
+tick_animation (ModeInfo *mi)
+{
+  esper_state *ss = &sss[MI_SCREEN(mi)];
+  anim_state prev_state = ss->anim_state;
+  sprite *sp = 0;
+  int i;
+
+  switch (ss->anim_state) {
+  case BLANK:
+    ss->anim_state = GRID_ON;
+    break;
+  case GRID_ON:
+    ss->anim_state = IMAGE_LOAD;
+    break;
+  case IMAGE_LOAD:
+    /* Only advance once an image has loaded. */
+    if (find_newest_sprite (mi, IMAGE))
+      ss->anim_state = RETICLE_ON;
+    else
+      ss->anim_state = IMAGE_LOAD;
+    break;
+  case RETICLE_ON:
+    ss->anim_state = RETICLE_MOVE;
+    break;
+  case RETICLE_MOVE:
+    if (random() % 6)
+      ss->anim_state = BOX_MOVE;
+    else
+      ss->anim_state = IMAGE_ZOOM;
+    break;
+  case BOX_MOVE:
+    ss->anim_state = IMAGE_ZOOM;
+    break;
+  case IMAGE_ZOOM:
+    {
+      sprite *sp = find_newest_sprite (mi, IMAGE);
+      double depth = (sp
+                      ? MIN (sp->current.w, sp->current.h)
+                      : 0);
+      if (depth > 20)
+        ss->anim_state = IMAGE_UNLOAD;
+      else
+        ss->anim_state = RETICLE_ON;
+    }
+    break;
+  case IMAGE_FORCE_UNLOAD:
+    ss->anim_state = IMAGE_UNLOAD;
+    break;
+  case IMAGE_UNLOAD:
+    ss->anim_state = IMAGE_LOAD;
+    break;
+  case MANUAL_BOX_ON:
+    ss->anim_state = MANUAL_BOX;
+    break;
+  case MANUAL_BOX:
+    break;
+  case MANUAL_RETICLE_ON:
+    ss->anim_state = MANUAL_RETICLE;
+    break;
+  case MANUAL_RETICLE:
+    break;
+  default:
+    abort();
+    break;
+  }
+
+  ss->anim_start = ss->now;
+  ss->anim_duration = 0;
+
+  if (debug_p)
+    fprintf (stderr, "%s: entering %s\n",
+             progname, state_name (ss->anim_state));
+
+  switch (ss->anim_state) {
+
+  case GRID_ON:                /* Start the grid fading in. */
+    if (! find_newest_sprite (mi, GRID))
+      {
+        sp = new_sprite (mi, GRID);
+        if (!sp) abort();
+        sp->fade_duration = 1.0 / speed;
+        sp->duration = 2.0 / speed;
+        sp->remain_p = True;
+        ss->anim_duration = (sp->pause_duration + sp->fade_duration * 2 +
+                             sp->duration);
+      }
+    break;
+
+  case IMAGE_LOAD:
+    fadeout_sprites (mi, IMAGE);
+    sp = new_sprite (mi, IMAGE);
+    if (! sp) 
+      {
+        if (debug_p) fprintf (stderr, "%s: image load failed\n", progname);
+        break;
+      }
+
+    sp->fade_duration = 0.5 / speed;
+    sp->duration = sp->fade_duration * 3;
+    sp->remain_p = True;
+    /* If we zoom in, we lose the pulse at the end. */
+    /* sp->from.w = sp->from.h = 0.0001; */
+    sp->current = sp->from;
+
+    ss->anim_duration = (sp->pause_duration + sp->fade_duration * 2 +
+                         sp->duration);
+
+    sp = push_text_sprite (mi, sp);
+    sp->fade_duration = 0.2 / speed;
+    sp->pause_duration = 0;
+    sp->duration = 2.5 / speed;
+    break;
+
+  case IMAGE_FORCE_UNLOAD:
+    break;
+
+  case IMAGE_UNLOAD:
+    sp = find_newest_sprite (mi, IMAGE);    
+    if (sp)
+      sp->fade_duration = ((prev_state == IMAGE_FORCE_UNLOAD ? 0.2 : 3.0)
+                           / speed);
+    fadeout_sprites (mi, IMAGE);
+    fadeout_sprites (mi, RETICLE);
+    fadeout_sprites (mi, BOX);
+    fadeout_sprites (mi, TEXT);
+    ss->anim_duration = (sp ? sp->fade_duration : 0) + 3.5 / speed;
+    break;
+
+  case RETICLE_ON:             /* Display reticle at center. */
+    fadeout_sprites (mi, TEXT);
+    sp = new_sprite (mi, RETICLE);
+    if (!sp) abort();
+    sp->fade_duration  = 0.2 / speed;
+    sp->pause_duration = 1.0 / speed;
+    sp->duration       = 1.5 / speed;
+    ss->anim_duration = (sp->pause_duration + sp->fade_duration * 2 +
+                         sp->duration);
+    ss->anim_duration -= sp->fade_duration * 2;
+    break;
+
+  case RETICLE_MOVE:
+    /* Reticle has faded in.  Now move it to somewhere else.
+       Create N new reticle sprites, wih staggered pause_durations.
+     */
+    {
+      GLfloat ox = 0.5;
+      GLfloat oy = 0.5;
+      GLfloat nx, ny, dist;
+
+      do {             /* pick a new position not too near the old */
+        nx = 0.3 + BELLRAND(0.4);
+        ny = 0.3 + BELLRAND(0.4);
+        dist = sqrt ((nx-ox)*(nx-ox) + (ny-oy)*(ny-oy));
+      } while (dist < 0.1);
+
+      sp = new_sprite (mi, RETICLE);
+      if (!sp) abort();
+
+      sp->from.x = ox;
+      sp->from.y = oy;
+      sp->current = sp->to = sp->from;
+      sp->to.x = nx;
+      sp->to.y = ny;
+      sp->fade_duration  = 0.2 / speed;
+      sp->pause_duration = 0;
+      compute_sprite_duration (mi, sp, False);
+
+      ss->anim_duration = (sp->pause_duration + sp->fade_duration * 2 +
+                           sp->duration - 0.1);
+      animate_sprite_path (mi, sp, False);
+    }
+    break;
+
+  case BOX_MOVE:
+    /* Reticle has moved, and faded out.
+       Start the box zooming into place.
+     */
+    {
+      GLfloat ox = 0.5;
+      GLfloat oy = 0.5;
+      GLfloat nx, ny;
+      GLfloat z;
+
+      /* Find the last-added reticle, for our destination position. */
+      sp = 0;
+      for (i = 0; i < ss->nsprites; i++)
+        {
+          sprite *sp2 = ss->sprites[i];
+          if (sp2->type == RETICLE &&
+              (!sp || sp->start_time < sp2->start_time))
+            sp = sp2;
+        }
+      if (sp)
+        {
+          nx = sp->to.x;
+          ny = sp->to.y;
+        }
+      else
+        {
+          nx = ny = 0.5;
+          if (debug_p)
+            fprintf (stderr, "%s: no reticle before box?\n", progname);
+        }
+
+      z = 0.3 + frand(0.5);
+
+      /* Ensure that the selected box is contained within the screen */
+      {
+        double margin = 0.005;
+        double maxw = 2 * MIN (1 - margin - nx, nx - margin);
+        double maxh = 2 * MIN (1 - margin - ny, ny - margin);
+        double max = MIN (maxw, maxh);
+        if (z > max) z = max;
+      }
+
+      sp = new_sprite (mi, BOX);
+      if (!sp) abort();
+      sp->from.x = ox;
+      sp->from.y = oy;
+      sp->from.w = 1.0;
+      sp->from.h = 1.0;
+      sp->current = sp->from;
+      sp->to.x = nx;
+      sp->to.y = ny;
+      sp->to.w = z;
+      sp->to.h = z;
+
+      /* Maybe zoom out instead of in.
+       */
+      {
+        sprite *img = find_newest_sprite (mi, IMAGE);
+        double depth = MIN (img->current.w, img->current.h);
+        if (depth > 1 &&  /* if zoomed in */
+            (depth < 6 ?  !(random() % 5) :   /* 20% */
+             depth < 12 ? !(random() % 2) :   /* 50% */
+             (random() % 3)))                 /* 66% */
+          {
+            sp->back_p = True;
+            if (depth < 1.5 && z < 0.8)
+              {
+                z = 0.8;       /* don't zoom out much past 100% */
+                sp->to.w = z;
+                sp->to.h = z;
+              }
+          }
+      }
+
+      sp->fade_duration  = 0.2 / speed;
+      sp->pause_duration = 2.0 / speed;
+      compute_sprite_duration (mi, sp, True);
+      ss->anim_duration = (sp->pause_duration + sp->fade_duration * 2 +
+                           sp->duration - 0.1);
+      animate_sprite_path (mi, sp, True);
+    }
+    break;
+
+  case IMAGE_ZOOM:
+
+    /* Box has moved, and faded out.
+       Or, if no box, then just a reticle.
+       Zoom the underlying image to track the box's position. */
+    {
+      sprite *img, *img2;
+
+      /* Find latest box or reticle, for our destination position. */
+      sp = find_newest_sprite (mi, BOX);
+      if (! sp)
+        sp = find_newest_sprite (mi, RETICLE);
+      if (! sp)
+        {
+          if (debug_p)
+            fprintf (stderr, "%s: no box or reticle before image\n",
+                     progname);
+          break;
+        }
+
+      img = find_newest_sprite (mi, IMAGE);
+      if (!img)
+        {
+          if (debug_p)
+            fprintf (stderr, "%s: no image?\n", progname);
+          break;
+        }
+
+      img2 = copy_sprite (mi, img);
+      if (!img2) abort();
+
+      img2->from = img->current;
+
+      fadeout_sprite (mi, img);
+
+      track_box_with_image (mi, sp, img2);
+
+      img2->fade_duration  = 0.2 / speed;
+      img2->pause_duration = 0.5 / speed;
+      img2->remain_p = True;
+      img2->throb_p = False;
+      compute_sprite_duration (mi, img2, False);
+
+      img->start_time += img2->pause_duration;
+
+      ss->anim_duration = (img2->pause_duration + img2->fade_duration * 2 +
+                           img2->duration);
+      animate_sprite_path (mi, img2, False);
+      fadeout_sprites (mi, TEXT);
+    }
+    break;
+
+  case MANUAL_BOX_ON:
+  case MANUAL_RETICLE_ON:
+    break;
+
+  case MANUAL_BOX:
+  case MANUAL_RETICLE:
+    {
+      sprite_type tt = (ss->anim_state == MANUAL_BOX ? BOX : RETICLE);
+      sprite *osp = find_newest_sprite (mi, tt);
+      fadeout_sprites (mi, RETICLE);
+      fadeout_sprites (mi, BOX);
+
+      sp = new_sprite (mi, tt);
+      if (!sp) abort();
+      if (osp)
+        sp->from = osp->current;
+      else
+        {
+          sp->from.x = 0.5;
+          sp->from.y = 0.5;
+          sp->from.w = 0.5;
+          sp->from.h = 0.5;
+        }
+      sp->to = sp->current = sp->from;
+      sp->fade_duration = 0.2 / speed;
+      sp->duration = 0.2 / speed;
+      sp->remain_p = True;
+      sp->throb_p = False;
+      ss->anim_duration = 9999;
+    }
+    break;
+
+  default:
+    fprintf (stderr,"%s: unknown state %d\n",
+             progname, (int) ss->anim_state);
+    abort();
+  }
+}
+
+
+/* Copied from gltrackball.c, sigh.
+ */
+static void
+adjust_for_device_rotation (double *x, double *y, double *w, double *h)
+{
+  int rot = (int) current_device_rotation();
+  int swap;
+
+  while (rot <= -180) rot += 360;
+  while (rot >   180) rot -= 360;
+
+  if (rot > 135 || rot < -135)         /* 180 */
+    {
+      *x = *w - *x;
+      *y = *h - *y;
+    }
+  else if (rot > 45)                   /* 90 */
+    {
+      swap = *x; *x = *y; *y = swap;
+      swap = *w; *w = *h; *h = swap;
+      *x = *w - *x;
+    }
+  else if (rot < -45)                  /* 270 */
+    {
+      swap = *x; *x = *y; *y = swap;
+      swap = *w; *w = *h; *h = swap;
+      *y = *h - *y;
+    }
+}
+
+
+ENTRYPOINT Bool
+esper_handle_event (ModeInfo *mi, XEvent *event)
+{
+  esper_state *ss = &sss[MI_SCREEN(mi)];
+
+  if (event->xany.type == Expose ||
+      event->xany.type == GraphicsExpose ||
+      event->xany.type == VisibilityNotify)
+    {
+      return False;
+    }
+  else if (event->xany.type == KeyPress)
+    {
+      KeySym keysym;
+      char c = 0;
+      sprite *sp = 0;
+      double delta = 0.025;
+      double margin = 0.005;
+      Bool ok = False;
+
+      XLookupString (&event->xkey, &c, 1, &keysym, 0);
+
+      if (c == '\t')
+        {
+          ss->anim_state = IMAGE_FORCE_UNLOAD;
+          return True;
+        }
+
+      if (! find_newest_sprite (mi, IMAGE))
+        return False;  /* Too early */
+
+      ss->now = double_time();
+
+# define BONK() do {                                 \
+       if (ss->anim_state != MANUAL_BOX_ON &&        \
+           ss->anim_state != MANUAL_BOX) {           \
+         ss->anim_state = MANUAL_BOX_ON;             \
+         tick_animation (mi);                        \
+       }                                             \
+       sp = find_newest_sprite (mi, BOX);            \
+       if (!sp) abort() ;                            \
+       sp->from = sp->current;                       \
+       sp->to = sp->from;                            \
+       sp->start_time = ss->now - sp->fade_duration; \
+       ok = True;                                    \
+      } while(0)
+
+      if (keysym == XK_Left || c == ',' || c == '<')
+        {
+          BONK();
+          sp->to.x -= delta;
+        }
+      else if (keysym == XK_Right || c == '.' || c == '>')
+        {
+          BONK();
+          sp->to.x += delta;
+        }
+      else if (keysym == XK_Down || c == '-')
+        {
+          BONK();
+          sp->to.y -= delta;
+        }
+      else if (keysym == XK_Up || c == '=')
+        {
+          BONK();
+          sp->to.y += delta, ok = True;
+        }
+      else if (keysym == XK_Prior || c == '+')
+        {
+          BONK();
+          sp->to.w += delta;
+          sp->to.h = sp->to.w * sp->from.w / sp->from.h;
+        }
+      else if (keysym == XK_Next || c == '_')
+        {
+          BONK();
+          sp->to.w -= delta;
+          sp->to.h = sp->to.w * sp->from.w / sp->from.h;
+        }
+      else if ((c == ' ' || c == '\t') && debug_p &&
+               ss->anim_state == MANUAL_BOX)
+        {
+          BONK();     /* Null motion: just flash the current image. */
+        }
+      else if ((keysym == XK_Home || c == ' ' || c == '\t') &&
+               ss->anim_state == MANUAL_BOX)
+        {
+          BONK();
+          sp->to.x = 0.5;
+          sp->to.y = 0.5;
+          sp->to.w = 0.5;
+          sp->to.h = 0.5;
+        }
+      else if ((c == '\r' || c == '\n' || c == 033) &&
+               ss->anim_state == MANUAL_BOX)
+        {
+          BONK();
+          ss->anim_state = BOX_MOVE;
+          ss->anim_duration = 9999;
+          ss->anim_start = ss->now - ss->anim_duration;
+          fadeout_sprite (mi, sp);
+          return True;
+        }
+      else
+        return False;
+
+      if (! ok)
+        return False;
+
+      /* Keep it on screen */
+      if (sp->to.w > 1 - margin)
+        {
+          GLfloat r = sp->to.h / sp->to.w;
+          sp->to.w = 1-margin;
+          sp->to.h = (1-margin) * r;
+        }
+      if (sp->to.h > 1)
+        {
+          GLfloat r = sp->to.h / sp->to.w;
+          sp->to.w = (1-margin) / r;
+          sp->to.h = 1-margin;
+        }
+
+      if (sp->to.x - sp->to.w/2 < margin)
+        sp->to.x = sp->to.w/2 + margin;
+      if (sp->to.y - sp->to.h/2 < margin)
+        sp->to.y = sp->to.h/2 + margin;
+
+      if (sp->to.x + sp->to.w/2 >= 1 + margin)
+        sp->to.x = 1 - (sp->to.w/2 + margin);
+      if (sp->to.y + sp->to.h/2 >= 1 + margin)
+        sp->to.y = 1 - (sp->to.h/2 + margin);
+
+      /* Now let's give a momentary glimpse of what the image would do. */
+      if (debug_p)
+        {
+          sprite *img;
+          int i;
+
+          /* Find the lingering image */
+          /* img = find__sprite (mi, IMAGE); */
+          for (i = 0; i < ss->nsprites; i++)
+            {
+              sprite *sp2 = ss->sprites[i];
+              if (sp2->type == IMAGE &&
+                  sp2->remain_p &&
+                  (!img || 
+                   (img->start_time < sp2->start_time &&
+                    ss->now >= sp2->start_time + sp2->pause_duration)))
+                img = sp2;
+            }
+
+          if (!img) abort();
+          img = copy_sprite (mi, img);
+          img->pause_duration = 0;
+          img->fade_duration = 0.1 / speed;
+          img->duration = 0.5 / speed;
+          img->start_time = ss->now;
+          img->remain_p = False;
+          track_box_with_image (mi, sp, img);
+          img->from = img->current = img->to;
+        }
+
+      return True;
+    }
+  else if (event->xany.type == ButtonPress &&
+           event->xbutton.button == Button1)
+    {
+      ss->button_down_p = 1;
+      return True;
+    }
+  else if (event->xany.type == ButtonRelease &&
+           event->xbutton.button == Button1)
+    {
+      ss->button_down_p = 0;
+
+      if (ss->anim_state == MANUAL_BOX)
+        {
+          sprite *sp = find_newest_sprite (mi, BOX);
+          if (sp) fadeout_sprite (mi, sp);
+          ss->anim_state = BOX_MOVE;
+          ss->anim_duration = 9999;
+          ss->anim_start = ss->now - ss->anim_duration;
+        }
+      else if (ss->anim_state == MANUAL_RETICLE)
+        {
+          sprite *sp = find_newest_sprite (mi, RETICLE);
+          if (sp) fadeout_sprite (mi, sp);
+          ss->anim_state = RETICLE_MOVE;
+          ss->anim_duration = 9999;
+          ss->anim_start = ss->now - ss->anim_duration;
+        }
+      return True;
+    }
+  else if (event->xany.type == MotionNotify &&
+           ss->button_down_p &&
+           (ss->anim_state == MANUAL_RETICLE ||
+            ss->anim_state == RETICLE_MOVE))
+    {
+      sprite *sp = 0;
+      double x = event->xmotion.x;
+      double y = event->xmotion.y;
+      double w = MI_WIDTH(mi);
+      double h = MI_HEIGHT(mi);
+
+      adjust_for_device_rotation (&x, &y, &w, &h);
+      x = x/w;
+      y = 1-y/h;
+
+      if (ss->anim_state != MANUAL_RETICLE_ON &&
+          ss->anim_state != MANUAL_RETICLE)
+        {
+         ss->anim_state = MANUAL_RETICLE_ON;
+         tick_animation (mi);
+       }
+      sp = find_newest_sprite (mi, RETICLE);
+      if (!sp) abort();
+      sp->from = sp->current;
+      sp->to = sp->from;
+      sp->start_time = ss->now - sp->fade_duration;
+      sp->remain_p = True;
+
+      sp->current.x = MIN (0.95, MAX (0.05, x));
+      sp->current.y = MIN (0.95, MAX (0.05, y));
+      sp->from = sp->to = sp->current;
+
+      /* Don't update the text sprite more often than once a second. */
+      {
+        sprite *sp2 = find_newest_sprite (mi, TEXT);
+        if (!sp2 || sp2->start_time < ss->now-1)
+          {
+            fadeout_sprites (mi, TEXT);
+            sp = push_text_sprite (mi, sp);
+            sp->remain_p = True;
+          }
+      }
+
+      return True;
+    }
+  else if (event->xany.type == MotionNotify &&
+           ss->button_down_p &&
+           (ss->anim_state == MANUAL_BOX ||
+            ss->anim_state == BOX_MOVE))
+    {
+      sprite *sp = 0;
+      double x = event->xmotion.x;
+      double y = event->xmotion.y;
+      double w = MI_WIDTH(mi);
+      double h = MI_HEIGHT(mi);
+      double max;
+      Bool ok = True;
+
+      adjust_for_device_rotation (&x, &y, &w, &h);
+      x = x/w;
+      y = 1-y/h;
+
+      BONK();
+      max = (2 * (0.5 - MAX (fabs (sp->current.x - 0.5),
+                             fabs (sp->current.y - 0.5)))
+             * 0.95);
+
+      x = fabs (x - sp->current.x);
+      y = fabs (y - sp->current.y);
+
+      if (x > y)
+        sp->current.w = sp->current.h = MIN (max, MAX (0.05, 2*x));
+      else
+        sp->current.w = sp->current.h = MIN (max, MAX (0.05, 2*y));
+      sp->from = sp->to = sp->current;
+
+      /* Don't update the text sprite more often than once a second. */
+      {
+        sprite *sp2 = find_newest_sprite (mi, TEXT);
+        if (!sp2 || sp2->start_time < ss->now-1)
+          {
+            fadeout_sprites (mi, TEXT);
+            sp = push_text_sprite (mi, sp);
+            sp->remain_p = True;
+          }
+      }
+
+      return True;
+    }
+  else if (screenhack_event_helper (MI_DISPLAY(mi), MI_WINDOW(mi), event))
+    {
+      ss->anim_state = IMAGE_FORCE_UNLOAD;
+      return True;
+    }
+# undef BONK
+
+  return False;
+}
+
+
+ENTRYPOINT void
+reshape_esper (ModeInfo *mi, int width, int height)
+{
+  GLfloat s;
+
+  glViewport (0, 0, width, height);
+  glMatrixMode (GL_PROJECTION);
+  glLoadIdentity();
+  glRotatef (current_device_rotation(), 0, 0, 1);
+  glMatrixMode (GL_MODELVIEW);
+  glLoadIdentity();
+
+  s = 2;
+
+  if (debug_p)
+    {
+      s *= 0.75;
+      if (s < 0.1) s = 0.1;
+    }
+
+  glScalef (s, s, s);
+  glTranslatef (-0.5, -0.5, 0);
+
+  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+  /* Stretch each existing image to match new window aspect. */
+  {
+    esper_state *ss = &sss[MI_SCREEN(mi)];
+    int i;
+    for (i = 0; i < ss->nsprites; i++)
+      {
+        sprite *sp = ss->sprites[i];
+        if (sp && sp->type == IMAGE && sp->img && sp->img->loaded_p)
+          {
+            GLfloat sp_asp  = sp->current.h / sp->current.w;
+            GLfloat img_asp = (sp->img->geom.height /
+                               (GLfloat) sp->img->geom.width);
+            GLfloat new_win = (MI_WIDTH(mi) / (double) MI_HEIGHT(mi));
+            GLfloat old_win = sp_asp / img_asp;
+            GLfloat r = old_win / new_win;
+            if (img_asp > 1)
+              {
+                sp->from.h    /= r;
+                sp->current.h /= r;
+                sp->to.h      /= r;
+              }
+            else
+              {
+                sp->from.w    *= r;
+                sp->current.w *= r;
+                sp->to.w      *= r;
+              }
+          }
+      }
+  }
+}
+
+
+static void
+parse_color (ModeInfo *mi, char *key, GLfloat color[4])
+{
+  XColor xcolor;
+  char *string = get_string_resource (mi->dpy, key, "EsperColor");
+  if (!XParseColor (mi->dpy, mi->xgwa.colormap, string, &xcolor))
+    {
+      fprintf (stderr, "%s: unparsable color in %s: %s\n", progname,
+               key, string);
+      exit (1);
+    }
+
+  color[0] = xcolor.red   / 65536.0;
+  color[1] = xcolor.green / 65536.0;
+  color[2] = xcolor.blue  / 65536.0;
+  color[3] = 1;
+}
+
+
+ENTRYPOINT void
+init_esper (ModeInfo *mi)
+{
+  int screen = MI_SCREEN(mi);
+  esper_state *ss;
+  int wire = MI_IS_WIREFRAME(mi);
+  
+  MI_INIT (mi, sss);
+  ss = &sss[screen];
+
+  if ((ss->glx_context = init_GL(mi)) != NULL) {
+    reshape_esper (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
+  } else {
+    MI_CLEARWINDOW(mi);
+  }
+
+  parse_color (mi, "gridColor", ss->grid_color);
+  parse_color (mi, "reticleColor", ss->reticle_color);
+  parse_color (mi, "textColor", ss->text_color);
+
+  glDisable (GL_LIGHTING);
+  glDisable (GL_DEPTH_TEST);
+  glDepthMask (GL_FALSE);
+  glEnable (GL_CULL_FACE);
+  glCullFace (GL_BACK);
+
+  if (! wire)
+    {
+      glEnable (GL_TEXTURE_2D);
+      glShadeModel (GL_SMOOTH);
+      glEnable (GL_BLEND);
+      glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+    }
+
+  ss->font_data = load_texture_font (mi->dpy, "titleFont");
+
+  ss->now = double_time();
+  ss->dawn_of_time = ss->now;
+
+  alloc_image (mi);
+
+  ss->anim_state = BLANK;
+  ss->anim_start = 0;
+  ss->anim_duration = 0;
+}
+
+
+ENTRYPOINT void
+draw_esper (ModeInfo *mi)
+{
+  esper_state *ss = &sss[MI_SCREEN(mi)];
+
+  if (!ss->glx_context)
+    return;
+
+  glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *(ss->glx_context));
+
+  mi->polygon_count = 0;
+
+  ss->now = double_time();
+
+  tick_sprites (mi);
+  draw_sprites (mi);
+  if (ss->now >= ss->anim_start + ss->anim_duration)
+    tick_animation (mi);
+
+  if (mi->fps_p) do_fps (mi);
+
+  glFinish();
+  glXSwapBuffers (MI_DISPLAY (mi), MI_WINDOW(mi));
+}
+
+XSCREENSAVER_MODULE ("Esper", esper)
+
+#endif /* USE_GL */