From http://www.jwz.org/xscreensaver/xscreensaver-5.30.tar.gz
[xscreensaver] / hacks / glx / winduprobot.c
diff --git a/hacks/glx/winduprobot.c b/hacks/glx/winduprobot.c
new file mode 100644 (file)
index 0000000..842a074
--- /dev/null
@@ -0,0 +1,2386 @@
+/* winduprobot, Copyright (c) 2014 Jamie Zawinski <jwz@jwz.org>
+ *
+ * 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.
+ *
+ * Draws a robot wind-up toy.
+ *
+ * I've had this little robot since I was about six years old!  When the time
+ * came for us to throw the Cocktail Robotics Grand Challenge at DNA Lounge, I
+ * photographed this robot (holding a tiny martini glass) to make a flyer for
+ * the event.  You can see that photo here:
+ * http://www.dnalounge.com/flyers/2014/09/14.html
+ *
+ * Then I decided to try and make award statues for the contest by modeling
+ * this robot and 3D-printing it (a robot on a post, with the DNA Lounge
+ * grommet around it.)  So I learned Maya and built a model.
+ *
+ * Well, that 3D printing idea didn't work out, but since I had the model
+ * already, I exported it and turned it into a screen saver.
+ *
+ * The DXF files that Maya exports aren't simple enough for my dxf2gl.pl
+ * script to process, so the exporting process went:
+ *
+ *  - Save from Maya to OBJ;
+ *  - Import OBJ into SketchUp using
+ *    http://www.scriptspot.com/sketchup/scripts/obj-importer
+ *  - Clean up the model a little bit more;
+ *  - Export to DXF with "Millimeters", "Triangles", using
+ *    http://www.guitar-list.com/download-software/convert-sketchup-skp-files-dxf-or-stl
+ */
+
+#define LABEL_FONT "-*-helvetica-bold-r-normal-*-240-*"
+
+#define DEFAULTS       "*delay:        20000       \n" \
+                       "*count:        25          \n" \
+                       "*showFPS:      False       \n" \
+                       "*wireframe:    False       \n" \
+                       "*labelFont:  " LABEL_FONT "\n" \
+                       "*legColor:     #AA2222"   "\n" \
+                       "*armColor:     #AA2222"   "\n" \
+                       "*handColor:    #AA2222"   "\n" \
+                       "*crankColor:   #444444"   "\n" \
+                       "*bodyColor:    #7777AA"   "\n" \
+                       "*domeColor:    #7777AA"   "\n" \
+                       "*insideColor:  #DDDDDD"   "\n" \
+                       "*gearboxColor: #444488"   "\n" \
+                       "*gearColor:    #008877"   "\n" \
+                       "*wheelColor:   #007788"   "\n" \
+                       "*wireColor:    #006600"   "\n" \
+                       "*groundColor:  #0000FF"   "\n" \
+                        "*textColor:       #FFFFFF""\n" \
+                        "*textBackground:  #444444""\n" \
+                        "*textBorderColor: #FFFF88""\n" \
+                        "*textLines:    10          \n" \
+                       "*program:      xscreensaver-text\n" \
+                       "*usePty:       False\n"
+
+#define DEBUG
+#define WORDBUBBLES
+
+# define refresh_robot 0
+#undef countof
+#define countof(x) (sizeof((x))/sizeof((*x)))
+
+#define DEF_SPEED       "1.0"
+#define DEF_ROBOT_SIZE  "1.0"
+#define DEF_TEXTURE     "True"
+#define DEF_FADE        "True"
+#define DEF_OPACITY     "1.0"
+#define DEF_TALK        "0.2"
+
+#include "xlockmore.h"
+#include "gltrackball.h"
+#include "xpm-ximage.h"
+#include "involute.h"
+#include "sphere.h"
+
+#ifdef WORDBUBBLES
+# include "textclient.h"
+# include "glxfonts.h"
+#endif
+
+#include <ctype.h>
+
+#ifndef HAVE_JWZGLES /* No SPHERE_MAP on iPhone */
+# define HAVE_TEXTURE
+#endif
+
+#ifdef HAVE_TEXTURE
+# include "../images/chromesphere.xpm"
+#endif
+
+#ifdef USE_GL /* whole file */
+
+#include "gllist.h"
+
+extern const struct gllist
+  *robot_arm_half, *robot_body_half_outside, *robot_body_half_inside,
+  *robot_crank_full, *robot_gearbox_half, *robot_hand_half,
+  *robot_leg_half, *robot_rotator_half, *robot_wireframe;
+
+static struct gllist *robot_dome = 0, *robot_gear = 0, *ground = 0;
+
+static const struct gllist * const *all_objs[] = {
+  &robot_arm_half, &robot_body_half_outside, &robot_body_half_inside,
+  &robot_crank_full, &robot_gearbox_half, &robot_hand_half,
+  &robot_leg_half, &robot_rotator_half, &robot_wireframe,
+  (const struct gllist * const *) &robot_dome,
+  (const struct gllist * const *) &robot_gear,
+  (const struct gllist * const *) &ground
+};
+
+#define ROBOT_ARM      0
+#define ROBOT_BODY_1   1
+#define ROBOT_BODY_2   2
+#define ROBOT_CRANK    3
+#define ROBOT_GEARBOX  4
+#define ROBOT_HAND     5
+#define ROBOT_LEG      6
+#define ROBOT_ROTATOR  7
+#define ROBOT_WIREFRAME        8
+#define ROBOT_DOME     9
+#define ROBOT_GEAR     10
+#define GROUND         11
+
+typedef struct {
+  GLfloat x, y, z;             /* position */
+  GLfloat facing;              /* direction of front of robot, degrees */
+  GLfloat pitch;               /* front/back tilt angle, degrees */
+  GLfloat roll;                        /* left/right tilt angle, degrees */
+  GLfloat speed;               /* some robots are faster */
+  GLfloat crank_rot;           /* gear state, degrees */
+  GLfloat hand_rot[2];         /* rotation of the hands, degrees */
+  GLfloat hand_pos[2];         /* openness of the hands, ratio */
+  GLfloat balance;             /* how off-true does it walk? degrees */
+  GLfloat body_transparency;   /* ratio */
+  int fading_p;                        /* -1, 0, 1 */
+
+} walker;
+
+typedef struct {
+  GLXContext *glx_context;
+  trackball_state *user_trackball;
+  Bool button_down_p;
+
+  GLuint *dlists;
+  GLfloat component_colors[countof(all_objs)][4];
+
+  int nwalkers;
+  walker *walkers;
+
+  GLfloat looking_x, looking_y, looking_z;     /* Where camera is aimed */
+  GLfloat olooking_x, olooking_y, olooking_z;  /* Where camera was aimed */
+  Bool camera_tracking_p;                      /* Whether camera in motion */
+  GLfloat tracking_ratio;
+
+# ifdef HAVE_TEXTURE
+  GLuint chrome_texture;
+# endif
+
+# ifdef WORDBUBBLES
+  texture_font_data *font_data;
+  int bubble_tick;
+  text_data *tc;
+  char words[10240];
+  int lines, max_lines;
+
+  GLfloat text_color[4], text_bg[4], text_bd[4];
+
+# endif /* WORDBUBBLES */
+
+
+# ifdef DEBUG
+  GLfloat debug_x, debug_y, debug_z;
+# endif
+
+} robot_configuration;
+
+static robot_configuration *bps = NULL;
+
+static GLfloat speed, size, opacity;
+static int do_texture, do_fade;
+#ifdef WORDBUBBLES
+static GLfloat talk_chance;
+#endif
+#ifdef DEBUG
+static int debug_p;
+#endif
+
+static XrmOptionDescRec opts[] = {
+  { "-speed",      ".speed",     XrmoptionSepArg, 0 },
+  { "-size",       ".robotSize", XrmoptionSepArg, 0 },
+  { "-opacity",    ".opacity",   XrmoptionSepArg, 0 },
+  { "-talk",       ".talk",      XrmoptionSepArg, 0 },
+  {"-texture",     ".texture",   XrmoptionNoArg, "True" },
+  {"+texture",     ".texture",   XrmoptionNoArg, "False" },
+  {"-fade",        ".fade",      XrmoptionNoArg, "True" },
+  {"+fade",        ".fade",      XrmoptionNoArg, "False" },
+#ifdef DEBUG
+  {"-debug",       ".debug",     XrmoptionNoArg, "True" },
+  {"+debug",       ".debug",     XrmoptionNoArg, "False" },
+#endif
+};
+
+static argtype vars[] = {
+  {&speed,       "speed",      "Speed",     DEF_SPEED,      t_Float},
+  {&size,        "robotSize",  "RobotSize", DEF_ROBOT_SIZE, t_Float},
+  {&opacity,     "opacity",    "Opacity",   DEF_OPACITY,    t_Float},
+  {&do_texture,  "texture",    "Texture",   DEF_TEXTURE,    t_Bool},
+  {&do_fade,     "fade",       "Fade",      DEF_FADE,       t_Bool},
+#ifdef WORDBUBBLES
+  {&talk_chance, "talk",       "Talk",      DEF_TALK,       t_Float},
+#endif
+#ifdef DEBUG
+  {&debug_p,     "debug",      "Debug",     "False",        t_Bool},
+#endif
+};
+
+ENTRYPOINT ModeSpecOpt robot_opts = {
+  countof(opts), opts, countof(vars), vars, NULL};
+
+
+/* Window management, etc
+ */
+ENTRYPOINT void
+reshape_robot (ModeInfo *mi, int width, int height)
+{
+  GLfloat h = (GLfloat) height / (GLfloat) width;
+
+  glViewport (0, 0, (GLint) width, (GLint) height);
+
+  glMatrixMode(GL_PROJECTION);
+  glLoadIdentity();
+  gluPerspective (40.0, 1/h, 1.0, 250);
+
+  glMatrixMode(GL_MODELVIEW);
+  glLoadIdentity();
+  gluLookAt( 0, 20, 30,
+             0, 0, 0,
+             0, 1, 0);
+
+  glClear(GL_COLOR_BUFFER_BIT);
+
+# ifdef WORDBUBBLES
+  {
+    robot_configuration *bp = &bps[MI_SCREEN(mi)];
+    int w = (width < 800 ? 25 : 40);
+    int h = 1000;
+    if (bp->tc)
+      textclient_reshape (bp->tc, w, h, w, h,
+                          /* Passing bp->max_lines isn't actually necessary */
+                          0);
+  }
+# endif
+
+}
+
+
+ENTRYPOINT Bool
+robot_handle_event (ModeInfo *mi, XEvent *event)
+{
+  robot_configuration *bp = &bps[MI_SCREEN(mi)];
+
+  if (gltrackball_event_handler (event, bp->user_trackball,
+                                 MI_WIDTH (mi), MI_HEIGHT (mi),
+                                 &bp->button_down_p))
+    return True;
+#ifdef DEBUG
+  else if (event->xany.type == KeyPress && debug_p)
+    {
+      KeySym keysym;
+      char c = 0;
+      double n[3] = { 1.0, 0.1, 0.1 };
+      int s = (event->xkey.state & ShiftMask ? 10 : 1);
+
+      XLookupString (&event->xkey, &c, 1, &keysym, 0);
+
+      if      (keysym == XK_Right)   bp->debug_x += n[0] * s;
+      else if (keysym == XK_Left)    bp->debug_x -= n[0] * s;
+      else if (keysym == XK_Up)      bp->debug_y += n[1] * s;
+      else if (keysym == XK_Down)    bp->debug_y -= n[1] * s;
+      else if (c == '=' || c == '+') bp->debug_z += n[2] * s;
+      else if (c == '-' || c == '_') bp->debug_z -= n[2] * s;
+      else if (c == ' ') bp->debug_x = bp->debug_y = bp->debug_z = 0;
+      else if (c == '\n' || c == '\r')
+        fprintf (stderr, "%.4f %.4f %.4f\n", 
+                 bp->debug_x, bp->debug_y, bp->debug_z);
+      else return False;
+      return True;
+    }
+#endif /* DEBUG */
+
+  return False;
+}
+
+
+#ifdef HAVE_TEXTURE
+
+static void
+load_textures (ModeInfo *mi)
+{
+  robot_configuration *bp = &bps[MI_SCREEN(mi)];
+  XImage *xi;
+
+  xi = xpm_to_ximage (mi->dpy, mi->xgwa.visual, mi->xgwa.colormap,
+                      chromesphere_xpm);
+  clear_gl_error();
+
+  glGenTextures (1, &bp->chrome_texture);
+  glBindTexture (GL_TEXTURE_2D, bp->chrome_texture);
+  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+  glPixelStorei (GL_UNPACK_ALIGNMENT, 1);
+  glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA,
+                xi->width, xi->height, 0,
+                GL_RGBA,
+# ifndef USE_IPHONE
+                GL_UNSIGNED_INT_8_8_8_8_REV,
+# else
+                GL_UNSIGNED_BYTE,
+# endif
+                xi->data);
+  check_gl_error("texture");
+
+  glEnable(GL_TEXTURE_GEN_S);
+  glEnable(GL_TEXTURE_GEN_T);
+  glEnable(GL_TEXTURE_2D);
+}
+
+#endif /* HAVE_TEXTURE */
+
+
+static int unit_gear (ModeInfo *, GLfloat color[4]);
+static int draw_ground (ModeInfo *, GLfloat color[4]);
+static void init_walker (ModeInfo *, walker *);
+
+static void
+parse_color (ModeInfo *mi, char *key, GLfloat color[4])
+{
+  XColor xcolor;
+  char *string = get_string_resource (mi->dpy, key, "RobotColor");
+  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_robot (ModeInfo *mi)
+{
+  robot_configuration *bp;
+  int wire = MI_IS_WIREFRAME(mi);
+  int i;
+  if (!bps) {
+    bps = (robot_configuration *)
+      calloc (MI_NUM_SCREENS(mi), sizeof (robot_configuration));
+    if (!bps) {
+      fprintf(stderr, "%s: out of memory\n", progname);
+      exit(1);
+    }
+  }
+
+  bp = &bps[MI_SCREEN(mi)];
+
+  bp->glx_context = init_GL(mi);
+
+  reshape_robot (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
+
+  glShadeModel(GL_SMOOTH);
+
+  glEnable(GL_DEPTH_TEST);
+  glEnable(GL_NORMALIZE);
+  glEnable(GL_CULL_FACE);
+
+  if (!wire)
+    {
+      GLfloat pos[4] = {0.4, 0.2, 0.4, 0.0};
+/*      GLfloat amb[4] = {0.0, 0.0, 0.0, 1.0};*/
+      GLfloat amb[4] = {0.2, 0.2, 0.2, 1.0};
+      GLfloat dif[4] = {1.0, 1.0, 1.0, 1.0};
+      GLfloat spc[4] = {1.0, 1.0, 1.0, 1.0};
+
+      glEnable(GL_LIGHTING);
+      glEnable(GL_LIGHT0);
+      glEnable(GL_DEPTH_TEST);
+      glEnable(GL_CULL_FACE);
+
+      glLightfv(GL_LIGHT0, GL_POSITION, pos);
+      glLightfv(GL_LIGHT0, GL_AMBIENT,  amb);
+      glLightfv(GL_LIGHT0, GL_DIFFUSE,  dif);
+      glLightfv(GL_LIGHT0, GL_SPECULAR, spc);
+
+      glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+    }
+
+# ifdef HAVE_TEXTURE
+  if (!wire && do_texture)
+    load_textures (mi);
+# endif
+
+  bp->user_trackball = gltrackball_init (False);
+
+  bp->dlists = (GLuint *) calloc (countof(all_objs)+1, sizeof(GLuint));
+  for (i = 0; i < countof(all_objs); i++)
+    bp->dlists[i] = glGenLists (1);
+
+  for (i = 0; i < countof(all_objs); i++)
+    {
+      const struct gllist *gll = *all_objs[i];
+      char *key = 0;
+      GLfloat spec1[4] = {1.00, 1.00, 1.00, 1.0};
+      GLfloat spec2[4] = {0.40, 0.40, 0.70, 1.0};
+      GLfloat *spec = spec1;
+      GLfloat shiny = 20;
+
+      glNewList (bp->dlists[i], GL_COMPILE);
+
+      glMatrixMode(GL_MODELVIEW);
+      glPushMatrix();
+      glMatrixMode(GL_TEXTURE);
+      glPushMatrix();
+      glMatrixMode(GL_MODELVIEW);
+
+      glRotatef (-90, 1, 0, 0);
+      glRotatef (180, 0, 0, 1);
+      glScalef (6, 6, 6);
+
+      glBindTexture (GL_TEXTURE_2D, 0);
+
+      switch (i) {
+      case ROBOT_BODY_1:
+        key = "bodyColor";
+        spec = spec1;
+        shiny = 128;
+# ifdef HAVE_TEXTURE
+        if (do_texture)
+          {
+            glBindTexture (GL_TEXTURE_2D, bp->chrome_texture);
+            glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
+            glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
+          }
+# endif
+        break;
+      case ROBOT_DOME:
+        key = "domeColor";
+        spec = spec1;
+        shiny = 128;
+# ifdef HAVE_TEXTURE
+        if (do_texture)
+          {
+            glBindTexture (GL_TEXTURE_2D, bp->chrome_texture);
+            glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
+            glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
+          }
+# endif
+        break;
+      case ROBOT_BODY_2:
+        key = "insideColor";
+        spec = spec2;
+        shiny = 128;
+        break;
+      case ROBOT_ARM:
+        key = "armColor";
+        spec = spec2;
+        shiny = 20;
+        break;
+      case ROBOT_HAND:
+        key = "handColor";
+        spec = spec2;
+        shiny = 20;
+        break;
+      case ROBOT_LEG:
+        key = "legColor";
+        spec = spec2;
+        shiny = 20;
+        break;
+      case ROBOT_CRANK:
+        key = "crankColor";
+        spec = spec2;
+        shiny = 20;
+        break;
+      case ROBOT_ROTATOR:
+        key = "wheelColor";
+        spec = spec2;
+        shiny = 20;
+        break;
+      case ROBOT_GEAR:
+        key = "gearColor";
+        spec = spec2;
+        shiny = 20;
+        break;
+      case ROBOT_GEARBOX:
+        key = "gearboxColor";
+        spec = spec2;
+        shiny = 20;
+        break;
+      case ROBOT_WIREFRAME:
+        key = "wireColor";
+        spec = spec2;
+        shiny = 20;
+        break;
+      case GROUND:
+        key = "groundColor";
+        spec = spec2;
+        shiny = 20;
+        break;
+      default:
+        abort();
+      }
+
+      parse_color (mi, key, bp->component_colors[i]);
+
+      glMaterialfv (GL_FRONT_AND_BACK, GL_SPECULAR,  spec);
+      glMaterialf  (GL_FRONT_AND_BACK, GL_SHININESS, shiny);
+
+      switch (i) {
+      case ROBOT_DOME:
+        if (! robot_dome)
+          robot_dome = (struct gllist *) calloc (1, sizeof(*robot_dome));
+        robot_dome->points = unit_dome (32, 32, MI_IS_WIREFRAME(mi));
+        break;
+      case ROBOT_GEAR:
+        if (! robot_gear)
+          robot_gear = (struct gllist *) calloc (1, sizeof(*robot_gear));
+        robot_gear->points = unit_gear (mi, bp->component_colors[i]);
+        break;
+      case GROUND:
+        if (! ground)
+          ground = (struct gllist *) calloc (1, sizeof(*ground));
+        ground->points = draw_ground (mi, bp->component_colors[i]);
+        break;
+      case ROBOT_WIREFRAME:
+        glLineWidth (0.3);
+        renderList (gll, True);
+        break;
+      default:
+        renderList (gll, wire);
+        /* glColor3f (1, 1, 1); renderListNormals (gll, 100, True); */
+        /* glColor3f (1, 1, 0); renderListNormals (gll, 100, False); */
+        break;
+      }
+
+      glMatrixMode(GL_TEXTURE);
+      glPopMatrix();
+      glMatrixMode(GL_MODELVIEW);
+      glPopMatrix();
+
+      glEndList ();
+    }
+
+# ifdef DEBUG
+  if (debug_p) MI_COUNT(mi) = 1;
+# endif
+
+  bp->nwalkers = MI_COUNT(mi);
+  bp->walkers = (walker *) calloc (bp->nwalkers, sizeof (walker));
+
+  for (i = 0; i < bp->nwalkers; i++)
+    init_walker (mi, &bp->walkers[i]);
+
+  /* Since #0 is the one we track, make sure it doesn't walk too straight.
+   */
+  bp->walkers[0].balance *= 1.5;
+
+# ifdef WORDBUBBLES
+  bp->font_data = load_texture_font (mi->dpy, "labelFont");
+  bp->max_lines = get_integer_resource (mi->dpy, "textLines", "TextLines");
+  bp->tc = textclient_open (MI_DISPLAY (mi));
+
+  parse_color (mi, "textColor", bp->text_color);
+  parse_color (mi, "textBackground", bp->text_bg);
+  parse_color (mi, "textBorderColor", bp->text_bd);
+
+  reshape_robot (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
+
+# endif /* WORDBUBBLES */
+
+  /* Let's tilt the floor a little. */
+# ifdef DEBUG
+  if (!debug_p)
+# endif
+    {
+      gltrackball_start (bp->user_trackball, 0, 500,  1000, 1000);
+      gltrackball_track (bp->user_trackball,
+                         0, 500 + (random() % 200) - 100,
+                         1000, 1000);
+    }
+}
+
+
+static int
+draw_component (ModeInfo *mi, int i)
+{
+  robot_configuration *bp = &bps[MI_SCREEN(mi)];
+  glMaterialfv (GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE,
+                bp->component_colors[i]);
+  glCallList (bp->dlists[i]);
+  return (*all_objs[i])->points / 3;
+}
+
+static int
+draw_transparent_component (ModeInfo *mi, int i, GLfloat alpha)
+{
+  robot_configuration *bp = &bps[MI_SCREEN(mi)];
+  int wire = MI_IS_WIREFRAME(mi);
+  int count = 0;
+
+  if (alpha < 0) return 0;
+  if (alpha > 1) alpha = 1;
+  bp->component_colors[i][3] = alpha;
+
+  if (wire || alpha >= 1)
+    return draw_component (mi, i);
+
+  /* Draw into the depth buffer but not the frame buffer */
+  glColorMask (GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
+  count += draw_component (mi, i);
+
+  /* Now draw into the frame buffer only where there's already depth */
+  glColorMask (GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+  glDepthFunc (GL_EQUAL);
+  glEnable (GL_BLEND);
+  glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+  count += draw_component (mi, i);
+  glDepthFunc (GL_LESS);
+  glDisable (GL_BLEND);
+  return count;
+}
+
+
+
+static int
+draw_arm_half (ModeInfo *mi, walker *f)
+{
+  int count = 0;
+  glPushMatrix();
+  count += draw_transparent_component (mi, ROBOT_ARM, f->body_transparency);
+  glPopMatrix();
+  return count;
+}
+
+static int
+draw_hand_half (ModeInfo *mi, walker *f)
+{
+  int count = 0;
+  glPushMatrix();
+  count += draw_transparent_component (mi, ROBOT_HAND, f->body_transparency);
+  glPopMatrix();
+  return count;
+}
+
+
+/* rotation of arm: 0-360.
+   openness of fingers: 0.0 - 1.0.
+ */
+static int
+draw_arm (ModeInfo *mi, walker *f, GLfloat left_p, GLfloat rot, GLfloat open)
+{
+  int count = 0;
+  GLfloat arm_x = 4766;        /* distance from origin to arm axis */
+  GLfloat arm_y = 12212;
+
+  open *= 5.5;   /* scale of finger range */
+
+# ifdef DEBUG
+  if (debug_p) rot = 0;
+# endif
+
+  glPushMatrix();
+
+  if (! left_p)
+    glTranslatef (0, 0, arm_x * 2);
+
+  glTranslatef (0, arm_y, -arm_x);     /* move to origin */
+  glRotatef (rot, 1, 0, 0);
+  glTranslatef (0, -arm_y, arm_x);     /* move back */
+
+  glFrontFace(GL_CCW);
+  count += draw_arm_half (mi, f);
+
+  glScalef (1, -1, 1);
+  glTranslatef (0, -arm_y * 2, 0);
+  glFrontFace(GL_CW);
+  count += draw_arm_half (mi, f);
+
+  glPushMatrix();
+  glTranslatef (0, 0, -arm_x * 2);
+  glScalef (1, 1, -1);
+  glFrontFace(GL_CCW);
+  count += draw_arm_half (mi, f);
+
+  glScalef (1, -1, 1);
+  glTranslatef (0, -arm_y * 2, 0);
+  glFrontFace(GL_CW);
+  count += draw_arm_half (mi, f);
+  glPopMatrix();
+
+  glTranslatef (0, 0, open);
+  glFrontFace(GL_CW);
+  count += draw_hand_half (mi, f);
+
+  glTranslatef (0, 0, -open);
+  glScalef (1, 1, -1);
+  glTranslatef (0, 0, arm_x * 2);
+  glFrontFace(GL_CCW);
+  glTranslatef (0, 0, open);
+  count += draw_hand_half (mi, f);
+
+  glPopMatrix();
+  return count;
+}
+
+
+static int
+draw_body_half (ModeInfo *mi, walker *f, Bool inside_p)
+{
+  int count = 0;
+  int which = (inside_p ? ROBOT_BODY_2 : ROBOT_BODY_1);
+  glPushMatrix();
+  count += draw_transparent_component (mi, which, f->body_transparency);
+  glPopMatrix();
+  return count;
+}
+
+
+static int
+draw_body (ModeInfo *mi, walker *f, Bool inside_p)
+{
+  int count = 0;
+  glPushMatrix();
+
+  glFrontFace(GL_CCW);
+  count += draw_body_half (mi, f, inside_p);
+
+  glScalef (1, 1, -1);
+  glFrontFace(GL_CW);
+  count += draw_body_half (mi, f, inside_p);
+
+  glPopMatrix();
+
+  return count;
+}
+
+
+static int
+draw_gearbox_half (ModeInfo *mi)
+{
+  int count = 0;
+  glPushMatrix();
+  count += draw_component (mi, ROBOT_GEARBOX);
+  glPopMatrix();
+  return count;
+}
+
+
+static int
+draw_gearbox (ModeInfo *mi)
+{
+  int count = 0;
+  glPushMatrix();
+
+  glFrontFace(GL_CCW);
+  count += draw_gearbox_half (mi);
+
+  glScalef (1, 1, -1);
+  glFrontFace(GL_CW);
+  count += draw_gearbox_half (mi);
+
+  glPopMatrix();
+  return count;
+}
+
+
+static int
+unit_gear (ModeInfo *mi, GLfloat color[4])
+{
+  int wire = MI_IS_WIREFRAME(mi);
+  gear G = { 0, };
+  gear *g = &G;
+
+  g->r          = 0.5;
+  g->nteeth     = 16;
+  g->tooth_h    = 0.12;
+  g->thickness  = 0.32;
+  g->thickness2 = g->thickness * 0.5;
+  g->thickness3 = g->thickness;
+  g->inner_r    = g->r * 0.7;
+  g->inner_r2   = g->r * 0.4;
+  g->inner_r3   = g->r * 0.1;
+  g->size       = INVOLUTE_LARGE;
+
+  g->color[0] = g->color2[0] = color[0];
+  g->color[1] = g->color2[1] = color[1];
+  g->color[2] = g->color2[2] = color[2];
+  g->color[3] = g->color2[3] = color[3];
+
+  return draw_involute_gear (g, wire);
+}
+
+
+static int
+draw_gear (ModeInfo *mi)
+{
+  int count = 0;
+  GLfloat n = 350;
+  glScalef (n, n, n);
+  count += draw_component (mi, ROBOT_GEAR);
+  return count;
+}
+
+
+static int
+draw_crank (ModeInfo *mi, walker *f, GLfloat rot)
+{
+  int count = 0;
+  GLfloat origin = 12210.0;
+
+  rot = -rot;
+
+  glPushMatrix();
+
+  glTranslatef (0, origin, 0);   /* position at origin */
+  glRotatef (rot, 0, 0, 1);
+
+  glPushMatrix();
+  glRotatef (90, 1, 0, 0);
+  count += draw_gear (mi);
+  glPopMatrix();
+
+  glTranslatef (0, -origin, 0);  /* move back */
+
+  glFrontFace(GL_CCW);
+  count += draw_component (mi, ROBOT_CRANK);
+
+  glPopMatrix();
+
+  return count;
+}
+
+
+static int
+draw_rotator_half (ModeInfo *mi)
+{
+  int count = 0;
+  glPushMatrix();
+  count += draw_component (mi, ROBOT_ROTATOR);
+  glPopMatrix();
+  return count;
+}
+
+
+static int
+draw_rotator (ModeInfo *mi, walker *f, GLfloat rot)
+{
+  int count = 0;
+  GLfloat origin = 10093.0;
+
+  glPushMatrix();
+
+  glTranslatef (0, origin, 0);   /* position at origin */
+  glRotatef (rot, 0, 0, 1);
+
+  glPushMatrix();
+  glRotatef (90, 1, 0, 0);
+  count += draw_gear (mi);
+  glPopMatrix();
+
+# ifdef DEBUG
+  if (debug_p)
+    {
+      glDisable(GL_LIGHTING);
+      glBegin(GL_LINES);
+      glVertex3f(0, 0, -3000);
+      glVertex3f(0, 0,  3000);
+      glEnd();
+      if (!MI_IS_WIREFRAME(mi)) glEnable(GL_LIGHTING);
+    }
+# endif
+
+  glTranslatef (0, -origin, 0);   /* move back */
+
+  glFrontFace(GL_CCW);
+  count += draw_rotator_half (mi);
+
+  glScalef (1, 1, -1);
+  glFrontFace(GL_CW);
+  glRotatef (180, 0, 0, 1);
+  glTranslatef (0, -origin * 2, 0);   /* move back */
+  count += draw_rotator_half (mi);
+
+  glPopMatrix();
+  return count;
+}
+
+
+static int
+draw_leg_half (ModeInfo *mi)
+{
+  int count = 0;
+  glPushMatrix();
+  count += draw_component (mi, ROBOT_LEG);
+  glPopMatrix();
+  return count;
+}
+
+
+static int
+draw_wireframe (ModeInfo *mi, walker *f)
+{
+  int wire = MI_IS_WIREFRAME(mi);
+  int count = 0;
+  GLfloat alpha = 0.6 - f->body_transparency;
+  if (alpha < 0) return 0;
+  alpha *= 0.3;
+  if (!wire) glDisable (GL_LIGHTING);
+  glPushMatrix();
+  count += draw_transparent_component (mi, ROBOT_WIREFRAME, alpha);
+  glPopMatrix();
+  if (!wire) glEnable (GL_LIGHTING);
+  return count;
+}
+
+
+static int
+draw_leg (ModeInfo *mi, GLfloat rot, Bool left_p)
+{
+  int count = 0;
+  GLfloat x, y;
+  GLfloat leg_distance = 9401;   /* distance from ground to leg axis */
+  GLfloat rot_distance = 10110;  /* distance from ground to rotator axis */
+  GLfloat pin_distance = 14541;  /* distance from ground to stop pin */
+  GLfloat orbit_r = rot_distance - leg_distance;  /* radius of rotation */
+
+  /* Actually it's the bottom of the pin minus its diameter, or something. */
+  pin_distance -= 590;
+
+  glPushMatrix();
+
+  if (left_p)
+    glRotatef (180, 0, 1, 0);
+
+  if (!left_p) rot = -(rot + 180);
+
+  rot -= 90;
+
+  x = orbit_r * cos (-rot * M_PI / 180);
+  y = orbit_r * sin (-rot * M_PI / 180);
+
+  {
+    /* Rotate the leg by angle B of the right                A
+       triangle ABC, where:                                 /:
+                                                           / :
+       A = position of stop pin                           /  :
+       D = position of rotator wheel's axis            , - ~ ~ ~ - ,
+       C = D + y                                   , '  /    :       ' ,
+       B = D + xy (leg's axis)                   ,     /     :           ,
+                                                ,     /      :            ,
+      So:                                      ,     /       :             ,
+        H = dist(A,B)                          ,    /        D             ,
+        O = dist(A,C)                          ,   /         :             ,
+        sin(th) = O/H                           , /          :            ,
+        th = asin(O/H)                           B ~ ~ ~ ~ ~ C           .
+                                                   ,                  , '
+                                                     ' - , _ _ _ ,  '
+    */
+    GLfloat Ay = pin_distance - leg_distance;
+    GLfloat Cx = 0, Cy = y;
+    GLfloat Bx = x;
+    GLfloat dBC = Cx - Bx;
+    GLfloat dAC = Cy - Ay;
+    GLfloat dAB = sqrt (dBC*dBC + dAC*dAC);
+    GLfloat th = asin (dAC / dAB);
+    rot = th / (M_PI / 180.0);
+    rot += 90;
+    if (dBC > 0) rot = 360-rot;
+  }
+
+  glTranslatef (0, orbit_r, 0);        /* position on rotator axis */
+  glTranslatef (x, y, 0);
+
+  glTranslatef (0, leg_distance, 0);   /* position on leg axis */
+  glRotatef(rot, 0, 0, 1);
+  glTranslatef (0, -leg_distance, 0);  /* move back */
+
+  glFrontFace(GL_CCW);
+  count += draw_leg_half (mi);
+
+  glScalef (-1, 1, 1);
+  glFrontFace(GL_CW);
+  count += draw_leg_half (mi);
+
+  glPopMatrix();
+  return count;
+}
+
+
+static int
+draw_dome (ModeInfo *mi, walker *f)
+{
+  robot_configuration *bp = &bps[MI_SCREEN(mi)];
+  int wire = MI_IS_WIREFRAME(mi);
+  int which = ROBOT_DOME;
+  int count = 0;
+  GLfloat n = 8.3;
+  GLfloat trans = f->body_transparency;
+  GLfloat max = 0.7;
+  GLfloat dome_y = 15290;
+
+  if (trans < 0) trans = 0;
+  if (trans > max) trans = max;
+
+  if (!wire) glEnable (GL_BLEND);
+
+  glPushMatrix();
+  glTranslatef (0, dome_y, 0);
+  glScalef (100, 100, 100);
+  glRotatef (90, 1, 0, 0);
+  glTranslatef (0.35, 0, 0);
+  glScalef (n, n, n);
+  glFrontFace(GL_CCW);
+  bp->component_colors[which][3] = trans;
+  count += draw_component (mi, which);
+  glPopMatrix();
+
+  if (!wire) glDisable (GL_BLEND);
+
+  return count;
+}
+
+
+/* Is this robot overlapping any other?
+ */
+static Bool
+collision_p (ModeInfo *mi, walker *w, GLfloat extra_space)
+{
+  robot_configuration *bp = &bps[MI_SCREEN(mi)];
+  int i;
+  if (MI_COUNT(mi) <= 1) return False;
+
+  for (i = 0; i < MI_COUNT(mi); i++)
+    {
+      walker *w2 = &bp->walkers[i];
+      GLfloat min = 0.75 + extra_space;
+      GLfloat d, dx, dy;
+      if (w == w2) continue;
+      dx = w->x - w2->x;
+      dy = w->y - w2->y;
+      d = (dx*dx + dy*dy);
+      if (d <= min*min) return True;
+    }
+  return False;
+}
+
+
+/* I couldn't figure out a straightforward trig solution to the
+   forward/backward tilting that happens as weight passes from one
+   foot to another, so I just built a tool to eyeball it manually.
+   { vertical_translation, rotation, forward_translation }
+ */
+static const struct { GLfloat up, rot, fwd; } wobble_profile[360] = {
+ {  0.0000,  0.00, 0.0000 },   /* 0 */
+ {  0.0000, -0.01, 0.0025 },
+ {  0.0000, -0.25, 0.0040 },
+ {  0.0000, -0.41, 0.0060 },
+ {  0.0000, -0.62, 0.0080 },
+ {  0.0000, -0.80, 0.0095 },
+ {  0.0000, -0.90, 0.0120 },
+ {  0.0000, -1.10, 0.0135 },
+ {  0.0000, -1.25, 0.0150 },
+ {  0.0000, -1.40, 0.0175 },
+ {  0.0000, -1.50, 0.0195 },   /* 10 */
+ {  0.0000, -1.70, 0.0215 },
+ {  0.0000, -1.80, 0.0230 },
+ {  0.0000, -2.00, 0.0250 },
+ {  0.0000, -2.10, 0.0270 },
+ { -0.0005, -2.30, 0.0290 },
+ { -0.0005, -2.50, 0.0305 },
+ { -0.0005, -2.60, 0.0330 },
+ { -0.0005, -2.70, 0.0330 },
+ { -0.0005, -2.90, 0.0350 },
+ { -0.0005, -3.00, 0.0365 },   /* 20 */
+ { -0.0010, -3.20, 0.0380 },
+ { -0.0005, -3.30, 0.0400 },
+ { -0.0010, -3.50, 0.0420 },
+ { -0.0010, -3.70, 0.0440 },
+ { -0.0010, -3.80, 0.0460 },
+ { -0.0010, -3.90, 0.0470 },
+ { -0.0015, -4.10, 0.0500 },
+ { -0.0015, -4.20, 0.0515 },
+ { -0.0015, -4.40, 0.0535 },
+ { -0.0015, -4.50, 0.0550 },   /* 30 */
+ { -0.0015, -4.60, 0.0565 },
+ { -0.0020, -4.80, 0.0585 },
+ { -0.0020, -5.00, 0.0600 },
+ { -0.0020, -5.10, 0.0620 },
+ { -0.0025, -5.20, 0.0635 },
+ { -0.0025, -5.40, 0.0655 },
+ { -0.0025, -5.50, 0.0675 },
+ { -0.0025, -5.60, 0.0690 },
+ { -0.0030, -5.80, 0.0710 },
+ { -0.0030, -5.90, 0.0720 },   /* 40 */
+ { -0.0030, -6.00, 0.0740 },
+ { -0.0035, -6.10, 0.0760 },
+ { -0.0035, -6.30, 0.0790 },
+ { -0.0040, -6.40, 0.0805 },
+ { -0.0040, -6.50, 0.0820 },
+ { -0.0040, -6.60, 0.0835 },
+ { -0.0045, -6.80, 0.0855 },
+ { -0.0045, -6.90, 0.0870 },
+ { -0.0050, -7.00, 0.0885 },
+ { -0.0050, -7.20, 0.0900 },   /* 50 */
+ { -0.0050, -7.20, 0.0915 },
+ { -0.0055, -7.40, 0.0930 },
+ { -0.0055, -7.50, 0.0945 },
+ { -0.0060, -7.60, 0.0960 },
+ { -0.0060, -7.70, 0.0970 },
+ { -0.0065, -7.80, 0.0985 },
+ { -0.0060, -7.70, 0.0995 },
+ { -0.0060, -7.60, 0.1010 },
+ { -0.0060, -7.50, 0.1020 },
+ { -0.0055, -7.30, 0.1030 },   /* 60 */
+ { -0.0050, -7.10, 0.1040 },
+ { -0.0050, -6.90, 0.1050 },
+ { -0.0045, -6.80, 0.1065 },
+ { -0.0045, -6.50, 0.1075 },
+ { -0.0040, -6.40, 0.1085 },
+ { -0.0040, -6.20, 0.1095 },
+ { -0.0040, -6.00, 0.1105 },
+ { -0.0035, -5.80, 0.1115 },
+ { -0.0030, -5.50, 0.1125 },
+ { -0.0030, -5.40, 0.1135 },   /* 70 */
+ { -0.0030, -5.10, 0.1145 },
+ { -0.0030, -4.90, 0.1150 },
+ { -0.0025, -4.70, 0.1160 },
+ { -0.0025, -4.40, 0.1165 },
+ { -0.0025, -4.20, 0.1175 },
+ { -0.0020, -3.90, 0.1180 },
+ { -0.0020, -3.70, 0.1185 },
+ { -0.0020, -3.40, 0.1190 },
+ { -0.0020, -3.10, 0.1195 },
+ { -0.0020, -2.90, 0.1200 },   /* 80 */
+ { -0.0015, -2.60, 0.1200 },
+ { -0.0015, -2.30, 0.1205 },
+ { -0.0015, -2.00, 0.1210 },
+ { -0.0015, -1.80, 0.1215 },
+ { -0.0015, -1.50, 0.1215 },
+ { -0.0015, -1.20, 0.1215 },
+ { -0.0015, -0.90, 0.1215 },
+ { -0.0015, -0.60, 0.1215 },
+ { -0.0015, -0.30, 0.1215 },
+ { -0.0010,  0.00, 0.1215 },   /* 90 */
+ { -0.0010,  0.30, 0.1215 },
+ { -0.0010,  0.60, 0.1215 },
+ { -0.0010,  0.90, 0.1215 },
+ { -0.0010,  1.20, 0.1215 },
+ { -0.0015,  1.40, 0.1215 },
+ { -0.0015,  1.70, 0.1215 },
+ { -0.0015,  2.00, 0.1215 },
+ { -0.0015,  2.30, 0.1215 },
+ { -0.0015,  2.60, 0.1215 },
+ { -0.0015,  2.80, 0.1220 },   /* 100 */
+ { -0.0020,  3.10, 0.1225 },
+ { -0.0020,  3.30, 0.1230 },
+ { -0.0020,  3.60, 0.1235 },
+ { -0.0020,  3.90, 0.1240 },
+ { -0.0025,  4.10, 0.1245 },
+ { -0.0025,  4.40, 0.1250 },
+ { -0.0025,  4.60, 0.1260 },
+ { -0.0025,  4.90, 0.1265 },
+ { -0.0030,  5.10, 0.1275 },
+ { -0.0030,  5.30, 0.1285 },   /* 110 */
+ { -0.0035,  5.60, 0.1290 },
+ { -0.0035,  5.80, 0.1300 },
+ { -0.0035,  6.00, 0.1310 },
+ { -0.0040,  6.20, 0.1325 },
+ { -0.0040,  6.40, 0.1335 },
+ { -0.0045,  6.60, 0.1345 },
+ { -0.0045,  6.70, 0.1355 },
+ { -0.0050,  6.90, 0.1365 },
+ { -0.0050,  7.10, 0.1375 },
+ { -0.0055,  7.30, 0.1390 },   /* 120 */
+ { -0.0055,  7.40, 0.1400 },
+ { -0.0060,  7.50, 0.1415 },
+ { -0.0065,  8.00, 0.1425 },
+ { -0.0065,  7.80, 0.1440 },
+ { -0.0060,  7.80, 0.1455 },
+ { -0.0060,  7.60, 0.1470 },
+ { -0.0055,  7.50, 0.1485 },
+ { -0.0055,  7.40, 0.1500 },
+ { -0.0050,  7.30, 0.1515 },
+ { -0.0050,  7.20, 0.1530 },   /* 130 */
+ { -0.0050,  7.00, 0.1545 },
+ { -0.0045,  6.90, 0.1560 },
+ { -0.0045,  6.80, 0.1575 },
+ { -0.0040,  6.70, 0.1590 },
+ { -0.0040,  6.50, 0.1605 },
+ { -0.0040,  6.40, 0.1625 },
+ { -0.0035,  6.30, 0.1640 },
+ { -0.0035,  6.10, 0.1655 },
+ { -0.0030,  6.10, 0.1670 },
+ { -0.0030,  5.90, 0.1690 },   /* 140 */
+ { -0.0030,  5.70, 0.1705 },
+ { -0.0025,  5.70, 0.1720 },
+ { -0.0025,  5.50, 0.1740 },
+ { -0.0025,  5.40, 0.1755 },
+ { -0.0025,  5.20, 0.1775 },
+ { -0.0020,  5.10, 0.1790 },
+ { -0.0020,  5.00, 0.1810 },
+ { -0.0020,  4.80, 0.1825 },
+ { -0.0015,  4.70, 0.1840 },
+ { -0.0015,  4.60, 0.1860 },   /* 150 */
+ { -0.0015,  4.40, 0.1880 },
+ { -0.0015,  4.20, 0.1900 },
+ { -0.0015,  4.10, 0.1915 },
+ { -0.0010,  4.00, 0.1935 },
+ { -0.0010,  3.80, 0.1955 },
+ { -0.0010,  3.70, 0.1970 },
+ { -0.0010,  3.50, 0.1990 },
+ { -0.0005,  3.40, 0.2010 },
+ { -0.0010,  3.20, 0.2025 },
+ { -0.0005,  3.10, 0.2045 },   /* 160 */
+ { -0.0005,  2.90, 0.2065 },
+ { -0.0005,  2.80, 0.2085 },
+ { -0.0005,  2.60, 0.2105 },
+ { -0.0005,  2.50, 0.2120 },
+ { -0.0005,  2.30, 0.2140 },
+ { -0.0005,  2.20, 0.2160 },
+ { -0.0005,  2.00, 0.2180 },
+ {  0.0000,  1.90, 0.2200 },
+ {  0.0000,  1.70, 0.2220 },
+ {  0.0000,  1.60, 0.2235 },   /* 170 */
+ {  0.0000,  1.40, 0.2255 },
+ {  0.0000,  1.30, 0.2275 },
+ {  0.0000,  1.10, 0.2295 },
+ {  0.0000,  0.90, 0.2315 },
+ {  0.0000,  0.80, 0.2335 },
+ {  0.0000,  0.60, 0.2355 },
+ {  0.0000,  0.50, 0.2375 },
+ {  0.0000,  0.30, 0.2395 },
+ {  0.0000,  0.10, 0.2415 },
+ {  0.0000,  0.00, 0.2430 },   /* 180 */
+ {  0.0000, -0.10, 0.2450 },
+ {  0.0000, -0.30, 0.2470 },
+ {  0.0000, -0.40, 0.2490 },
+ {  0.0000, -0.60, 0.2510 },
+ {  0.0000, -0.80, 0.2530 },
+ {  0.0000, -0.90, 0.2550 },
+ {  0.0000, -1.10, 0.2570 },
+ {  0.0000, -1.20, 0.2590 },
+ {  0.0000, -1.40, 0.2610 },
+ {  0.0000, -1.50, 0.2625 },   /* 190 */
+ {  0.0000, -1.70, 0.2645 },
+ {  0.0000, -1.80, 0.2665 },
+ { -0.0005, -2.00, 0.2685 },
+ { -0.0005, -2.10, 0.2705 },
+ { -0.0005, -2.30, 0.2725 },
+ { -0.0005, -2.40, 0.2740 },
+ { -0.0005, -2.60, 0.2760 },
+ { -0.0005, -2.80, 0.2780 },
+ { -0.0005, -2.90, 0.2800 },
+ { -0.0005, -3.00, 0.2820 },   /* 200 */
+ { -0.0010, -3.20, 0.2835 },
+ { -0.0005, -3.30, 0.2855 },
+ { -0.0010, -3.50, 0.2875 },
+ { -0.0010, -3.70, 0.2895 },
+ { -0.0010, -3.80, 0.2910 },
+ { -0.0010, -3.90, 0.2930 },
+ { -0.0010, -4.00, 0.2950 },
+ { -0.0015, -4.20, 0.2965 },
+ { -0.0015, -4.40, 0.2985 },
+ { -0.0015, -4.50, 0.3000 },   /* 210 */
+ { -0.0015, -4.60, 0.3020 },
+ { -0.0020, -4.80, 0.3040 },
+ { -0.0020, -5.00, 0.3055 },
+ { -0.0020, -5.00, 0.3075 },
+ { -0.0025, -5.20, 0.3090 },
+ { -0.0025, -5.30, 0.3110 },
+ { -0.0025, -5.50, 0.3125 },
+ { -0.0025, -5.60, 0.3140 },
+ { -0.0030, -5.70, 0.3160 },
+ { -0.0030, -5.90, 0.3175 },   /* 220 */
+ { -0.0030, -6.00, 0.3190 },
+ { -0.0035, -6.10, 0.3210 },
+ { -0.0035, -6.30, 0.3225 },
+ { -0.0040, -6.40, 0.3240 },
+ { -0.0040, -6.50, 0.3255 },
+ { -0.0040, -6.60, 0.3270 },
+ { -0.0045, -6.80, 0.3290 },
+ { -0.0045, -6.90, 0.3305 },
+ { -0.0050, -7.00, 0.3320 },
+ { -0.0050, -7.20, 0.3335 },   /* 230 */
+ { -0.0050, -7.20, 0.3350 },
+ { -0.0055, -7.40, 0.3365 },
+ { -0.0055, -7.50, 0.3380 },
+ { -0.0060, -7.60, 0.3390 },
+ { -0.0060, -7.70, 0.3405 },
+ { -0.0065, -7.80, 0.3420 },
+ { -0.0060, -7.60, 0.3425 },
+ { -0.0060, -7.50, 0.3440 },
+ { -0.0055, -7.40, 0.3455 },
+ { -0.0055, -7.20, 0.3470 },   /* 240 */
+ { -0.0050, -7.10, 0.3480 },
+ { -0.0050, -6.90, 0.3490 },
+ { -0.0045, -6.80, 0.3500 },
+ { -0.0045, -6.50, 0.3510 },
+ { -0.0040, -6.40, 0.3520 },
+ { -0.0040, -6.10, 0.3535 },
+ { -0.0035, -6.00, 0.3545 },
+ { -0.0035, -5.80, 0.3550 },
+ { -0.0030, -5.50, 0.3560 },
+ { -0.0030, -5.30, 0.3570 },   /* 250 */
+ { -0.0030, -5.10, 0.3580 },
+ { -0.0030, -4.90, 0.3585 },
+ { -0.0025, -4.70, 0.3595 },
+ { -0.0025, -4.40, 0.3600 },
+ { -0.0020, -4.10, 0.3610 },
+ { -0.0020, -3.90, 0.3615 },
+ { -0.0020, -3.70, 0.3620 },
+ { -0.0020, -3.30, 0.3625 },
+ { -0.0020, -3.10, 0.3630 },
+ { -0.0015, -2.80, 0.3635 },   /* 260 */
+ { -0.0015, -2.60, 0.3640 },
+ { -0.0015, -2.40, 0.3645 },
+ { -0.0015, -2.00, 0.3645 },
+ { -0.0015, -1.80, 0.3650 },
+ { -0.0015, -1.40, 0.3650 },
+ { -0.0015, -1.20, 0.3655 },
+ { -0.0010, -0.90, 0.3655 },
+ { -0.0010, -0.60, 0.3655 },
+ { -0.0010, -0.30, 0.3655 },
+ { -0.0010,  0.00, 0.3655 },   /* 270 */
+ { -0.0010,  0.30, 0.3655 },
+ { -0.0010,  0.60, 0.3655 },
+ { -0.0010,  0.90, 0.3655 },
+ { -0.0015,  1.10, 0.3655 },
+ { -0.0015,  1.40, 0.3655 },
+ { -0.0015,  1.70, 0.3655 },
+ { -0.0015,  1.90, 0.3660 },
+ { -0.0015,  2.20, 0.3660 },
+ { -0.0015,  2.50, 0.3665 },
+ { -0.0015,  2.80, 0.3670 },   /* 280 */
+ { -0.0015,  3.10, 0.3675 },
+ { -0.0020,  3.40, 0.3680 },
+ { -0.0020,  3.70, 0.3685 },
+ { -0.0020,  3.90, 0.3690 },
+ { -0.0025,  4.10, 0.3695 },
+ { -0.0025,  4.40, 0.3700 },
+ { -0.0025,  4.60, 0.3710 },
+ { -0.0025,  4.80, 0.3715 },
+ { -0.0025,  5.00, 0.3730 },
+ { -0.0030,  5.40, 0.3735 },   /* 290 */
+ { -0.0035,  5.60, 0.3745 },
+ { -0.0035,  5.80, 0.3755 },
+ { -0.0035,  6.00, 0.3765 },
+ { -0.0040,  6.20, 0.3775 },
+ { -0.0045,  6.50, 0.3785 },
+ { -0.0045,  6.60, 0.3795 },
+ { -0.0045,  6.80, 0.3805 },
+ { -0.0050,  7.00, 0.3815 },
+ { -0.0050,  7.10, 0.3825 },
+ { -0.0055,  7.20, 0.3840 },   /* 300 */
+ { -0.0055,  7.40, 0.3850 },
+ { -0.0060,  7.50, 0.3865 },
+ { -0.0060,  7.70, 0.3875 },
+ { -0.0065,  7.80, 0.3890 },
+ { -0.0060,  7.80, 0.3900 },
+ { -0.0060,  7.60, 0.3915 },
+ { -0.0055,  7.60, 0.3930 },
+ { -0.0055,  7.40, 0.3945 },
+ { -0.0050,  7.30, 0.3960 },
+ { -0.0050,  7.20, 0.3975 },   /* 310 */
+ { -0.0050,  7.00, 0.3990 },
+ { -0.0045,  6.90, 0.4005 },
+ { -0.0045,  6.80, 0.4020 },
+ { -0.0040,  6.70, 0.4035 },
+ { -0.0040,  6.60, 0.4050 },
+ { -0.0040,  6.40, 0.4065 },
+ { -0.0035,  6.30, 0.4085 },
+ { -0.0035,  6.20, 0.4100 },
+ { -0.0030,  6.10, 0.4115 },
+ { -0.0030,  5.90, 0.4130 },   /* 320 */
+ { -0.0030,  5.80, 0.4150 },
+ { -0.0025,  5.70, 0.4165 },
+ { -0.0025,  5.50, 0.4180 },
+ { -0.0025,  5.40, 0.4200 },
+ { -0.0025,  5.20, 0.4215 },
+ { -0.0020,  5.10, 0.4235 },
+ { -0.0020,  5.00, 0.4250 },
+ { -0.0020,  4.80, 0.4270 },
+ { -0.0015,  4.70, 0.4285 },
+ { -0.0015,  4.60, 0.4305 },   /* 330 */
+ { -0.0015,  4.40, 0.4325 },
+ { -0.0015,  4.20, 0.4340 },
+ { -0.0015,  4.10, 0.4360 },
+ { -0.0010,  4.00, 0.4375 },
+ { -0.0010,  3.80, 0.4395 },
+ { -0.0010,  3.70, 0.4415 },
+ { -0.0010,  3.50, 0.4435 },
+ { -0.0005,  3.40, 0.4450 },
+ { -0.0010,  3.20, 0.4470 },
+ { -0.0005,  3.10, 0.4490 },   /* 340 */
+ { -0.0005,  2.90, 0.4510 },
+ { -0.0005,  2.80, 0.4525 },
+ { -0.0005,  2.60, 0.4545 },
+ { -0.0005,  2.40, 0.4565 },
+ { -0.0005,  2.30, 0.4585 },
+ { -0.0005,  2.20, 0.4605 },
+ { -0.0005,  2.00, 0.4620 },
+ {  0.0000,  1.90, 0.4640 },
+ {  0.0000,  1.70, 0.4660 },
+ {  0.0000,  1.60, 0.4680 },   /* 350 */
+ {  0.0000,  1.40, 0.4700 },
+ {  0.0000,  1.20, 0.4720 },
+ {  0.0000,  1.10, 0.4740 },
+ {  0.0000,  0.90, 0.4760 },
+ {  0.0000,  0.80, 0.4780 },
+ {  0.0000,  0.60, 0.4795 },
+ {  0.0000,  0.50, 0.4815 },
+ {  0.0000,  0.30, 0.4835 },
+ {  0.0000,  0.20, 0.4855 },   /* 359 */
+};
+
+
+/* Turn the crank by 1 degree, which moves the legs and displaces the robot.
+ */
+static void
+tick_walker (ModeInfo *mi, walker *f)
+{
+  robot_configuration *bp = &bps[MI_SCREEN(mi)];
+  int deg;
+  GLfloat th, fwd;
+
+  if (bp->button_down_p) return;
+
+  f->crank_rot++;
+  deg = ((int) (f->crank_rot + 0.5)) % 360;
+
+# ifdef DEBUG
+  if (debug_p)
+    {
+      f->crank_rot = bp->debug_x;
+      f->pitch = wobble_profile[deg].rot;
+      f->z     = wobble_profile[deg].up;
+    }
+  else
+# endif /* DEBUG */
+
+  if (deg == 0)
+    {
+      fwd      = wobble_profile[deg].fwd;
+      f->pitch = wobble_profile[deg].rot;
+      f->z     = wobble_profile[deg].up;
+    }
+  else
+    {
+      fwd       = (wobble_profile[deg].fwd - wobble_profile[deg-1].fwd);
+      f->pitch += (wobble_profile[deg].rot - wobble_profile[deg-1].rot);
+      f->z     += (wobble_profile[deg].up  - wobble_profile[deg-1].up);
+    }
+
+  /* Lean slightly toward the foot that is raised off the ground. */
+  f->roll = -2.5 * sin ((deg - 90) * M_PI / 180);
+
+  if (!(random() % 10))
+    {
+      GLfloat b = f->balance / 10.0;
+      int s = (f->balance > 0 ? 1 : -1);
+      if (s < 0) b = -b;
+      f->facing += s * frand (b);
+    }
+
+# ifdef DEBUG
+  if (debug_p) fwd = 0;
+# endif
+
+  {
+    GLfloat ox = f->x;
+    GLfloat oy = f->y;
+    th = f->facing * M_PI / 180.0;
+    f->x += fwd * cos (th);
+    f->y += fwd * sin (th);
+
+    /* If moving this robot would collide with another, undo the move,
+       recoil, and randomly turn.
+     */
+    if (collision_p (mi, f, 0))
+      {
+        fwd *= -1.5;
+        f->x = ox + fwd * cos (th);
+        f->y = oy + fwd * sin (th);
+        f->facing += frand(10) - 5;
+        if (! random() % 30)
+          f->facing += frand(90) - 45;
+      }
+  }
+
+
+  if (!do_fade ||
+      opacity > 0.5)   /* Don't bother fading if it's already transparent. */
+    {
+      GLfloat tick = 0.002;
+      GLfloat linger = 3;
+
+      /* If we're not fading, maybe start fading out. */
+      if (f->fading_p == 0 && ! (random() % 40000))
+        f->fading_p = -1;
+
+# ifdef DEBUG
+      if (debug_p) f->fading_p = 0;
+# endif
+
+      if (f->fading_p < 0)
+        {
+          f->body_transparency -= tick;
+          if (f->body_transparency <= -linger)
+            {
+              f->body_transparency = -linger;
+              f->fading_p = 1;
+            }
+        }
+      else if (f->fading_p > 0)
+        {
+          f->body_transparency += tick;
+          if (f->body_transparency >= opacity)
+            {
+              f->body_transparency = opacity;
+              f->fading_p = 0;
+            }
+        }
+    }
+}
+
+
+static void
+init_walker (ModeInfo *mi, walker *f)
+{
+  int i, start_tick = random() % 360;
+
+  f->crank_rot = 0;
+  f->pitch = wobble_profile[0].rot;
+  f->z     = wobble_profile[0].up;
+
+  f->body_transparency = opacity;
+
+  f->hand_rot[0] = frand(180);
+  f->hand_pos[0] = 0.6 + frand(0.4);
+  f->hand_rot[1] = 180 - f->hand_rot[0];
+  f->hand_pos[1] = f->hand_pos[0];
+
+  if (! (random() % 30)) f->hand_rot[1] += frand(10);
+  if (! (random() % 30)) f->hand_pos[1] = 0.6 + frand(0.4);
+
+  f->facing = frand(360);
+  f->balance = frand(10) - 5;
+
+  if (MI_COUNT(mi) == 1)
+    f->speed = 1.0;
+  else
+    f->speed = 0.6 + frand(0.8);
+
+# ifdef DEBUG
+  if (debug_p)
+    {
+      start_tick = 0;
+      f->facing = 0;
+      f->balance = 0;
+    }
+# endif
+
+  for (i = 0; i < start_tick; i++)
+    tick_walker (mi, f);
+
+  /* Place them randomly, but non-overlapping. */
+  for (i = 0; i < 1000; i++)
+    {
+      GLfloat range = 10;
+      if (MI_COUNT(mi) > 10) range += MI_COUNT(mi) / 10.0;
+      f->x = frand(range) - range/2;
+      f->y = frand(range) - range/2;
+      if (! collision_p (mi, f, 1.5))
+        break;
+    }
+
+# ifdef DEBUG
+  if (debug_p) f->x = f->y = 0;
+# endif
+
+}
+
+
+/* Draw a robot standing in the right place, 1 unit tall.
+ */
+static int
+draw_walker (ModeInfo *mi, walker *f)
+{
+  int count = 0;
+  glPushMatrix();
+
+  glTranslatef (f->y, f->z, f->x);
+
+  {
+    GLfloat n = 0.01;
+    glScalef (n, n, n);
+  }
+
+  glRotatef (90, 0, 1, 0);
+  glRotatef (f->facing, 0, 1, 0);
+  glRotatef (f->pitch,  0, 0, 1);
+  glRotatef (f->roll,   1, 0, 0);
+
+  {
+    GLfloat n = 0.00484;          /* make it 1 unit tall */
+    glScalef (n, n, n);
+  }
+
+  count += draw_gearbox (mi);
+  count += draw_crank (mi, f, f->crank_rot);
+  count += draw_rotator (mi, f, f->crank_rot);
+  count += draw_leg (mi, f->crank_rot, False);
+  count += draw_leg (mi, f->crank_rot, True);
+  count += draw_wireframe (mi, f);
+
+  /* Draw these last, and outer shell first, to make transparency work.
+     The order in which things hit the depth buffer matters.
+   */
+ if (f->body_transparency >= 0.001)
+   {
+     count += draw_arm (mi, f, True,  f->hand_rot[0], f->hand_pos[0]);
+     count += draw_arm (mi, f, False, f->hand_rot[1], f->hand_pos[1]);
+     count += draw_body (mi, f, False);
+     count += draw_body (mi, f, True);
+     count += draw_dome (mi, f);
+   }
+
+  glPopMatrix();
+  return count;
+}
+
+
+static int
+draw_ground (ModeInfo *mi, GLfloat color[4])
+{
+  int wire = MI_IS_WIREFRAME(mi);
+  GLfloat i;
+  GLfloat cell_size = 0.9;
+  int cells = 1000 * size;
+  int points = 0;
+
+# ifdef DEBUG
+  if (debug_p) return 0;
+# endif
+
+  glPushMatrix();
+
+  glRotatef (frand(90), 0, 0, 1);
+
+  if (!wire)
+    {
+      GLfloat fog_color[4] = { 0, 0, 0, 1 };
+
+      glLineWidth (2);
+      glEnable (GL_LINE_SMOOTH);
+      glHint (GL_LINE_SMOOTH_HINT, GL_NICEST);
+      glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 
+      glEnable (GL_BLEND);
+
+      glFogi (GL_FOG_MODE, GL_EXP2);
+      glFogfv (GL_FOG_COLOR, fog_color);
+      glFogf (GL_FOG_DENSITY, 0.017);
+      glFogf (GL_FOG_START, -cells/2 * cell_size);
+# ifndef USE_IPHONE  /* #### Not working on iOS for some reason */
+      glEnable (GL_FOG);
+# endif
+    }
+
+  glColor4fv (color);
+  glMaterialfv (GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, color);
+
+  glBegin (GL_LINES);
+  for (i = -cells/2; i < cells/2; i++)
+    {
+      GLfloat a = i * cell_size;
+      GLfloat b = cells/2 * cell_size;
+      glVertex3f (a, -b, 0); glVertex3f (a, b, 0); points++;
+      glVertex3f (-b, a, 0); glVertex3f (b, a, 0); points++;
+    }
+  glEnd();
+
+  if (!wire)
+    {
+      glDisable (GL_LINE_SMOOTH);
+      glDisable (GL_BLEND);
+      glDisable (GL_FOG);
+    }
+
+  glPopMatrix();
+
+  return points;
+}
+
+
+/* If the target robot (robot #0) has moved too far from the point at which
+   the camera is aimed, then initiate an animation to move the observer to
+   the new spot.
+
+   Because of the jerky forward motion of the robots, just always focusing
+   on the center of the robot looks terrible, so instead we let them walk
+   a little out of the center of the frame, and then catch up.
+ */
+static void
+look_at_center (ModeInfo *mi)
+{
+  robot_configuration *bp = &bps[MI_SCREEN(mi)];
+  GLfloat target_x = bp->walkers[0].x;
+  GLfloat target_y = bp->walkers[0].y;
+  GLfloat target_z = 0.8;   /* Look a little bit above his head */
+  GLfloat max_dist = 2.5 / size;
+
+# ifdef DEBUG
+  if (debug_p) return;
+# endif
+
+  if (max_dist < 1)  max_dist = 1;
+  if (max_dist > 10) max_dist = 10;
+
+  if (bp->camera_tracking_p)
+    {
+      GLfloat r = (1 - cos (bp->tracking_ratio * M_PI)) / 2;
+      bp->looking_x = bp->olooking_x + r * (target_x - bp->olooking_x);
+      bp->looking_y = bp->olooking_y + r * (target_y - bp->olooking_y);
+      bp->looking_z = bp->olooking_z + r * (target_z - bp->olooking_z);
+
+      bp->tracking_ratio += 0.02;
+      if (bp->tracking_ratio >= 1)
+        {
+          bp->camera_tracking_p = False;
+          bp->olooking_x = bp->looking_x;
+          bp->olooking_y = bp->looking_y;
+          bp->olooking_z = bp->looking_z;
+        }
+    }
+          
+  if (! bp->camera_tracking_p)
+    {
+      GLfloat dist =
+        sqrt ((target_x - bp->looking_x) * (target_x - bp->looking_x) +
+              (target_y - bp->looking_y) * (target_y - bp->looking_y) +
+              (target_z - bp->looking_z) * (target_z - bp->looking_z));
+
+      if (dist > max_dist)
+        {
+          bp->camera_tracking_p = True;
+          bp->tracking_ratio = 0;
+          bp->olooking_x = bp->looking_x;
+          bp->olooking_y = bp->looking_y;
+          bp->olooking_z = bp->looking_z;
+        }
+    }
+
+  glTranslatef (-bp->looking_y, -bp->looking_z, -bp->looking_x);
+
+# if 0 /* DEBUG */
+  {
+    GLfloat th;
+    glPushMatrix();
+    glColor3f(1, 0, 0);
+    glTranslatef (target_y, target_z, target_x);
+    glBegin(GL_LINES);
+    glVertex3f(0, -target_z, 0);
+    glVertex3f(0, 1, 0);
+    glVertex3f(-0.1, 0, -0.1);
+    glVertex3f( 0.1, 0,  0.1);
+    glVertex3f(-0.1, 0,  0.1);
+    glVertex3f( 0.1, 0, -0.1);
+    glEnd();
+    glPopMatrix();
+
+    glPushMatrix();
+    glColor3f(0, 1, 0);
+    glTranslatef (bp->looking_y, bp->looking_z, bp->looking_x);
+    glRotatef (30, 0, 1, 0);
+    glBegin(GL_LINES);
+    glVertex3f(0, -bp->looking_z, 0);
+    glVertex3f(0, 1, 0);
+    glVertex3f(-0.1, 0, -0.1);
+    glVertex3f( 0.1, 0,  0.1);
+    glVertex3f(-0.1, 0,  0.1);
+    glVertex3f( 0.1, 0, -0.1);
+    glEnd();
+    glPopMatrix();
+
+    glPushMatrix();
+    glColor3f(0, 0, 1);
+    glTranslatef (bp->olooking_y, bp->olooking_z, bp->olooking_x);
+    glRotatef (60, 0, 1, 0);
+    glBegin(GL_LINES);
+    glVertex3f(0, -bp->olooking_z, 0);
+    glVertex3f(0, 1, 0);
+    glVertex3f(-0.1, 0, -0.1);
+    glVertex3f( 0.1, 0,  0.1);
+    glVertex3f(-0.1, 0,  0.1);
+    glVertex3f( 0.1, 0, -0.1);
+    glEnd();
+
+    glTranslatef (0, -bp->olooking_z, 0);
+    glBegin (GL_LINE_LOOP);
+    for (th = 0; th < M_PI * 2; th += 0.1)
+      glVertex3f (bp->olooking_y + max_dist * cos(th), 0,
+                  bp->olooking_x + max_dist * sin(th));
+    glEnd();
+    glPopMatrix();
+  }
+# endif /* DEBUG */
+}
+
+
+#ifdef WORDBUBBLES
+
+/* Draw a cartoony word bubble.
+   W and H are the inside size, for text.
+   Origin is at bottom left.
+   The bubble frame and arrow are outside that.
+ */
+static void
+draw_bubble_box (ModeInfo *mi,
+                 GLfloat width, GLfloat height,
+                 GLfloat corner_radius,
+                 GLfloat arrow_h, GLfloat arrow_x,
+                 GLfloat fg[4], GLfloat bg[4])
+{
+
+# define CORNER_POINTS 16
+  GLfloat outline_points[ (CORNER_POINTS + 2) * 4 + 8 ][3];
+  int i = 0;
+  GLfloat th;
+  GLfloat tick = M_PI / 2 / CORNER_POINTS;
+
+  GLfloat arrow_w = arrow_h / 2;
+  GLfloat arrow_x2 = MAX(0, MIN(width - arrow_w, arrow_x));
+
+  GLfloat w2 = MAX(arrow_w, width  - corner_radius * 1.10);
+  GLfloat h2 = MAX(0,       height - corner_radius * 1.28);
+  GLfloat x2 = (width  - w2) / 2;
+  GLfloat y2 = (height - h2) / 2;
+                                       /*        A  B         C   D    */
+  GLfloat xa = x2 -corner_radius;      /*    E     _------------_      */
+  GLfloat xb = x2;                     /*    D   /__|         |__\     */
+  GLfloat xc = xb + w2;                        /*        |  |         |  |     */
+  GLfloat xd = xc + corner_radius;     /*    C   |__|   EF    |__|     */
+  GLfloat xe = xb + arrow_x2;          /*    B    \_|_________|_/      */
+  GLfloat xf = xe + arrow_w;           /*    A          \|             */
+
+  GLfloat ya = y2 - (corner_radius + arrow_h);
+  GLfloat yb = y2 - corner_radius;
+  GLfloat yc = y2;
+  GLfloat yd = yc + h2;
+  GLfloat ye = yd + corner_radius;
+
+  GLfloat z = 0;
+
+  /* Let the lines take precedence over the fills. */
+  glEnable (GL_POLYGON_OFFSET_FILL);
+  glPolygonOffset (1.0, 1.0);
+
+  glColor4fv (bg);
+  glFrontFace(GL_CW);
+
+  /* top left corner */
+
+  glBegin (GL_TRIANGLE_FAN);
+  glVertex3f (xb, yd, 0);
+  for (th = 0; th < M_PI/2 + tick; th += tick)
+    {
+      GLfloat x = xb - corner_radius * cos(th);
+      GLfloat y = yd + corner_radius * sin(th);
+      glVertex3f (x, y, z);
+      outline_points[i][0] = x;
+      outline_points[i][1] = y;
+      outline_points[i][2] = z;
+      i++;
+    }
+  glEnd();
+
+  /* top edge */
+  outline_points[i][0] = xc;
+  outline_points[i][1] = ye;
+  outline_points[i][2] = z;
+  i++;
+
+  /* top right corner */
+
+  glBegin (GL_TRIANGLE_FAN);
+  glVertex3f (xc, yd, 0);
+  for (th = M_PI/2; th > -tick; th -= tick)
+    {
+      GLfloat x = xc + corner_radius * cos(th);
+      GLfloat y = yd + corner_radius * sin(th);
+      glVertex3f (x, y, z);
+      outline_points[i][0] = x;
+      outline_points[i][1] = y;
+      outline_points[i][2] = z;
+      i++;
+    }
+  glEnd();
+
+  /* right edge */
+  outline_points[i][0] = xd;
+  outline_points[i][1] = yc;
+  outline_points[i][2] = z;
+  i++;
+
+  /* bottom right corner */
+
+  glBegin (GL_TRIANGLE_FAN);
+  glVertex3f (xc, yc, 0);
+  for (th = 0; th < M_PI/2 + tick; th += tick)
+    {
+      GLfloat x = xc + corner_radius * cos(th);
+      GLfloat y = yc - corner_radius * sin(th);
+      glVertex3f (x, y, z);
+      outline_points[i][0] = x;
+      outline_points[i][1] = y;
+      outline_points[i][2] = z;
+      i++;
+    }
+  glEnd();
+
+  /* bottom right edge */
+  outline_points[i][0] = xf;
+  outline_points[i][1] = yb;
+  outline_points[i][2] = z;
+  i++;
+
+  /* arrow triangle */
+  glFrontFace(GL_CW);
+  glBegin (GL_TRIANGLES);
+
+  /* bottom arrow point */
+  outline_points[i][0] = xf;
+  outline_points[i][1] = yb;
+  outline_points[i][2] = z;
+  glVertex3f (outline_points[i][0],
+              outline_points[i][1],
+              outline_points[i][2]);
+  i++;
+
+  /* bottom right edge */
+  outline_points[i][0] = xf;
+  outline_points[i][1] = ya;
+  outline_points[i][2] = z;
+  glVertex3f (outline_points[i][0],
+              outline_points[i][1],
+              outline_points[i][2]);
+  i++;
+
+  outline_points[i][0] = xe;
+  outline_points[i][1] = yb;
+  outline_points[i][2] = z;
+  glVertex3f (outline_points[i][0],
+              outline_points[i][1],
+              outline_points[i][2]);
+  i++;
+  glEnd();
+
+
+  /* bottom left corner */
+
+  glBegin (GL_TRIANGLE_FAN);
+  glVertex3f (xb, yc, 0);
+  for (th = M_PI/2; th > -tick; th -= tick)
+    {
+      GLfloat x = xb - corner_radius * cos(th);
+      GLfloat y = yc - corner_radius * sin(th);
+      glVertex3f (x, y, z);
+      outline_points[i][0] = x;
+      outline_points[i][1] = y;
+      outline_points[i][2] = z;
+      i++;
+    }
+  glEnd();
+
+  glFrontFace(GL_CCW);
+
+  /* left edge */
+  outline_points[i][0] = xa;
+  outline_points[i][1] = yd;
+  outline_points[i][2] = z;
+  i++;
+
+  glFrontFace(GL_CW);
+  glBegin (GL_QUADS);
+  /* left box */
+  glVertex3f (xa, yd, z);
+  glVertex3f (xb, yd, z);
+  glVertex3f (xb, yc, z);
+  glVertex3f (xa, yc, z);
+
+  /* center box */
+  glVertex3f (xb, ye, z);
+  glVertex3f (xc, ye, z);
+  glVertex3f (xc, yb, z);
+  glVertex3f (xb, yb, z);
+
+  /* right box */
+  glVertex3f (xc, yd, z);
+  glVertex3f (xd, yd, z);
+  glVertex3f (xd, yc, z);
+  glVertex3f (xc, yc, z);
+
+  glEnd();
+
+  glLineWidth (2.8);
+  glColor4fv (fg);
+
+  glBegin (GL_LINE_LOOP);
+  while (i > 0)
+    glVertex3fv (outline_points[--i]);
+  glEnd();
+
+  glDisable (GL_POLYGON_OFFSET_FILL);
+}
+
+
+static void
+draw_label (ModeInfo *mi, walker *f, GLfloat y_off, GLfloat scale,
+            const char *label)
+{
+  robot_configuration *bp = &bps[MI_SCREEN(mi)];
+  int wire = MI_IS_WIREFRAME(mi);
+  GLfloat m[4][4];
+
+  if (scale == 0) return;
+
+  if (!wire)
+    glDisable (GL_LIGHTING);   /* don't light fonts */
+
+  glPushMatrix();
+
+  /* First, we translate the origin to the center of the robot.
+
+     Then we retrieve the prevailing modelview matrix, which
+     includes any rotation, wandering, and user-trackball-rolling
+     of the scene.
+
+     We set the top 3x3 cells of that matrix to be the identity
+     matrix.  This removes all rotation from the matrix, while
+     leaving the translation alone.  This has the effect of
+     leaving the prevailing coordinate system perpendicular to
+     the camera view: were we to draw a square face, it would
+     be in the plane of the screen.
+
+     Now we translate by `size' toward the viewer -- so that the
+     origin is *just in front* of the ball.
+
+     Then we draw the label text, allowing the depth buffer to
+     do its work: that way, labels on atoms will be occluded
+     properly when other atoms move in front of them.
+
+     This technique (of neutralizing rotation relative to the
+     observer, after both rotations and translations have been
+     applied) is known as "billboarding".
+   */
+
+  if (f)
+    glTranslatef(f->y, 0, f->x);                   /* get matrix */
+
+  glTranslatef (0, y_off, 0);
+
+  glGetFloatv (GL_MODELVIEW_MATRIX, &m[0][0]);  /* load rot. identity */
+  m[0][0] = 1; m[1][0] = 0; m[2][0] = 0;
+  m[0][1] = 0; m[1][1] = 1; m[2][1] = 0;
+  m[0][2] = 0; m[1][2] = 0; m[2][2] = 1;
+  glLoadIdentity();                             /* reset modelview */
+  glMultMatrixf (&m[0][0]);                     /* replace with ours */
+
+  glTranslatef (0, 0, 0.1);                    /* move toward camera */
+
+  glRotatef (current_device_rotation(), 0, 0, 1);  /* right side up */
+
+  {
+    int cw, ch, w, h;
+    GLfloat s;
+    GLfloat max = 24;   /* max point size to avoid pixellated text */
+
+    /* Let the font be much larger on iPhone */
+    if (mi->xgwa.height <= 640 || mi->xgwa.width <= 640)
+      max *= 3;
+    
+    cw = texture_string_width (bp->font_data, "X", &ch);  /* line height */
+    s = 1.0 / ch;
+    if (ch > max) s *= max/ch;
+
+    s *= scale;
+
+    w = texture_string_width (bp->font_data, label, &h);
+
+    glScalef (s, s, 1);
+    glTranslatef (-w/2, h*2/3 + (cw * 7), 0);
+
+    glPushMatrix();
+    glTranslatef (0, -h - ch/4, -0.1);
+    draw_bubble_box (mi, w, h, 
+                     ch * 2,           /* corner radius */
+                     ch * 2.5,         /* arrow height */
+                     w / 2 - cw * 8,   /* arrow x */
+                     bp->text_bd, bp->text_bg);
+    glPopMatrix();
+
+    glColor4fv (bp->text_color);
+    print_gl_string (mi->dpy, bp->font_data,
+                     0, 0, 0, 0,
+                     label, False);
+  }
+
+  glPopMatrix();
+
+  /* More efficient to always call glEnable() with correct values
+     than to call glPushAttrib()/glPopAttrib(), since reading
+     attributes from GL does a round-trip and  stalls the pipeline.
+   */
+  if (!wire)
+    glEnable (GL_LIGHTING);
+}
+
+
+static void
+fill_words (ModeInfo *mi)
+{
+  robot_configuration *bp = &bps[MI_SCREEN(mi)];
+  char *p = bp->words + strlen(bp->words);
+  char *c;
+  int lines = 0;
+  int max = bp->max_lines;
+
+  /* Fewer lines on iPhone */
+  if ((mi->xgwa.height <= 640 || mi->xgwa.width <= 640) &&
+      max > 4)
+    max = 4;
+
+  for (c = bp->words; c < p; c++)
+    if (*c == '\n')
+      lines++;
+
+  while (p < bp->words + sizeof(bp->words) - 1 &&
+         lines < max)
+    {
+      int c = textclient_getc (bp->tc);
+      if (c == '\n')
+        lines++;
+      if (c > 0)
+        *p++ = (char) c;
+      else
+        break;
+    }
+  *p = 0;
+
+  bp->lines = lines;
+}
+
+
+static void
+bubble (ModeInfo *mi)
+{
+  robot_configuration *bp = &bps[MI_SCREEN(mi)];
+  int duration = 200;
+  GLfloat fade = 0.015;
+  int chance = (talk_chance <= 0.0  ? 0 :
+                talk_chance >= 0.99 ? 1 :
+                (1-talk_chance) * 1000);
+  GLfloat scale;
+  char *s0 = strdup (bp->words);
+  char *s = s0;
+  int L;
+
+  while (*s == '\n') s++;
+  L = strlen(s);
+  while (L > 0 && (s[L-1] == '\n' || s[L-1] == ' ' || s[L-1] == '\t'))
+    s[--L] = 0;
+  if (! *s) goto DONE;
+
+# ifdef DEBUG
+  if (debug_p) goto DONE;
+# endif
+
+  if (chance <= 0) goto DONE;
+
+  if (bp->bubble_tick > 0)
+    {
+      bp->bubble_tick--;
+      if (! bp->bubble_tick)
+        *bp->words = 0;
+    }
+
+  if (! bp->bubble_tick)
+    {
+      if (!(random() % chance))
+        bp->bubble_tick = duration;
+      else
+        goto DONE;
+    }
+
+  scale = (bp->bubble_tick < duration * fade
+           ? bp->bubble_tick / (duration * fade)
+           : (bp->bubble_tick > duration * (1 - fade)
+              ? 1 - ((bp->bubble_tick - duration * (1 - fade))
+                     / (duration * fade))
+              : 1));
+
+  draw_label (mi, &bp->walkers[0], 1.5, scale, s);
+
+ DONE:
+  free (s0);
+}
+#endif /* WORDBUBBLES */
+
+
+
+ENTRYPOINT void
+draw_robot (ModeInfo *mi)
+{
+  robot_configuration *bp = &bps[MI_SCREEN(mi)];
+  Display *dpy = MI_DISPLAY(mi);
+  Window window = MI_WINDOW(mi);
+  GLfloat robot_size;
+  int i;
+
+  if (!bp->glx_context)
+    return;
+
+  glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *(bp->glx_context));
+
+  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+  glPushMatrix ();
+  glRotatef(current_device_rotation(), 0, 0, 1);
+  gltrackball_rotate (bp->user_trackball);
+
+  robot_size = size * 7;
+  glScalef (robot_size, robot_size, robot_size);
+  look_at_center (mi);
+
+  glPushMatrix();
+  glScalef (1/robot_size, 1/robot_size, 1/robot_size);
+  glCallList (bp->dlists[GROUND]);
+  glPopMatrix();
+
+# ifdef WORDBUBBLES
+  fill_words (mi);
+  bubble (mi);
+# endif
+
+#ifdef DEBUG
+  if (debug_p)
+    {
+      glTranslatef (0, -1.2, 0);
+      glScalef (3, 3, 3);
+      glRotatef (-43.5, 0, 0, 1);
+      glRotatef (-90, 0, 1, 0);
+
+      /* glTranslatef (bp->debug_z, bp->debug_y, 0); */
+
+      glPushMatrix();
+      if (!MI_IS_WIREFRAME(mi)) glDisable(GL_LIGHTING);
+      if (!MI_IS_WIREFRAME(mi) && do_texture) glDisable(GL_TEXTURE_2D);
+
+      glBegin(GL_LINES);
+      glVertex3f(-10, 0, 0); glVertex3f(10, 0, 0);
+      glVertex3f(0, -10, 0); glVertex3f(0, 10, 0);
+      glVertex3f(0, 0, -10); glVertex3f(0, 0, 10);
+      glEnd();
+
+      glTranslatef (-0.5, 0, -0.5);
+
+      glColor3f (1, 0, 0);
+      glBegin (GL_LINE_LOOP);
+      glVertex3f (0, 0, 0); glVertex3f (0, 0, 1);
+      glVertex3f (0, 1, 1); glVertex3f (0, 1, 0);
+      glEnd();
+
+      glBegin (GL_LINE_LOOP);
+      glVertex3f (1, 0, 0); glVertex3f (1, 0, 1);
+      glVertex3f (1, 1, 1); glVertex3f (1, 1, 0);
+      glEnd();
+
+# if 1
+      glColor3f (0.5, 0.5, 0.5);
+      glFrontFace (GL_CCW);
+      glBegin (GL_QUADS);
+      /* glVertex3f (0, 1, 0); glVertex3f (0, 1, 1); */
+      /* glVertex3f (1, 1, 1); glVertex3f (1, 1, 0); */
+      glVertex3f (0, 0, 0); glVertex3f (0, 0, 1);
+      glVertex3f (1, 0, 1); glVertex3f (1, 0, 0);
+      glEnd();
+
+      glFrontFace (GL_CW);
+      glBegin (GL_QUADS);
+      glVertex3f (0, 1, 0); glVertex3f (0, 1, 1);
+      glVertex3f (1, 1, 1); glVertex3f (1, 1, 0);
+      glVertex3f (0, 0, 0); glVertex3f (0, 0, 1);
+      glVertex3f (1, 0, 1); glVertex3f (1, 0, 0);
+      glEnd();
+# endif
+
+      glColor3f (1, 0, 0);
+      glBegin (GL_LINES);
+      glVertex3f (0, 0, 0); glVertex3f (1, 0, 0);
+      glVertex3f (0, 0, 1); glVertex3f (1, 0, 1);
+      glVertex3f (0, 1, 0); glVertex3f (1, 1, 0);
+      glVertex3f (0, 1, 1); glVertex3f (1, 1, 1);
+
+      glVertex3f (0, 0, 0); glVertex3f (1, 0, 1);
+      glVertex3f (0, 0, 1); glVertex3f (1, 0, 0);
+      glVertex3f (0, 1, 0); glVertex3f (1, 1, 1);
+      glVertex3f (0, 1, 1); glVertex3f (1, 1, 0);
+      glEnd();
+
+      if (!MI_IS_WIREFRAME(mi)) glEnable(GL_LIGHTING);
+      if (!MI_IS_WIREFRAME(mi) && do_texture) glEnable(GL_TEXTURE_2D);
+
+      glPopMatrix();
+    }
+
+# endif /* DEBUG */
+
+  mi->polygon_count = 0;
+  for (i = 0; i < bp->nwalkers; i++)
+    {
+      walker *f = &bp->walkers[i];
+      int i, ticks = 22 * speed * f->speed;
+      int max = 180;
+
+      if (ticks < 1) ticks = 1;
+      if (ticks > max) ticks = max;
+
+      mi->polygon_count += draw_walker (mi, f);
+
+# ifdef DEBUG
+      if (debug_p)
+        {
+          char s[1024];
+          sprintf (s, "%.4f, %.4f,  %.4f",
+                   bp->debug_x, bp->debug_y, bp->debug_z);
+          glColor3f (1, 1, 1);
+          draw_label (mi, f, -0.3, 1, s);
+        }
+# endif /* DEBUG */
+
+      for (i = 0; i < ticks; i++)
+        tick_walker (mi, f);
+    }
+
+
+  glPopMatrix ();
+
+  if (mi->fps_p) do_fps (mi);
+  glFinish();
+
+  glXSwapBuffers(dpy, window);
+}
+
+ENTRYPOINT void
+release_robot (ModeInfo *mi)
+{
+# ifdef WORDBUBBLES
+  robot_configuration *bp = &bps[MI_SCREEN(mi)];
+  textclient_close (bp->tc);
+# endif
+}
+
+XSCREENSAVER_MODULE_2 ("WindupRobot", winduprobot, robot)
+
+#endif /* USE_GL */