From http://www.jwz.org/xscreensaver/xscreensaver-5.33.tar.gz
[xscreensaver] / hacks / glx / splitflap.c
diff --git a/hacks/glx/splitflap.c b/hacks/glx/splitflap.c
new file mode 100644 (file)
index 0000000..8a0df74
--- /dev/null
@@ -0,0 +1,1406 @@
+/* splitflap, Copyright (c) 2015 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 split-flap text display.
+ */
+
+#define FLAP_FONT "-*-helvetica-bold-r-normal-*-*-1440-*-*-*-*-*-*"
+
+#define DEFAULTS       "*delay:        20000       \n" \
+                       "*showFPS:      False       \n" \
+                       "*wireframe:    False       \n" \
+                       "*flapFont:   " FLAP_FONT  "\n" \
+                       "*frameColor:   #444444"   "\n" \
+                       "*caseColor:    #666666"   "\n" \
+                       "*discColor:    #888888"   "\n" \
+                       "*finColor:     #222222"   "\n" \
+                       "*textColor:    #FFFF00"   "\n" \
+                       "*multiSample:  True        \n" \
+                       "*program:      xscreensaver-text\n" \
+                       "*usePty:       False\n"
+
+# define refresh_splitflap 0
+#undef countof
+#define countof(x) (sizeof((x))/sizeof((*x)))
+
+#define DEF_SPEED       "1.0"
+#define DEF_WIDTH       "22"
+#define DEF_HEIGHT      "8"
+#define DEF_SPIN        "XYZ"
+#define DEF_WANDER      "True"
+#define DEF_FACE_FRONT  "True"
+#define DEF_MODE        "Text"
+
+#include "xlockmore.h"
+
+#include <ctype.h>
+
+#ifdef USE_GL /* whole file */
+
+#include "gltrackball.h"
+#include "rotator.h"
+#include "xpm-ximage.h"
+#include "utf8wc.h"
+#include "textclient.h"
+#include "texfont.h"
+#include "gllist.h"
+
+extern const struct gllist
+  *splitflap_obj_box_quarter_frame, *splitflap_obj_disc_quarter,
+  *splitflap_obj_fin_edge_half, *splitflap_obj_fin_face_half;
+static struct gllist *splitflap_obj_outer_frame = 0;
+
+static const struct gllist * const *all_objs[] = {
+  &splitflap_obj_box_quarter_frame, &splitflap_obj_disc_quarter, 
+  &splitflap_obj_fin_edge_half, &splitflap_obj_fin_face_half,
+  (const struct gllist * const *) &splitflap_obj_outer_frame
+};
+
+#define SPLITFLAP_QUARTER_FRAME        0
+#define SPLITFLAP_DISC_QUARTER 1
+#define SPLITFLAP_FIN_EDGE_HALF        2
+#define SPLITFLAP_FIN_FACE_HALF        3
+#define SPLITFLAP_OUTER_FRAME  4
+
+#define COLON_WIDTH 0.5
+
+typedef struct {
+  int target_index;            /* desired character */
+  double current_index;                /* currently displayed, fractional */
+  GLfloat sticky;              /* bottom fin doesn't fall all the way */
+  int missing;                 /* which fin has snapped off, or -1 */
+  const char * const *spool;   /* chars available for display */
+  int spool_size;              /* how many fins on the spool */
+} flapper;
+
+typedef struct {
+  const char *text;
+  GLuint texid;
+  XCharStruct metrics;
+  int tex_width, tex_height;
+} texinfo;
+
+typedef struct {
+  GLXContext *glx_context;
+  rotator *rot, *rot2;
+  trackball_state *trackball;
+  Bool button_down_p;
+  Bool spinx, spiny, spinz;
+
+  texinfo *texinfo;
+  int texinfo_size;
+
+  GLuint *dlists;
+  GLfloat component_colors[countof(all_objs)][4];
+  GLfloat text_color[4];
+
+  flapper *flappers;  /* grid_width * grid_height */
+
+  texture_font_data *font_data;
+  int ascent, descent;
+
+  text_data *tc;
+  unsigned char text[5];
+  int linger;
+  int clock_p;
+  Bool first_time_p;
+
+} splitflap_configuration;
+
+static const char * const digit_s1_spool[] = { " ", "1" };
+static const char * const digit_01_spool[] = { "0", "1" };
+static const char * const ap_spool[]       = { "A", "P" };
+static const char * const m_spool[]        = { "M" };
+static const char * const digit_05_spool[] = { "0", "1", "2", "3", "4", "5" };
+static const char * const digit_spool[] = {
+  "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
+};
+
+static const char * const ascii_spool[] = {
+  " ", "!", "\"", "#", "$", "%", "&", "'",
+  "(", ")", "*", "+", ",", "-", ".", "/",
+  "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
+  ":", ";", "<", "=", ">", "?", "@",
+  "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
+  "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
+  "[", "\\", "]", "^", "_", "`", "{", "|", "}", "~", 
+};
+
+
+/* If we include these, the flappers just take too long. It's boring. */
+static const char * const latin1_spool[] = {
+  " ", "!", "\"", "#", "$", "%", "&", "'",
+  "(", ")", "*", "+", ",", "-", ".", "/",
+  "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
+  ":", ";", "<", "=", ">", "?", "@",
+  "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
+  "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
+  "[", "\\", "]", "^", "_", "`", "{", "|", "}", "~", 
+
+  "\302\241", "\302\242", "\302\243", "\302\245",
+  "\302\247", "\302\251", "\302\265", "\302\266",
+
+  "\303\200", "\303\201", "\303\202", "\303\203",
+  "\303\204", "\303\205", "\303\206", "\303\207",
+  "\303\210", "\303\211", "\303\212", "\303\213",
+  "\303\214", "\303\215", "\303\216", "\303\217",
+  "\303\220", "\303\221", "\303\222", "\303\223",
+  "\303\224", "\303\225", "\303\226", "\303\230",
+  "\303\231", "\303\232", "\303\233", "\303\234",
+  "\303\235", "\303\236", "\303\237", "\303\267",
+};
+
+
+static splitflap_configuration *bps = NULL;
+
+static GLfloat speed;
+static int grid_width, grid_height;
+static char *do_spin;
+static Bool do_wander;
+static Bool face_front_p;
+static char *mode_str;
+
+static XrmOptionDescRec opts[] = {
+  { "-speed",   ".speed",     XrmoptionSepArg, 0 },
+  { "-width",   ".width",     XrmoptionSepArg, 0 },
+  { "-height",  ".height",    XrmoptionSepArg, 0 },
+  { "-spin",    ".spin",      XrmoptionSepArg, 0 },
+  { "+spin",    ".spin",      XrmoptionNoArg, "" },
+  { "-wander",  ".wander",    XrmoptionNoArg, "True" },
+  { "+wander",  ".wander",    XrmoptionNoArg, "False" },
+  { "-front",   ".faceFront", XrmoptionNoArg, "True" },
+  { "+front",   ".faceFront", XrmoptionNoArg, "False" },
+  { "-mode",    ".mode",      XrmoptionSepArg, 0 },
+};
+
+static argtype vars[] = {
+  {&speed,       "speed",      "Speed",     DEF_SPEED,      t_Float},
+  {&grid_width,  "width",      "Width",     DEF_WIDTH,      t_Int},
+  {&grid_height, "height",     "Height",    DEF_HEIGHT,     t_Int},
+  {&do_spin,      "spin",      "Spin",      DEF_SPIN,       t_String},
+  {&do_wander,    "wander",    "Wander",    DEF_WANDER,     t_Bool},
+  {&face_front_p, "faceFront", "FaceFront", DEF_FACE_FRONT, t_Bool},
+  {&mode_str,     "mode",       "Mode",     DEF_MODE,       t_String},
+};
+
+ENTRYPOINT ModeSpecOpt splitflap_opts = {
+  countof(opts), opts, countof(vars), vars, NULL};
+
+
+/* Window management, etc
+ */
+ENTRYPOINT void
+reshape_splitflap (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, 0.5, 25);
+
+  glMatrixMode(GL_MODELVIEW);
+  glLoadIdentity();
+  gluLookAt( 0, 0, 3,  /* 10x lower than traditional, for better depth rez */
+             0, 0, 0,
+             0, 1, 0);
+
+  glClear(GL_COLOR_BUFFER_BIT);
+}
+
+
+ENTRYPOINT Bool
+splitflap_handle_event (ModeInfo *mi, XEvent *event)
+{
+  splitflap_configuration *bp = &bps[MI_SCREEN(mi)];
+
+  if (gltrackball_event_handler (event, bp->trackball,
+                                 MI_WIDTH (mi), MI_HEIGHT (mi),
+                                 &bp->button_down_p))
+    return True;
+
+  return False;
+}
+
+
+static void
+init_textures (ModeInfo *mi)
+{
+  splitflap_configuration *bp = &bps[MI_SCREEN(mi)];
+  int i;
+  const char * const *spool = latin1_spool;
+  int max = countof(latin1_spool);
+
+  bp->texinfo = (texinfo *) calloc (max+1, sizeof(*bp->texinfo));
+  texture_string_metrics (bp->font_data, "", 0, &bp->ascent, &bp->descent);
+
+  for (i = 0; i < max; i++)
+    {
+      texinfo *ti = &bp->texinfo[i];
+      glGenTextures (1, &ti->texid);
+      glBindTexture (GL_TEXTURE_2D, ti->texid);
+
+      ti->text = spool[i];
+
+      /* fprintf(stderr, "%d \\%03o\\%03o %s\n", i,
+              (unsigned char) ti->text[0],
+              (unsigned char) ti->text[1],
+              ti->text); */
+
+      string_to_texture (bp->font_data, ti->text, &ti->metrics,
+                         &ti->tex_width, &ti->tex_height);
+    }
+  bp->texinfo_size = i;
+
+  glBindTexture (GL_TEXTURE_2D, 0);
+}
+
+
+static void
+parse_color (ModeInfo *mi, char *key, GLfloat color[4])
+{
+  XColor xcolor;
+  char *string = get_string_resource (mi->dpy, key, "Color");
+  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;
+}
+
+
+static int draw_outer_frame (ModeInfo *mi);
+
+ENTRYPOINT void 
+init_splitflap (ModeInfo *mi)
+{
+  splitflap_configuration *bp;
+  int wire = MI_IS_WIREFRAME(mi);
+  int i;
+  if (!bps) {
+    bps = (splitflap_configuration *)
+      calloc (MI_NUM_SCREENS(mi), sizeof (splitflap_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_splitflap (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
+
+  bp->first_time_p = True;
+
+  if (!mode_str || !*mode_str || !strcasecmp(mode_str, "text"))
+    {
+      bp->clock_p = 0;
+    }
+  else if (!strcasecmp (mode_str, "clock") ||
+           !strcasecmp (mode_str, "clock12"))
+    {
+      bp->clock_p = 12;
+      grid_width  = 8;
+      grid_height = 1;
+    }
+  else if (!strcasecmp (mode_str, "clock24"))
+    {
+      bp->clock_p = 24;
+      grid_width  = 6;
+      grid_height = 1;
+    }
+  else
+    {
+      fprintf (stderr,
+           "%s: `mode' must be text, clock12 or clock24: not `%s'\n",
+               progname, mode_str);
+      exit (1);
+    }
+
+  if (! bp->clock_p)
+    {
+      bp->tc = textclient_open (MI_DISPLAY (mi));
+      bp->text[0] = 0;
+
+      if (grid_width > 10)
+        textclient_reshape (bp->tc, 
+                            grid_width, grid_height,
+                            grid_width, grid_height,
+                            0);
+    }
+
+  if (bp->clock_p)
+    speed /= 4;
+
+  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);
+    }
+
+
+  {
+    double spin_speed   = 0.5;
+    double wander_speed = 0.005;
+    double tilt_speed   = 0.001;
+    double spin_accel   = 0.5;
+
+    char *s = do_spin;
+    while (*s)
+      {
+        if      (*s == 'x' || *s == 'X') bp->spinx = True;
+        else if (*s == 'y' || *s == 'Y') bp->spiny = True;
+        else if (*s == 'z' || *s == 'Z') bp->spinz = True;
+        else if (*s == '0') ;
+        else
+          {
+            fprintf (stderr,
+         "%s: spin must contain only the characters X, Y, or Z (not \"%s\")\n",
+                     progname, do_spin);
+            exit (1);
+          }
+        s++;
+      }
+
+    bp->rot = make_rotator (bp->spinx ? spin_speed : 0,
+                            bp->spiny ? spin_speed : 0,
+                            bp->spinz ? spin_speed : 0,
+                            spin_accel,
+                            do_wander ? wander_speed : 0,
+                            False);
+    bp->rot2 = (face_front_p
+                ? make_rotator (0, 0, 0, 0, tilt_speed, True)
+                : 0);
+    bp->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);
+
+  parse_color (mi, "textColor", bp->text_color);
+  for (i = 0; i < countof(all_objs); i++)
+    {
+      const struct gllist *gll = *all_objs[i];
+      char *key = 0;
+      GLfloat spec[4] = {0.4, 0.4, 0.4, 1.0};
+      GLfloat shiny = 80; /* 0-128 */
+
+      glNewList (bp->dlists[i], GL_COMPILE);
+
+      glMatrixMode(GL_MODELVIEW);
+      glPushMatrix();
+      glMatrixMode(GL_TEXTURE);
+      glPushMatrix();
+      glMatrixMode(GL_MODELVIEW);
+
+      glRotatef (-90, 1, 0, 0);
+
+      glBindTexture (GL_TEXTURE_2D, 0);
+
+      switch (i) {
+      case SPLITFLAP_QUARTER_FRAME:
+        key = "frameColor";
+        break;
+      case SPLITFLAP_OUTER_FRAME:
+        key = "caseColor";
+        break;
+      case SPLITFLAP_DISC_QUARTER:
+        key = (wire ? "frameColor" : "discColor");
+        break;
+      case SPLITFLAP_FIN_EDGE_HALF:
+      case SPLITFLAP_FIN_FACE_HALF:
+        key = "finColor";
+        break;
+      default:
+        abort();
+      }
+
+      parse_color (mi, key, bp->component_colors[i]);
+
+      if (wire && i == SPLITFLAP_FIN_EDGE_HALF)
+        bp->component_colors[i][0] = 
+        bp->component_colors[i][1] = 
+        bp->component_colors[i][2] = 0.7;
+
+      glMaterialfv (GL_FRONT_AND_BACK, GL_SPECULAR,  spec);
+      glMaterialf  (GL_FRONT_AND_BACK, GL_SHININESS, shiny);
+
+      switch (i) {
+      case SPLITFLAP_OUTER_FRAME:
+        if (! splitflap_obj_outer_frame)
+          splitflap_obj_outer_frame =
+            (struct gllist *) calloc (1, sizeof(*splitflap_obj_outer_frame));
+        splitflap_obj_outer_frame->points = draw_outer_frame(mi);
+        break;
+      default:
+        renderList (gll, wire);
+        break;
+      }
+
+      glMatrixMode(GL_TEXTURE);
+      glPopMatrix();
+      glMatrixMode(GL_MODELVIEW);
+      glPopMatrix();
+
+      glEndList ();
+    }
+
+  if (grid_width < 1)  grid_width  = 1;
+  if (grid_height < 1) grid_height = 1;
+  bp->flappers = (flapper *) calloc (grid_width * grid_height,
+                                     sizeof (flapper));
+
+  for (i = 0; i < grid_width * grid_height; i++)
+    {
+      flapper *f = &bp->flappers[i];
+
+      if (!bp->clock_p)
+        {
+          f->spool = ascii_spool;
+          f->spool_size = countof (ascii_spool);
+        }
+      else
+        {
+          switch (i) {
+          case 0:
+            if (bp->clock_p == 12)
+              {
+                f->spool = digit_s1_spool;
+                f->spool_size = countof (digit_s1_spool);
+              }
+            else
+              {
+                f->spool = digit_01_spool;
+                f->spool_size = countof (digit_01_spool);
+              }
+            break;
+          case 1: case 3: case 5:
+            f->spool = digit_spool;
+            f->spool_size = countof (digit_spool);
+            break;
+          case 2: case 4:
+            f->spool = digit_05_spool;
+            f->spool_size = countof (digit_05_spool);
+            break;
+          case 6:
+            f->spool = ap_spool;
+            f->spool_size = countof (ap_spool);
+            break;
+          case 7:
+            f->spool = m_spool;
+            f->spool_size = countof (m_spool);
+            break;
+          default:
+            abort();
+          }
+        }
+
+      f->target_index = random() % f->spool_size;
+      /* f->target_index = 0; */
+      f->current_index = f->target_index;
+      f->missing = (((random() % 10) == 0)
+                    ? (random() % f->spool_size)
+                    : -1);
+    }
+
+  bp->font_data = load_texture_font (mi->dpy, "flapFont");
+  init_textures (mi);
+
+  reshape_splitflap (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
+}
+
+
+static int
+draw_component (ModeInfo *mi, int i)
+{
+  splitflap_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_frame_quarter (ModeInfo *mi, flapper *f)
+{
+  int count = 0;
+  glPushMatrix();
+  count += draw_component (mi, SPLITFLAP_QUARTER_FRAME);
+  glPopMatrix();
+  return count;
+}
+
+static int
+draw_disc_quarter (ModeInfo *mi, flapper *f)
+{
+  int count = 0;
+  glPushMatrix();
+  count += draw_component (mi, SPLITFLAP_DISC_QUARTER);
+  glPopMatrix();
+  return count;
+}
+
+static int
+draw_fin_edge_half (ModeInfo *mi, flapper *f)
+{
+  int count = 0;
+  glPushMatrix();
+  count += draw_component (mi, SPLITFLAP_FIN_EDGE_HALF);
+  glPopMatrix();
+  return count;
+}
+
+static int
+draw_fin_face_half (ModeInfo *mi, flapper *f)
+{
+  int count = 0;
+  if (MI_IS_WIREFRAME(mi)) return 0;
+  glPushMatrix();
+  count += draw_component (mi, SPLITFLAP_FIN_FACE_HALF);
+  glPopMatrix();
+  return count;
+}
+
+
+static int
+draw_frame (ModeInfo *mi, flapper *f)
+{
+  int count = 0;
+
+  glPushMatrix();
+
+  glFrontFace (GL_CCW);
+  count += draw_frame_quarter (mi, f);
+  count += draw_disc_quarter (mi, f);
+
+  glScalef (-1, 1, 1);
+  glFrontFace (GL_CW);
+  count += draw_frame_quarter (mi, f);
+  count += draw_disc_quarter (mi, f);
+
+  glScalef ( 1, -1, 1);
+  glFrontFace (GL_CCW);
+  count += draw_frame_quarter (mi, f);
+  count += draw_disc_quarter (mi, f);
+
+  glScalef (-1, 1, 1);
+  glFrontFace (GL_CW);
+  count += draw_frame_quarter (mi, f);
+  count += draw_disc_quarter (mi, f);
+
+  glPopMatrix();
+  return count;
+}
+
+
+static void
+draw_fin_text_quad (ModeInfo *mi, flapper *f, int index, Bool top_p)
+{
+  int wire = MI_IS_WIREFRAME(mi);
+  splitflap_configuration *bp = &bps[MI_SCREEN(mi)];
+
+  /* 15  / is weird
+     27  ; descends too far
+     32  @ is too wide
+     59  [ descends too far
+     79  A^ is taller than the font
+     89  I` is weird
+   */
+
+  GLfloat z = 0.035;   /* Lifted off the surface by this distance */
+  GLfloat bot = 0.013; /* Distance away from the mid gutter */
+  GLfloat scale = 1.8;  /* Scale to fill the panel */
+
+  int lh = bp->ascent + bp->descent;
+  texinfo *ti;
+  GLfloat qx0, qy0, qx1, qy1;
+  GLfloat tx0, ty0, tx1, ty1;
+  XCharStruct overall;
+  int tex_width, tex_height;
+  int i;
+
+  for (i = 0; i < bp->texinfo_size; i++)
+    {
+      ti = &bp->texinfo[i];
+      if (!strcmp (f->spool[index], ti->text))
+        break;
+    }
+  if (i >= bp->texinfo_size) abort();
+
+  overall = ti->metrics;
+  tex_width  = ti->tex_width;
+  tex_height = ti->tex_height;
+
+  if (bp->ascent < overall.ascent)
+    /* WTF! &Aacute; has a higher ascent than the font itself!
+       Scale it down so that it doesn't overlap the fin. */
+    scale *= bp->ascent / (GLfloat) overall.ascent * 0.98;
+
+  glPushMatrix();
+
+  glNormal3f (0, 0, 1);
+  glFrontFace (top_p ? GL_CCW : GL_CW);
+
+  if (! wire)
+    {
+      glBindTexture (GL_TEXTURE_2D, ti->texid);
+      enable_texture_string_parameters();
+    }
+
+  glTranslatef (0, 0, z);              /* Move to just above the surface */
+  glScalef (1.0 / lh, 1.0 / lh, 1);    /* Scale to font pixel coordinates */
+  glScalef (scale, scale, 1);          /* Fill the panel with the font */
+
+  if (!top_p)
+    {
+      glRotatef (180, 0, 0, 1);
+    }
+
+  /* Position the XCharStruct origin at 0,0 in the scene. */
+  qx0 = -overall.lbearing;
+  qy0 = -overall.descent;
+  qx1 =  overall.rbearing;
+  qy1 =  overall.ascent;
+
+  /* Center horizontally. */
+  qx0 -= (overall.rbearing - overall.lbearing) / 2.0;
+  qx1 -= (overall.rbearing - overall.lbearing) / 2.0;
+
+
+  /* Move origin to below font descenders. */
+  qy0 += bp->descent;
+  qy1 += bp->descent;
+
+  /* Center vertically. */
+  qy0 -= (bp->ascent + bp->descent) / 2.0;
+  qy1 -= (bp->ascent + bp->descent) / 2.0;
+
+  /* Move the descenders down a bit, if there's room.
+     This means that weirdos like [ and $ might not be on the baseline.
+     #### This looks good with X11 fonts but bad with MacOS fonts.  WTF?
+   */
+#ifndef HAVE_COCOA
+  {
+    GLfloat off = bp->descent / 3.0;
+    GLfloat max = bp->descent - off;
+    if (overall.descent > max)
+      off = max - overall.descent;
+    if (off < 0)
+      off = 0;
+    qy0 -= off;
+    qy1 -= off;
+  }
+# endif /* !HAVE_COCOA */
+
+  /* Attach the texture to the quad. */
+  tx0 = 0;
+  ty1 = 0;
+  tx1 = (overall.rbearing - overall.lbearing) / (GLfloat) tex_width;
+  ty0 = (overall.ascent   + overall.descent)  / (GLfloat) tex_height;
+
+  /* Convert from font ascent/descent to character ascent/descent. */
+
+  /* Flip texture horizontally on bottom panel. */
+  if (!top_p)
+    {
+      GLfloat s = tx0;
+      tx0 = tx1;
+      tx1 = s;
+    }
+
+
+  /* Cut the character in half, truncating just above the split line. */
+  {
+    GLfloat oqy0 = qy0;
+    GLfloat oqy1 = qy1;
+    GLfloat r0, r1;
+
+    bot *= lh * scale;
+
+    if (top_p)
+      {
+        if (qy0 < bot)
+          qy0 = bot;
+      }
+    else
+      {
+        if (qy1 > -bot)
+          qy1 = -bot;
+      }
+
+    r0 = (qy0 - oqy0) / (oqy1 - oqy0);
+    r1 = (qy1 - oqy1) / (oqy1 - oqy0);
+    ty0 -= r0 * (ty0 - ty1);
+    ty1 -= r1 * (ty0 - ty1);
+  }
+
+  glColor4fv (bp->text_color);
+  glBegin (wire ? GL_LINE_LOOP : GL_QUADS);
+  glTexCoord2f (tx0, ty0); glVertex3f (qx0, qy0, 0);
+  glTexCoord2f (tx1, ty0); glVertex3f (qx1, qy0, 0);
+  glTexCoord2f (tx1, ty1); glVertex3f (qx1, qy1, 0);
+  glTexCoord2f (tx0, ty1); glVertex3f (qx0, qy1, 0);
+  glEnd();
+
+  glPopMatrix();
+
+  if (! wire)
+    {
+      glDisable (GL_BLEND);
+      glEnable (GL_LIGHTING);
+      glDisable (GL_TEXTURE_2D);
+    }
+}
+
+
+static int
+draw_fin (ModeInfo *mi, flapper *f, int front_index, int back_index,
+          Bool text_p)
+{
+  int count = 0;
+
+  glPushMatrix();
+
+  glFrontFace (GL_CCW);
+
+  if (! text_p)
+    count += draw_fin_edge_half (mi, f);
+
+  if (front_index >= 0)
+    {
+      if (text_p)
+        {
+          draw_fin_text_quad (mi, f, front_index, True);
+          count++;
+        }
+      else
+        count += draw_fin_face_half (mi, f);
+    }
+
+  glScalef (-1, 1, 1);
+  if (! text_p)
+    {
+      glFrontFace (GL_CW);
+      count += draw_fin_edge_half (mi, f);
+      if (front_index >= 0)
+        count += draw_fin_face_half (mi, f);
+    }
+
+  if (back_index >= 0)
+    {
+      glRotatef (180, 0, 1, 0);
+      if (text_p)
+        {
+          draw_fin_text_quad (mi, f, back_index, False);
+          count++;
+        }
+      else
+        {
+          count += draw_fin_face_half (mi, f);
+          glScalef (-1, 1, 1);
+          glFrontFace (GL_CCW);
+          count += draw_fin_face_half (mi, f);
+        }
+    }
+
+  glPopMatrix();
+  return count;
+}
+
+
+/* The case holding the grid of flappers.
+ */
+static int
+draw_outer_frame (ModeInfo *mi)
+{
+  splitflap_configuration *bp = &bps[MI_SCREEN(mi)];
+  int count = 0;
+  GLfloat w = grid_width;
+  GLfloat h = grid_height;
+  GLfloat d = 1;
+
+  if (bp->clock_p == 12)
+    w += COLON_WIDTH * 3;
+  else if (bp->clock_p == 24)
+    w += COLON_WIDTH * 2;
+
+  w += 0.2;
+  h += 0.2;
+
+  if (bp->clock_p) w += 0.25;
+  if (w > 3) w += 0.5;
+  if (h > 3) h += 0.5;
+
+  if (MI_IS_WIREFRAME(mi))
+    return 0;
+
+  glFrontFace (GL_CCW);
+  glPushMatrix();
+  glTranslatef (0, 1.03, 0);
+
+  glBegin (GL_QUADS);
+
+  glNormal3f ( 0,  1,  0);     /* back */
+  glVertex3f (-w,  d,  h);
+  glVertex3f ( w,  d,  h);
+  glVertex3f ( w,  d, -h);
+  glVertex3f (-w,  d, -h);
+  count++;
+
+  glNormal3f ( 0, -1,  0);     /* front */
+  glVertex3f (-w, -d, -h);
+  glVertex3f ( w, -d, -h);
+  glVertex3f ( w, -d,  h);
+  glVertex3f (-w, -d,  h);
+  count++;
+
+  glNormal3f ( 0, 0,   1);     /* top */
+  glVertex3f (-w, -d,  h);
+  glVertex3f ( w, -d,  h);
+  glVertex3f ( w,  d,  h);
+  glVertex3f (-w,  d,  h);
+  count++;
+
+  glNormal3f ( 0,  0, -1);     /* bottom */
+  glVertex3f (-w,  d, -h);
+  glVertex3f ( w,  d, -h);
+  glVertex3f ( w, -d, -h);
+  glVertex3f (-w, -d, -h);
+  count++;
+
+  glNormal3f ( 1,  0,  0);     /* left */
+  glVertex3f ( w, -d,  h);
+  glVertex3f ( w, -d, -h);
+  glVertex3f ( w,  d, -h);
+  glVertex3f ( w,  d,  h);
+  count++;
+
+  glNormal3f (-1,  0,  0);     /* right */
+  glVertex3f (-w, -d, -h);
+  glVertex3f (-w, -d,  h);
+  glVertex3f (-w,  d,  h);
+  glVertex3f (-w,  d, -h);
+  count++;
+
+  glEnd();
+  glPopMatrix();
+
+  return count;
+}
+
+
+static void
+tick_flapper (ModeInfo *mi, flapper *f)
+{
+  splitflap_configuration *bp = &bps[MI_SCREEN(mi)];
+  double prev = f->current_index;
+  Bool wrapped_p = False;
+
+  if (bp->button_down_p) return;
+  if (f->current_index == f->target_index)
+    return;
+
+  f->current_index += speed * 0.35;            /* turn the crank */
+
+  while (f->current_index > f->spool_size)
+    {
+      f->current_index -= f->spool_size;
+      wrapped_p = True;
+    }
+
+  if (f->current_index < 0) abort();
+
+  if ((prev < f->target_index || wrapped_p) &&
+      f->current_index > f->target_index)      /* just overshot */
+    f->current_index = f->target_index;
+}
+
+
+#define MOD(M,N) (((M)+(N)) % (N))  /* Works with negatives */
+
+static int
+draw_flapper (ModeInfo *mi, flapper *f, Bool text_p)
+{
+  int prev_index = floor (f->current_index);
+  int next_index = MOD (prev_index+1, f->spool_size);
+  int count = 0;
+  GLfloat epsilon = 0.02;
+  GLfloat r = f->current_index - prev_index;
+  Bool moving_p = (r > 0 && r < 1);
+  GLfloat sticky = f->sticky;
+
+  if (f->missing >= 0)
+    sticky = 0;
+
+  if (f->missing >= 0 &&
+      MOD (prev_index, f->spool_size) == f->missing)
+    {
+      moving_p = False;
+      sticky = 0;
+    }
+
+  if (!moving_p)
+    next_index = prev_index;
+
+  if (! text_p)
+    count += draw_frame (mi, f);
+
+  /* Top flap, flat: top half of target char */
+  if (!moving_p || !text_p || r > epsilon)
+    {
+      int p2 = next_index;
+
+      if (p2 == f->missing)
+        p2 = MOD (p2+1, f->spool_size);
+
+      count += draw_fin (mi, f, p2, -1, text_p);
+    }
+
+  /* Bottom flap, flat: bottom half of prev char */
+  if (!moving_p || !text_p || r < 1 - epsilon)
+    {
+      int p2 = prev_index;
+
+      if (!moving_p && sticky)
+        p2 = MOD (p2-1, f->spool_size);
+
+      if (f->missing >= 0 &&
+          p2 == MOD (f->missing+1, f->spool_size))
+        p2 = MOD (p2-1, f->spool_size);
+
+      glPushMatrix();
+      glRotatef (180, 1, 0, 0);
+      count += draw_fin (mi, f, -1, p2, text_p);
+      glPopMatrix();
+    }
+
+  /* Moving flap, front: top half of prev char */
+  /* Moving flap, back: bottom half of target char */
+  if (moving_p || sticky)
+    {
+      if (!moving_p)
+        r = 1.0;
+      if (sticky && r > 1 - sticky)
+        r = 1 - sticky;
+      glPushMatrix();
+      glRotatef (r * 180, 1, 0, 0);
+      count += draw_fin (mi, f, prev_index, next_index, text_p);
+      glPopMatrix();
+    }
+
+  return count;
+}
+
+
+static int
+draw_colon (ModeInfo *mi)
+{
+  splitflap_configuration *bp = &bps[MI_SCREEN(mi)];
+  GLfloat s = 1.0 / (bp->ascent + bp->descent);
+  GLfloat z = 0.01;
+  int count = 0;
+  XCharStruct m;
+
+  texture_string_metrics (bp->font_data, ":", &m, 0, 0);
+
+  s *= 2;
+
+  glPushMatrix();
+
+  glTranslatef (-(1 + COLON_WIDTH), 0, 0);
+  glScalef (s, s, 1);
+
+  glTranslatef (-m.lbearing - (m.rbearing - m.lbearing)/2,
+                -(m.ascent + m.descent) / 2,
+                0);
+
+  glEnable (GL_TEXTURE_2D);
+
+  /* draw the text five times, to give it a border. */
+  {
+    const XPoint offsets[] = {{ -1, -1 },
+                              { -1,  1 },
+                              {  1,  1 },
+                              {  1, -1 },
+                              {  0,  0 }};
+    int i;
+    GLfloat n = 1.5;
+
+    glColor3f (0, 0, 0);
+    for (i = 0; i < countof(offsets); i++)
+      {
+        glPushMatrix();
+        if (offsets[i].x == 0)
+          {
+            glColor4fv (bp->text_color);
+            glTranslatef (0, 0, z * 2);
+          }
+        glTranslatef (n * offsets[i].x, n * offsets[i].y, 0);
+        print_texture_string (bp->font_data, ":");
+        count++;
+        glPopMatrix();
+      }
+  }
+
+  glPopMatrix();
+
+  return count;
+}
+
+
+/* Reads and returns a single Unicode character from the text client.
+ */
+static unsigned long
+read_unicode (ModeInfo *mi)
+{
+  splitflap_configuration *bp = &bps[MI_SCREEN(mi)];
+  const unsigned char *end = bp->text + sizeof(bp->text) - 1;  /* 4 bytes */
+  unsigned long uc = 0;
+  long L;
+  int i;
+
+  if (bp->clock_p || !bp->tc) abort();
+
+  /* Fill the buffer with available input.
+   */
+  i = strlen ((char *) bp->text);
+  while (i < (end - bp->text))
+    {
+      int c = textclient_getc (bp->tc);
+      if (c <= 0) break;
+      bp->text[i++] = (char) c;
+      bp->text[i] = 0;
+    }
+
+  /* Pop 1-4 bytes from the front of the buffer and extract a UTF8 character.
+   */
+  L = utf8_decode (bp->text, i, &uc);
+  if (L)
+    {
+      int j = end - bp->text - L;
+      memmove (bp->text, bp->text + L, j);
+      bp->text[j] = 0;
+    }
+  else
+    uc = 0;
+
+  return uc;
+}
+
+
+/* Given a Unicode character, finds the corresponding index on the spool,
+   if any. Returns 0 if not found.
+ */
+static int
+find_index (ModeInfo *mi, flapper *f, long uc)
+{
+  char string[5];
+  int L = utf8_encode (uc, string, sizeof(string) - 1);
+  int i;
+  if (L <= 0) return 0;
+  string[L] = 0;
+  for (i = 0; i < f->spool_size; i++)
+    {
+      if (!strcmp (string, f->spool[i]))
+        return i;
+    }
+  return 0;
+}
+
+
+/* Read input from the text client and populate the spool with it.
+ */
+static void
+fill_targets (ModeInfo *mi)
+{
+  splitflap_configuration *bp = &bps[MI_SCREEN(mi)];
+  int x, y;
+  Bool cls_p = False;
+
+  if (bp->clock_p)
+    {
+      char buf[80];
+      time_t now = time ((time_t *) 0);
+        struct tm *tm = localtime (&now);
+      const char *fmt = (bp->clock_p == 24
+                         ? "%H%M%S"
+                         : "%I%M%S%p");
+      int i;
+      strftime (buf, sizeof(buf)-1, fmt, tm);
+      if (bp->clock_p == 12 && buf[0] == '0')
+        buf[0] = ' ';
+
+      for (i = 0; i < strlen(buf); i++)
+        {
+          flapper *f = &bp->flappers[i];
+          f->target_index = find_index (mi, f, buf[i]);
+        }
+      for (; i < grid_width * grid_height; i++)
+        {
+          flapper *f = &bp->flappers[i];
+          f->target_index = find_index (mi, f, ' ');
+        }
+      return;
+    }
+
+  for (y = 0; y < grid_height; y++)
+    {
+      Bool nl_p = False;
+      for (x = 0; x < grid_width; x++)
+        {
+          int i = y * grid_width + x;
+          flapper *f = &bp->flappers[i];
+          unsigned long uc = ((nl_p || cls_p) ? ' ' : read_unicode (mi));
+          if (uc == '\r' || uc == '\n')
+            nl_p = True;
+          else if (uc == 12)  /* ^L */
+            cls_p = True;
+
+          /* Convert Unicode to the closest Latin1 equivalent. */
+          if (uc > 127)
+            {
+              Bool ascii_p = (f->spool != latin1_spool);
+              unsigned char s[5], *s2;
+              int L = utf8_encode (uc, (char *) s, sizeof(s));
+              s[L] = 0;
+              s2 = (unsigned char *) utf8_to_latin1 ((char *) s, ascii_p);
+              
+              if (s2[0] < 128) /* ASCII */
+                uc = s2[0];
+              else             /* Latin1 -> UTF8 -> Unicode */
+                {
+                  s[0] = (s2[0] > 0xBF ? 0xC3 : 0xC2);
+                  s[1] = s2[0] & (s2[0] > 0xBF ? 0xBF : 0xFF);
+                  s[2] = 0;
+                  utf8_decode (s, 2, &uc);
+                }
+
+              free (s2);
+            }
+
+          /* Upcase ASCII. Upcasing Unicrud would be rocket surgery. */
+          if (uc >= 'a' && uc <= 'z') uc += ('A'-'a');
+
+          f->target_index = find_index (mi, f, uc);
+
+          f->sticky = (((random() % 20) == 0)
+                       ? 0.05 + frand(0.1) + frand(0.1)
+                       : 0);
+        }
+    }
+
+# if 0
+  for (y = 0; y < grid_height; y++)
+    {
+      fprintf (stderr, "# ");
+      for (x = 0; x < grid_width; x++)
+        {
+          int i = y * grid_width + x;
+          flapper *f = &bp->flappers[i];
+          fprintf(stderr, "%s", bp->spool[f->target_index]);
+        }
+      fprintf (stderr, " #\n");
+    }
+  fprintf (stderr, "\n");
+# endif
+}
+
+
+static void
+draw_flappers (ModeInfo *mi, Bool text_p)
+{
+  splitflap_configuration *bp = &bps[MI_SCREEN(mi)];
+  int x, y;
+  int running = 0;
+
+  for (y = 0; y < grid_height; y++)
+    for (x = 0; x < grid_width; x++)
+      {
+        int i = (grid_height - y - 1) * grid_width + x;
+        flapper *f = &bp->flappers[i];
+        GLfloat xx = x;
+        GLfloat yy = y;
+
+        if (bp->clock_p)
+          {
+            if (x >= 2) xx += COLON_WIDTH;
+            if (x >= 4) xx += COLON_WIDTH;
+            if (x >= 6) xx += COLON_WIDTH;
+          }
+
+        xx *= 2.01;
+        yy *= 1.98;
+
+        glPushMatrix();
+        glTranslatef (xx, yy, 0);
+        mi->polygon_count += draw_flapper (mi, f, text_p);
+
+        if (text_p && bp->clock_p && (x == 2 || x == 4))
+          mi->polygon_count += draw_colon (mi);
+
+        glPopMatrix();
+
+        if (text_p)
+          {
+            tick_flapper (mi, f);
+            if (f->current_index != f->target_index)
+              running++;
+          }
+      }
+
+  if (text_p && !running)
+    {
+      if (bp->clock_p)
+        fill_targets (mi);
+      else if (bp->linger)
+        {
+          bp->linger--;
+          if (!bp->linger)
+            fill_targets (mi);
+        }
+      else
+        {
+          /* Base of 1 second, plus 1 second for every 25 characters.
+             Also multiply by speed? */
+          bp->linger = 30;
+          if (!bp->first_time_p)
+            bp->linger += (grid_width * grid_height * 1.2);
+          bp->first_time_p = False;
+        }
+    }
+}
+
+
+ENTRYPOINT void
+draw_splitflap (ModeInfo *mi)
+{
+  splitflap_configuration *bp = &bps[MI_SCREEN(mi)];
+  Display *dpy = MI_DISPLAY(mi);
+  Window window = MI_WINDOW(mi);
+
+  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);
+
+  glScalef (0.1, 0.1, 0.1);  /* because of gluLookAt */
+
+  {
+    double x, y, z;
+    get_position (bp->rot, &x, &y, &z, !bp->button_down_p);
+    glTranslatef((x - 0.5) * 8,
+                 (y - 0.5) * 8,
+                 (z - 0.5) * 8);
+
+    gltrackball_rotate (bp->trackball);
+
+    if (face_front_p)
+      {
+        double maxx = 120;
+        double maxy = 60;
+        double maxz = 45;
+        get_position (bp->rot2, &x, &y, &z, !bp->button_down_p);
+        if (bp->spinx) glRotatef (maxy/2 - x*maxy, 1, 0, 0);
+        if (bp->spiny) glRotatef (maxx/2 - y*maxx, 0, 1, 0);
+        if (bp->spinz) glRotatef (maxz/2 - z*maxz, 0, 0, 1);
+      }
+    else
+      {
+        get_rotation (bp->rot, &x, &y, &z, !bp->button_down_p);
+        glRotatef (x * 360, 1, 0, 0);
+        glRotatef (y * 360, 0, 1, 0);
+        glRotatef (z * 360, 0, 0, 1);
+      }
+  }
+
+  /* Fit the whole grid on the screen */
+  {
+    GLfloat r = MI_HEIGHT(mi) / (GLfloat) MI_WIDTH(mi);
+    int cells = (grid_width > grid_height
+                 ? grid_width * r
+                 : grid_height);
+    GLfloat s = 8;
+# ifdef USE_IPHONE
+    s *= 2; /* #### What. Why is this necessary? */
+#endif
+    s /= cells;
+    glScalef (s, s, s);
+  }
+
+  mi->polygon_count = 0;
+  mi->polygon_count += draw_component (mi, SPLITFLAP_OUTER_FRAME);
+
+  {
+    GLfloat xoff = (bp->clock_p == 12 ? COLON_WIDTH * 3 :
+                    bp->clock_p == 24 ? COLON_WIDTH * 2 :
+                    0);
+    glTranslatef (1 - (grid_width + xoff), 1 - grid_height, 0);
+  }
+
+  /* We must render all text after all polygons, or alpha blending
+     doesn't work right. */
+  draw_flappers (mi, False);
+  draw_flappers (mi, True);
+
+  glPopMatrix ();
+
+  if (mi->fps_p) do_fps (mi);
+  glFinish();
+
+  glXSwapBuffers(dpy, window);
+}
+
+ENTRYPOINT void
+release_splitflap (ModeInfo *mi)
+{
+  splitflap_configuration *bp = &bps[MI_SCREEN(mi)];
+  if (bp->tc)
+    textclient_close (bp->tc);
+  /* #### bp->texinfo */
+}
+
+XSCREENSAVER_MODULE ("SplitFlap", splitflap)
+
+#endif /* USE_GL */