http://packetstormsecurity.org/UNIX/admin/xscreensaver-4.14.tar.gz
[xscreensaver] / hacks / fontglide.c
diff --git a/hacks/fontglide.c b/hacks/fontglide.c
new file mode 100644 (file)
index 0000000..02493c8
--- /dev/null
@@ -0,0 +1,1589 @@
+/* xscreensaver, Copyright (c) 2003 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.
+ *
+ * fontglide -- reads text from a subprocess and puts it on the screen using
+ * large characters that glide in from the edges, assemble, then disperse.
+ * Requires a system with scalable fonts.  (X's font handing sucks.  A lot.)
+ */
+
+#include <math.h>
+#include "screenhack.h"
+#include <X11/Intrinsic.h>
+
+#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
+#include "xdbe.h"
+#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
+
+extern XtAppContext app;
+
+
+typedef struct {
+  char *text;
+  int x, y, width, height;
+  int ascent, lbearing, rbearing;
+
+  int nticks, tick;
+  int start_x,  start_y;
+  int target_x, target_y;
+  Pixmap pixmap, mask;
+} word;
+
+
+typedef struct {
+  int id;
+  XColor fg;
+  XColor bg;
+  Bool dark_p;
+  Bool move_chars_p;
+  int width;
+
+  char *font_name;
+  XFontStruct *font;
+
+  GC fg_gc;
+
+  int nwords;
+  word **words;
+
+  enum { IN, PAUSE, OUT } anim_state;
+  enum { LEFT, CENTER, RIGHT } alignment;
+  int pause_tick;
+
+} sentence;
+
+
+typedef struct {
+  Display *dpy;
+  Window window;
+  XWindowAttributes xgwa;
+
+  Pixmap b, ba;        /* double-buffer to reduce flicker */
+  GC bg_gc;
+
+#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
+  XdbeBackBuffer backb;
+#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
+
+  Bool dbuf;            /* Whether we're using double buffering. */
+  Bool dbeclear_p;      /* ? */
+
+  int border_width;     /* size of the font outline */
+  char *charset;        /* registry and encoding for font lookups */
+  double speed;                /* frame rate multiplier */
+  double linger;       /* multiplier for how long to leave words on screen */
+  Bool trails_p;
+  Bool debug_p;
+  enum { PAGE, SCROLL } mode;
+
+  char *font_override;  /* if -font was specified on the cmd line */
+
+  FILE *pipe;
+  XtInputId pipe_id;
+  Time subproc_relaunch_delay;
+  Bool input_available_p;
+
+  char buf [40];       /* this only needs to be as big as one "word". */
+  int buf_tail;
+
+  int nsentences;
+  sentence **sentences;
+  Bool spawn_p;                /* whether it is time to create a new sentence */
+  int latest_sentence;
+
+} state;
+
+
+static void launch_text_generator (state *);
+static void drain_input (state *s);
+
+
+/* Finds the set of scalable fonts on the system; picks one;
+   and loads that font in a random pixel size.
+   Returns False if something went wrong.
+ */
+static Bool
+pick_font_1 (state *s, sentence *se)
+{
+  char pattern[1024];
+  char **names = 0;
+  char **names2 = 0;
+  XFontStruct *info = 0;
+  int count = 0, count2 = 0;
+  int i;
+  Bool ok = False;
+
+  if (se->font)
+    {
+      XFreeFont (s->dpy, se->font);
+      free (se->font_name);
+      se->font = 0;
+      se->font_name = 0;
+    }
+
+  if (s->font_override)
+    sprintf (pattern, "%.200s", s->font_override);
+  else
+    sprintf (pattern, "-%s-%s-%s-%s-%s-%s-%s-%s-%s-%s-%s-%s-%s",
+             "*",         /* foundry */
+             "*",         /* family */
+             "*",         /* weight */
+             "*",         /* slant */
+             "*",         /* swidth */
+             "*",         /* adstyle */
+             "0",         /* pixel size */
+             "0",         /* point size */
+             "0",         /* resolution x */
+             "0",         /* resolution y */
+             "p",         /* spacing */
+             "0",         /* avg width */
+             s->charset); /* registry + encoding */
+
+  names = XListFonts (s->dpy, pattern, 1000, &count);
+
+  if (count <= 0)
+    {
+      if (s->font_override)
+        fprintf (stderr, "%s: -font option bogus: %s\n", progname, pattern);
+      else
+        fprintf (stderr, "%s: no scalable fonts found!  (pattern: %s)\n",
+                 progname, pattern);
+      exit (1);
+    }
+
+  i = random() % count;
+
+  names2 = XListFontsWithInfo (s->dpy, names[i], 1000, &count2, &info);
+  if (count2 <= 0)
+    {
+      fprintf (stderr, "%s: pattern %s\n"
+                "     gave unusable %s\n\n",
+               progname, pattern, names[i]);
+      goto FAIL;
+    }
+
+  {
+    XFontStruct *font = &info[0];
+    unsigned long value = 0;
+    char *foundry=0, *family=0, *weight=0, *slant=0, *setwidth=0, *add_style=0;
+    unsigned long pixel=0, point=0, res_x=0, res_y=0;
+    char *spacing=0;
+    unsigned long avg_width=0;
+    char *registry=0, *encoding=0;
+    Atom a;
+    char *bogus = "\"?\"";
+
+# define STR(ATOM,VAR)                                 \
+  bogus = (ATOM);                                      \
+  a = XInternAtom (s->dpy, (ATOM), False);             \
+  if (XGetFontProperty (font, a, &value))              \
+    VAR = XGetAtomName (s->dpy, value);                        \
+  else                                                 \
+    goto FAIL2
+
+# define INT(ATOM,VAR)                                 \
+  bogus = (ATOM);                                      \
+  a = XInternAtom (s->dpy, (ATOM), False);             \
+  if (!XGetFontProperty (font, a, &VAR) ||             \
+      VAR > 9999)                                      \
+    goto FAIL2
+
+    STR ("FOUNDRY",          foundry);
+    STR ("FAMILY_NAME",      family);
+    STR ("WEIGHT_NAME",      weight);
+    STR ("SLANT",            slant);
+    STR ("SETWIDTH_NAME",    setwidth);
+    STR ("ADD_STYLE_NAME",   add_style);
+    INT ("PIXEL_SIZE",       pixel);
+    INT ("POINT_SIZE",       point);
+    INT ("RESOLUTION_X",     res_x);
+    INT ("RESOLUTION_Y",     res_y);
+    STR ("SPACING",          spacing);
+    INT ("AVERAGE_WIDTH",    avg_width);
+    STR ("CHARSET_REGISTRY", registry);
+    STR ("CHARSET_ENCODING", encoding);
+
+#undef INT
+#undef STR
+
+    {
+      double scale = s->xgwa.height / 1024.0;  /* shrink for small windows */
+      int min, max, r;
+
+      min = scale * 24;
+      max = scale * 260;
+
+      if (min < 10) min = 10;
+      if (max < 30) max = 30;
+
+      r = ((max-min)/3)+1;
+
+      pixel = min + ((random() % r) + (random() % r) + (random() % r));
+
+      if (s->mode == SCROLL)  /* scroll mode likes bigger fonts */
+        pixel *= 1.5;
+    }
+
+#if 0
+    /* Occasionally change the aspect ratio of the font, by increasing
+       either the X or Y resolution (while leaving the other alone.)
+
+       #### Looks like this trick doesn't really work that well: the
+            metrics of the individual characters are ok, but the
+            overall font ascent comes out wrong (unscaled.)
+     */
+    if (! (random() % 8))
+      {
+        double n = 2.5 / 3;
+        double scale = 1 + (frand(n) + frand(n) + frand(n));
+        if (random() % 2)
+          res_x *= scale;
+        else
+          res_y *= scale;
+      }
+# endif
+
+    sprintf (pattern,
+             "-%s-%s-%s-%s-%s-%s-%ld-%s-%ld-%ld-%s-%s-%s-%s",
+             foundry, family, weight, slant, setwidth, add_style,
+             pixel, "*", /* point, */
+             res_x, res_y, spacing,
+             "*", /* avg_width */
+             registry, encoding);
+    ok = True;
+
+  FAIL2:
+    if (!ok)
+      fprintf (stderr, "%s: font has bogus %s property: %s\n",
+               progname, bogus, names[i]);
+
+    if (foundry)   XFree (foundry);
+    if (family)    XFree (family);
+    if (weight)    XFree (weight);
+    if (slant)     XFree (slant);
+    if (setwidth)  XFree (setwidth);
+    if (add_style) XFree (add_style);
+    if (spacing)   XFree (spacing);
+    if (registry)  XFree (registry);
+    if (encoding)  XFree (encoding);
+  }
+
+ FAIL: 
+
+  XFreeFontInfo (names2, info, count2);
+  XFreeFontNames (names);
+
+  if (! ok) return False;
+
+  se->font = XLoadQueryFont (s->dpy, pattern);
+  if (! se->font)
+    {
+      fprintf (stderr, "%s: unable to load font %s\n",
+               progname, pattern);
+      return False;
+    }
+
+  if (se->font->min_bounds.width == se->font->max_bounds.width &&
+      !s->font_override)
+    {
+      /* This is to weed out
+         "-urw-nimbus mono l-medium-o-normal--*-*-*-*-p-*-iso8859-1" and
+         "-urw-courier-medium-r-normal--*-*-*-*-p-*-iso8859-1".
+         We asked for only proportional fonts, but this fixed-width font
+         shows up anyway -- but it has goofy metrics (see below) so it
+         looks terrible anyway.
+       */
+      if (s->debug_p)
+        fprintf (stderr,
+                 "%s: skipping bogus monospace non-charcell font: %s\n",
+                 progname, pattern);
+      return False;
+    }
+
+  if (s->debug_p) 
+    fprintf(stderr, "%s: %s\n", progname, pattern);
+
+  se->font_name = strdup (pattern);
+  XSetFont (s->dpy, se->fg_gc, se->font->fid);
+  return True;
+}
+
+
+/* Finds the set of scalable fonts on the system; picks one;
+   and loads that font in a random pixel size.
+ */
+static void
+pick_font (state *s, sentence *se)
+{
+  int i;
+  for (i = 0; i < 20; i++)
+    if (pick_font_1 (s, se))
+      return;
+  fprintf (stderr, "%s: too many failures: giving up!\n", progname);
+  exit (1);
+}
+
+
+static char *unread_word_text = 0;
+
+/* Returns a newly-allocated string with one word in it, or NULL if there
+   is no complete word available.
+ */
+static char *
+get_word_text (state *s)
+{
+  char *start = s->buf;
+  char *end;
+  char *result = 0;
+  int lfs = 0;
+
+  if (unread_word_text)
+    {
+      char *s = unread_word_text;
+      unread_word_text = 0;
+      return s;
+    }
+
+  /* Skip over whitespace at the beginning of the buffer,
+     and count up how many linebreaks we see while doing so.
+   */
+  while (*start &&
+         (*start == ' ' ||
+          *start == '\t' ||
+          *start == '\r' ||
+          *start == '\n'))
+    {
+      if (*start == '\n' || (*start == '\r' && start[1] != '\n'))
+        lfs++;
+      start++;
+    }
+
+  end = start;
+
+  /* If we saw a blank line, then return NULL (treat it as a temporary "eof",
+     to trigger a sentence break here.) */
+  if (lfs >= 2)
+    goto DONE;
+
+  /* Skip forward to the end of this word (find next whitespace.) */
+  while (*end &&
+         (! (*end == ' ' ||
+             *end == '\t' ||
+             *end == '\r' ||
+             *end == '\n')))
+    end++;
+
+  /* If we have a word, allocate a string for it */
+  if (end > start)
+    {
+      result = malloc ((end - start) + 1);
+      strncpy (result, start, (end-start));
+      result [end-start] = 0;
+    }
+
+ DONE:
+
+  /* Make room in the buffer by compressing out any bytes we've processed.
+   */
+  if (end > s->buf)
+    {
+      int n = end - s->buf;
+      memmove (s->buf, end, sizeof(s->buf) - n);
+      s->buf_tail -= n;
+    }
+
+  /* See if there is more to be read, now that there's room in the buffer. */
+  drain_input (s);
+
+  return result;
+}
+
+
+/* Gets some random text, and creates a "word" object from it.
+ */
+static word *
+new_word (state *s, sentence *se, char *txt, Bool alloc_p)
+{
+  word *w;
+  XCharStruct overall;
+  int dir, ascent, descent;
+  int bw = s->border_width;
+
+  if (!txt)
+    return 0;
+
+  w = (word *) calloc (1, sizeof(*w));
+  XTextExtents (se->font, txt, strlen(txt), &dir, &ascent, &descent, &overall);
+
+  w->width    = overall.rbearing - overall.lbearing + bw + bw;
+  w->height   = overall.ascent   + overall.descent  + bw + bw;
+  w->ascent   = overall.ascent   + bw;
+  w->lbearing = overall.lbearing - bw;
+  w->rbearing = overall.width    + bw;
+
+# if 0
+  /* The metrics on some fonts are strange -- e.g.,
+     "-urw-nimbus mono l-medium-o-normal--*-*-*-*-p-*-iso8859-1" and
+     "-urw-courier-medium-r-normal--*-*-*-*-p-*-iso8859-1" both have
+     an rbearing so wide that it looks like there are two spaces after
+     each letter.  If this character says it has an rbearing that is to
+     the right of its ink, ignore that.
+
+     #### Of course, this hack only helps when we're in `move_chars_p' mode
+          and drawing a char at a time -- when we draw the whole word at once,
+          XDrawString believes the bogus metrics and spaces the font out
+          crazily anyway.
+
+     Sigh, this causes some text to mis-render in, e.g.,
+     "-adobe-utopia-medium-i-normal--114-*-100-100-p-*-iso8859-1"
+     (in "ux", we need the rbearing on "r" or we get too much overlap.)
+   */
+  if (w->rbearing > w->width)
+    w->rbearing = w->width;
+# endif /* 0 */
+
+  if (s->mode == SCROLL && !alloc_p) abort();
+
+  if (alloc_p)
+    {
+      int i, j;
+      XGCValues gcv;
+      GC gc0, gc1;
+
+      w->pixmap = XCreatePixmap (s->dpy, s->b, w->width, w->height, 1L);
+      w->mask   = XCreatePixmap (s->dpy, s->b, w->width, w->height, 1L);
+
+      gcv.font = se->font->fid;
+      gcv.foreground = 0L;
+      gcv.background = 1L;
+      gc0 = XCreateGC (s->dpy, w->pixmap, GCFont|GCForeground|GCBackground,
+                       &gcv);
+      gcv.foreground = 1L;
+      gcv.background = 0L;
+      gc1 = XCreateGC (s->dpy, w->pixmap, GCFont|GCForeground|GCBackground,
+                       &gcv);
+
+      XFillRectangle (s->dpy, w->mask,   gc0, 0, 0, w->width, w->height);
+      XFillRectangle (s->dpy, w->pixmap, gc0, 0, 0, w->width, w->height);
+
+      if (s->debug_p)
+        {
+          /* bounding box (behind the characters) */
+          XDrawRectangle (s->dpy, w->pixmap, (se->dark_p ? gc0 : gc1),
+                          0, 0, w->width-1, w->height-1);
+          XDrawRectangle (s->dpy, w->mask,   gc1,
+                          0, 0, w->width-1, w->height-1);
+        }
+
+      /* Draw foreground text */
+      XDrawString (s->dpy, w->pixmap, gc1, -w->lbearing, w->ascent,
+                   txt, strlen(txt));
+
+      /* Cheesy hack to draw a border */
+      /* (I should be able to do this in i*2 time instead of i*i time,
+         but I can't get it right, so fuck it.) */
+      XSetFunction (s->dpy, gc1, GXor);
+      for (i = -bw; i <= bw; i++)
+        for (j = -bw; j <= bw; j++)
+          XCopyArea (s->dpy, w->pixmap, w->mask, gc1,
+                     0, 0, w->width, w->height,
+                     i, j);
+
+      if (s->debug_p)
+        {
+          XSetFunction (s->dpy, gc1, GXset);
+          if (w->ascent != w->height)
+            {
+              /* baseline (on top of the characters) */
+              XDrawLine (s->dpy, w->pixmap, (se->dark_p ? gc0 : gc1),
+                         0, w->ascent, w->width-1, w->ascent);
+              XDrawLine (s->dpy, w->mask,   gc1,
+                         0, w->ascent, w->width-1, w->ascent);
+            }
+
+          if (w->lbearing != 0)
+            {
+              /* left edge of charcell */
+              XDrawLine (s->dpy, w->pixmap, (se->dark_p ? gc0 : gc1),
+                         w->lbearing, 0, w->lbearing, w->height-1);
+              XDrawLine (s->dpy, w->mask,   gc1,
+                         w->lbearing, 0, w->lbearing, w->height-1);
+            }
+
+          if (w->rbearing != w->width)
+            {
+              /* right edge of charcell */
+              XDrawLine (s->dpy, w->pixmap, (se->dark_p ? gc0 : gc1),
+                         w->rbearing, 0, w->rbearing, w->height-1);
+              XDrawLine (s->dpy, w->mask,   gc1,
+                         w->rbearing, 0, w->rbearing, w->height-1);
+            }
+        }
+
+      XFreeGC (s->dpy, gc0);
+      XFreeGC (s->dpy, gc1);
+    }
+
+  w->text = txt;
+  return w;
+}
+
+
+static void
+free_word (state *s, word *w)
+{
+  if (w->text)   free (w->text);
+  if (w->pixmap) XFreePixmap (s->dpy, w->pixmap);
+  if (w->mask)   XFreePixmap (s->dpy, w->mask);
+}
+
+
+static sentence *
+new_sentence (state *s)
+{
+  static int id = 0;
+  XGCValues gcv;
+  sentence *se = (sentence *) calloc (1, sizeof (*se));
+  se->fg_gc = XCreateGC (s->dpy, s->b, 0, &gcv);
+  se->anim_state = IN;
+  se->id = ++id;
+  return se;
+}
+
+
+static void
+free_sentence (state *s, sentence *se)
+{
+  int i;
+  for (i = 0; i < se->nwords; i++)
+    free_word (s, se->words[i]);
+  if (se->words) free (se->words);
+
+  if (se->fg.flags)
+    XFreeColors (s->dpy, s->xgwa.colormap, &se->fg.pixel, 1, 0);
+  if (se->bg.flags)
+    XFreeColors (s->dpy, s->xgwa.colormap, &se->bg.pixel, 1, 0);
+
+  if (se->font_name) free (se->font_name);
+  if (se->font) XFreeFont (s->dpy, se->font);
+  if (se->fg_gc) XFreeGC (s->dpy, se->fg_gc);
+
+  free (se);
+}
+
+
+/* free the word, and put its text back at the front of the input queue,
+   to be read next time. */
+static void
+unread_word (state *s, word *w)
+{
+  if (unread_word_text)
+    abort();
+  unread_word_text = w->text;
+  w->text = 0;
+  free_word (s, w);
+}
+
+
+/* Divide each of the words in the sentence into one character words,
+   without changing the positions of those characters.
+ */
+static void
+split_words (state *s, sentence *se)
+{
+  word **words2;
+  int nwords2 = 0;
+  int i, j;
+  for (i = 0; i < se->nwords; i++)
+    nwords2 += strlen (se->words[i]->text);
+
+  words2 = (word **) calloc (nwords2, sizeof(*words2));
+
+  for (i = 0, j = 0; i < se->nwords; i++)
+    {
+      word *ow = se->words[i];
+      int L = strlen (ow->text);
+      int k;
+
+      int x  = ow->x;
+      int y  = ow->y;
+      int sx = ow->start_x;
+      int sy = ow->start_y;
+      int tx = ow->target_x;
+      int ty = ow->target_y;
+
+      for (k = 0; k < L; k++)
+        {
+          char *t2 = malloc (2);
+          word *w2;
+          int xoff, yoff;
+
+          t2[0] = ow->text[k];
+          t2[1] = 0;
+          w2 = new_word (s, se, t2, True);
+          words2[j++] = w2;
+
+          xoff = (w2->lbearing - ow->lbearing);
+          yoff = (ow->ascent - w2->ascent);
+
+          w2->x        = x  + xoff;
+          w2->y        = y  + yoff;
+          w2->start_x  = sx + xoff;
+          w2->start_y  = sy + yoff;
+          w2->target_x = tx + xoff;
+          w2->target_y = ty + yoff;
+
+          x  += w2->rbearing;
+          sx += w2->rbearing;
+          tx += w2->rbearing;
+        }
+
+      free_word (s, ow);
+      se->words[i] = 0;
+    }
+  free (se->words);
+
+  se->words = words2;
+  se->nwords = nwords2;
+}
+
+
+/* Set the source or destination position of the words to be somewhere
+   off screen.
+ */
+static void
+scatter_sentence (state *s, sentence *se)
+{
+  int i = 0;
+  int off = 100;
+
+  int flock_p = ((random() % 4) == 0);
+  int mode = (flock_p ? (random() % 12) : 0);
+
+  for (i = 0; i < se->nwords; i++)
+    {
+      word *w = se->words[i];
+      int x, y;
+      int r = (flock_p ? mode : (random() % 4));
+      switch (r)
+        {
+          /* random positions on the edges */
+
+        case 0:
+          x = -off - w->width;
+          y = random() % s->xgwa.height;
+          break;
+        case 1:
+          x = off + s->xgwa.width;
+          y = random() % s->xgwa.height;
+          break;
+        case 2:
+          x = random() % s->xgwa.width;
+          y = -off - w->height;
+          break;
+        case 3:
+          x = random() % s->xgwa.width;
+          y = off + s->xgwa.height;
+          break;
+
+          /* straight towards the edges */
+
+        case 4:
+          x = -off - w->width;
+          y = w->target_y;
+          break;
+        case 5:
+          x = off + s->xgwa.width;
+          y = w->target_y;
+          break;
+        case 6:
+          x = w->target_x;
+          y = -off - w->height;
+          break;
+        case 7:
+          x = w->target_x;
+          y = off + s->xgwa.height;
+          break;
+
+          /* corners */
+
+        case 8:
+          x = -off - w->width;
+          y = -off - w->height;
+          break;
+        case 9:
+          x = -off - w->width;
+          y =  off + s->xgwa.height;
+          break;
+        case 10:
+          x =  off + s->xgwa.width;
+          y =  off + s->xgwa.height;
+          break;
+        case 11:
+          x =  off + s->xgwa.width;
+          y = -off - w->height;
+          break;
+
+        default:
+          abort();
+          break;
+        }
+
+      if (se->anim_state == IN)
+        {
+          w->start_x = x;
+          w->start_y = y;
+        }
+      else
+        {
+          w->start_x = w->x;
+          w->start_y = w->y;
+          w->target_x = x;
+          w->target_y = y;
+        }
+
+      w->nticks = ((100 + ((random() % 140) +
+                           (random() % 140) +
+                           (random() % 140)))
+                   / s->speed);
+      if (w->nticks < 2)
+        w->nticks = 2;
+      w->tick = 0;
+    }
+}
+
+
+/* Set the source position of the words to be off the right side,
+   and the destination to be off the left side.
+ */
+static void
+aim_sentence (state *s, sentence *se)
+{
+  int i = 0;
+  int nticks;
+  int yoff = 0;
+
+  if (se->nwords <= 0) abort();
+
+  /* Have the sentence shift up or down a little bit; not too far, and
+     never let it fall off the top or bottom of the screen before its
+     last character has reached the left edge.
+   */
+  for (i = 0; i < 10; i++)
+    {
+      int ty = random() % (s->xgwa.height - se->words[0]->ascent);
+      yoff = ty - se->words[0]->target_y;
+      if (yoff < s->xgwa.height/3)  /* this one is ok */
+        break;
+    }
+
+  for (i = 0; i < se->nwords; i++)
+    {
+      word *w = se->words[i];
+      w->start_x   = w->target_x + s->xgwa.width;
+      w->target_x -= se->width;
+      w->start_y   = w->target_y;
+      w->target_y += yoff;
+    }
+
+  nticks = ((se->words[0]->start_x - se->words[0]->target_x)
+            / (s->speed * 10));
+  nticks *= (frand(0.9) + frand(0.9) + frand(0.9));
+
+  if (nticks < 2)
+    nticks = 2;
+
+  for (i = 0; i < se->nwords; i++)
+    {
+      word *w = se->words[i];
+      w->nticks = nticks;
+      w->tick = 0;
+    }
+}
+
+
+/* Randomize the order of the words in the list (since that changes
+   which ones are "on top".)
+ */
+static void
+shuffle_words (state *s, sentence *se)
+{
+  int i;
+  for (i = 0; i < se->nwords-1; i++)
+    {
+      int j = i + (random() % (se->nwords - i));
+      word *swap = se->words[i];
+      se->words[i] = se->words[j];
+      se->words[j] = swap;
+    }
+}
+
+
+/* qsort comparitor */
+static int
+cmp_sentences (const void *aa, const void *bb)
+{
+  const sentence *a = *(sentence **) aa;
+  const sentence *b = *(sentence **) bb;
+  return ((a ? a->id : 999999) - (b ? b->id : 999999));
+}
+
+
+/* Sort the sentences by id, so that sentences added later are on top.
+ */
+static void
+sort_sentences (state *s)
+{
+  qsort (s->sentences, s->nsentences, sizeof(*s->sentences), cmp_sentences);
+}
+
+
+/* Re-pick the colors of the text and border
+ */
+static void
+recolor (state *s, sentence *se)
+{
+  if (se->fg.flags)
+    XFreeColors (s->dpy, s->xgwa.colormap, &se->fg.pixel, 1, 0);
+  if (se->bg.flags)
+    XFreeColors (s->dpy, s->xgwa.colormap, &se->bg.pixel, 1, 0);
+
+  se->fg.flags  = DoRed|DoGreen|DoBlue;
+  se->bg.flags  = DoRed|DoGreen|DoBlue;
+
+  switch (random() % 2)
+    {
+    case 0:   /* bright fg, dim bg */
+      se->fg.red    = (random() % 0x8888) + 0x8888;
+      se->fg.green  = (random() % 0x8888) + 0x8888;
+      se->fg.blue   = (random() % 0x8888) + 0x8888;
+      se->bg.red    = (random() % 0x5555);
+      se->bg.green  = (random() % 0x5555);
+      se->bg.blue   = (random() % 0x5555);
+      break;
+
+    case 1:   /* bright bg, dim fg */
+      se->fg.red    = (random() % 0x4444);
+      se->fg.green  = (random() % 0x4444);
+      se->fg.blue   = (random() % 0x4444);
+      se->bg.red    = (random() % 0x4444) + 0xCCCC;
+      se->bg.green  = (random() % 0x4444) + 0xCCCC;
+      se->bg.blue   = (random() % 0x4444) + 0xCCCC;
+      break;
+
+    default:
+      abort();
+      break;
+    }
+
+  if (s->debug_p)
+    se->dark_p = (se->fg.red*2 + se->fg.green*3 + se->fg.blue <
+                  se->bg.red*2 + se->bg.green*3 + se->bg.blue);
+
+  if (XAllocColor (s->dpy, s->xgwa.colormap, &se->fg))
+    XSetForeground (s->dpy, se->fg_gc, se->fg.pixel);
+  if (XAllocColor (s->dpy, s->xgwa.colormap, &se->bg))
+    XSetBackground (s->dpy, se->fg_gc, se->bg.pixel);
+}
+
+
+static void
+align_line (state *s, sentence *se, int line_start, int x, int right)
+{
+  int off, j;
+  switch (se->alignment)
+    {
+    case LEFT:   off = 0;               break;
+    case CENTER: off = (right - x) / 2; break;
+    case RIGHT:  off = (right - x);     break;
+    default:     abort();               break;
+    }
+
+  if (off != 0)
+    for (j = line_start; j < se->nwords; j++)
+      se->words[j]->target_x += off;
+}
+
+
+/* Fill the sentence with new words: in "page" mode, fills the page
+   with text; in "scroll" mode, just makes one long horizontal sentence.
+   The sentence might have *no* words in it, if no text is currently
+   available.
+ */
+static void
+populate_sentence (state *s, sentence *se)
+{
+  int i = 0;
+  int left, right, top, x, y;
+  int space = 0;
+  int line_start = 0;
+  Bool done = False;
+
+  int array_size = 100;
+
+  se->move_chars_p = (s->mode == SCROLL ? False :
+                      (random() % 3) ? False : True);
+  se->alignment = (random() % 3);
+
+  recolor (s, se);
+
+  if (se->words)
+    {
+      for (i = 0; i < se->nwords; i++)
+        free_word (s, se->words[i]);
+      free (se->words);
+    }
+
+  se->words = (word **) calloc (array_size, sizeof(*se->words));
+  se->nwords = 0;
+
+  switch (s->mode)
+    {
+    case PAGE:
+      left  = random() % (s->xgwa.width / 3);
+      right = s->xgwa.width - (random() % (s->xgwa.width / 3));
+      top = random() % (s->xgwa.height * 2 / 3);
+      break;
+    case SCROLL:
+      left = 0;
+      right = s->xgwa.width;
+      top = random() % s->xgwa.height;
+      break;
+    default:
+      abort();
+      break;
+    }
+
+  x = left;
+  y = top;
+
+  while (!done)
+    {
+      char *txt = get_word_text (s);
+      word *w;
+      if (!txt)
+        {
+          if (se->nwords == 0)
+            return;            /* If the stream is empty, bail. */
+          else
+            break;             /* If EOF after some words, end of sentence. */
+        }
+
+      if (! se->font)           /* Got a word: need a font now */
+        {
+          pick_font (s, se);
+          if (y < se->font->ascent)
+            y += se->font->ascent;
+          space = XTextWidth (se->font, " ", 1);
+        }
+
+      w = new_word (s, se, txt, !se->move_chars_p);
+
+      /* If we have a few words, let punctuation terminate the sentence:
+         stop gathering more words if the last word ends in a period, etc. */
+      if (se->nwords >= 4)
+        {
+          char c = w->text[strlen(w->text)-1];
+          if (c == '.' || c == '?' || c == '!')
+            done = True;
+        }
+
+      /* If the sentence is kind of long already, terminate at commas, etc. */
+      if (se->nwords >= 12)
+        {
+          char c = w->text[strlen(w->text)-1];
+          if (c == ',' || c == ';' || c == ':' || c == '-' ||
+              c == ')' || c == ']' || c == '}')
+            done = True;
+        }
+
+      if (se->nwords >= 25)  /* ok that's just about enough out of you */
+        done = True;
+
+      if (s->mode == PAGE &&
+          x + w->rbearing > right)                     /* wrap line */
+        {
+          align_line (s, se, line_start, x, right);
+          line_start = se->nwords;
+
+          x = left;
+          y += se->font->ascent;
+
+          /* If we're close to the bottom of the screen, stop, and 
+             unread the current word.  (But not if this is the first
+             word, otherwise we might just get stuck on it.)
+           */
+          if (se->nwords > 0 &&
+              y + se->font->ascent > s->xgwa.height)
+            {
+              unread_word (s, w);
+              done = True;
+              break;
+            }
+        }
+
+      w->target_x = x + w->lbearing;
+      w->target_y = y - w->ascent;
+
+      x += w->rbearing + space;
+      se->width = x;
+
+      if (se->nwords >= (array_size - 1))
+        {
+          array_size += 100;
+          se->words = (word **) realloc (se->words,
+                                         array_size * sizeof(*se->words));
+          if (!se->words)
+            {
+              fprintf (stderr, "%s: out of memory (%d words)\n",
+                       progname, array_size);
+              exit (1);
+            }
+        }
+
+      se->words[se->nwords++] = w;
+    }
+
+  se->width -= space;
+
+  switch (s->mode)
+    {
+    case PAGE:
+      align_line (s, se, line_start, x, right);
+      if (se->move_chars_p)
+        split_words (s, se);
+      scatter_sentence (s, se);
+      shuffle_words (s, se);
+      break;
+    case SCROLL:
+      aim_sentence (s, se);
+      break;
+    default:
+      abort();
+      break;
+    }
+
+# ifdef DEBUG
+  if (s->debug_p)
+    {
+      fprintf (stderr, "%s: sentence %d:", progname, se->id);
+      for (i = 0; i < se->nwords; i++)
+        fprintf (stderr, " %s", se->words[i]->text);
+      fprintf (stderr, "\n");
+    }
+# endif
+}
+
+
+/* Render a single word object to the screen.
+ */
+static void
+draw_word (state *s, sentence *se, word *w)
+{
+  if (! w->pixmap) return;
+
+  if (w->x + w->width < 0 ||
+      w->y + w->height < 0 ||
+      w->x > s->xgwa.width ||
+      w->y > s->xgwa.height)
+    return;
+
+  XSetClipMask (s->dpy, se->fg_gc, w->mask);
+  XSetClipOrigin (s->dpy, se->fg_gc, w->x, w->y);
+  XCopyPlane (s->dpy, w->pixmap, s->b, se->fg_gc,
+              0, 0, w->width, w->height,
+              w->x, w->y,
+              1L);
+}
+
+
+/* If there is room for more sentences, add one.
+ */
+static void
+more_sentences (state *s)
+{
+  int i;
+  Bool any = False;
+  for (i = 0; i < s->nsentences; i++)
+    {
+      sentence *se = s->sentences[i];
+      if (! se)
+        {
+          se = new_sentence (s);
+          populate_sentence (s, se);
+          if (se->nwords > 0)
+            s->spawn_p = False, any = True;
+          else
+            {
+              free_sentence (s, se);
+              se = 0;
+            }
+          s->sentences[i] = se;
+          if (se)
+            s->latest_sentence = se->id;
+          break;
+        }
+    }
+
+  if (any) sort_sentences (s);
+}
+
+
+/* Render all the words to the screen, and run the animation one step.
+ */
+static void
+draw_sentence (state *s, sentence *se)
+{
+  int i;
+  Bool moved = False;
+
+  if (! se) return;
+
+  for (i = 0; i < se->nwords; i++)
+    {
+      word *w = se->words[i];
+
+      switch (s->mode)
+        {
+        case PAGE:
+          if (se->anim_state != PAUSE &&
+              w->tick <= w->nticks)
+            {
+              int dx = w->target_x - w->start_x;
+              int dy = w->target_y - w->start_y;
+              double r = sin (w->tick * M_PI / (2 * w->nticks));
+              w->x = w->start_x + (dx * r);
+              w->y = w->start_y + (dy * r);
+
+              w->tick++;
+              if (se->anim_state == OUT && s->mode == PAGE)
+                w->tick++;  /* go out faster */
+              moved = True;
+            }
+          break;
+        case SCROLL:
+          {
+            int dx = w->target_x - w->start_x;
+            int dy = w->target_y - w->start_y;
+            double r = (double) w->tick / w->nticks;
+            w->x = w->start_x + (dx * r);
+            w->y = w->start_y + (dy * r);
+            w->tick++;
+            moved = (w->tick <= w->nticks);
+
+            /* Launch a new sentence when:
+               - the front of this sentence is almost off the left edge;
+               - the end of this sentence is almost on screen.
+               - or, randomly
+             */
+            if (se->anim_state != OUT &&
+                i == 0 &&
+                se->id == s->latest_sentence)
+              {
+                Bool new_p = (w->x < (s->xgwa.width * 0.4) &&
+                              w->x + se->width < (s->xgwa.width * 2.1));
+                Bool rand_p = (new_p ? 0 : !(random() % 2000));
+
+                if (new_p || rand_p)
+                  {
+                    se->anim_state = OUT;
+                    s->spawn_p = True;
+# ifdef DEBUG
+                    if (s->debug_p)
+                      fprintf (stderr, "%s: OUT   %d (x2 = %d%s)\n",
+                               progname, se->id,
+                               se->words[0]->x + se->width,
+                               rand_p ? " randomly" : "");
+# endif
+                  }
+              }
+          }
+          break;
+        default:
+          abort();
+          break;
+        }
+
+      draw_word (s, se, w);
+    }
+
+  if (moved && se->anim_state == PAUSE)
+    abort();
+
+  if (! moved)
+    {
+      switch (se->anim_state)
+        {
+        case IN:
+          se->anim_state = PAUSE;
+          se->pause_tick = (se->nwords * 7 * s->linger);
+          if (se->move_chars_p)
+            se->pause_tick /= 5;
+          scatter_sentence (s, se);
+          shuffle_words (s, se);
+# ifdef DEBUG
+          if (s->debug_p)
+            fprintf (stderr, "%s: PAUSE %d\n", progname, se->id);
+# endif
+          break;
+        case PAUSE:
+          if (--se->pause_tick <= 0)
+            {
+              se->anim_state = OUT;
+              s->spawn_p = True;
+# ifdef DEBUG
+              if (s->debug_p)
+                fprintf (stderr, "%s: OUT   %d\n", progname, se->id);
+# endif
+            }
+          break;
+        case OUT:
+# ifdef DEBUG
+          if (s->debug_p)
+            fprintf (stderr, "%s: DEAD  %d\n", progname, se->id);
+# endif
+          {
+            int j;
+            for (j = 0; j < s->nsentences; j++)
+              if (s->sentences[j] == se)
+                s->sentences[j] = 0;
+            free_sentence (s, se);
+          }
+          break;
+        default:
+          abort();
+          break;
+        }
+    }
+}
+
+
+/* Render all the words to the screen, and run the animation one step.
+   Clear screen first, swap buffers after.
+ */
+static void
+draw_fontglide (state *s)
+{
+  int i;
+
+  if (s->spawn_p)
+    more_sentences (s);
+
+  if (!s->trails_p)
+    XFillRectangle (s->dpy, s->b, s->bg_gc,
+                    0, 0, s->xgwa.width, s->xgwa.height);
+
+  for (i = 0; i < s->nsentences; i++)
+    draw_sentence (s, s->sentences[i]);
+
+#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
+  if (s->backb)
+    {
+      XdbeSwapInfo info[1];
+      info[0].swap_window = s->window;
+      info[0].swap_action = (s->dbeclear_p ? XdbeBackground : XdbeUndefined);
+      XdbeSwapBuffers (s->dpy, info, 1);
+    }
+  else
+#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
+  if (s->dbuf)
+    {
+      XCopyArea (s->dpy, s->b, s->window, s->bg_gc,
+                0, 0, s->xgwa.width, s->xgwa.height, 0, 0);
+    }
+}
+
+
+static void
+handle_events (state *s)
+{
+  while (XPending (s->dpy))
+    {
+      XEvent event;
+      XNextEvent (s->dpy, &event);
+
+      if (event.xany.type == ConfigureNotify)
+        {
+          XGetWindowAttributes (s->dpy, s->window, &s->xgwa);
+
+          if (s->dbuf && (s->ba))
+            {
+              XFreePixmap (s->dpy, s->ba);
+              s->ba = XCreatePixmap (s->dpy, s->window, 
+                                    s->xgwa.width, s->xgwa.height,
+                                    s->xgwa.depth);
+              XFillRectangle (s->dpy, s->ba, s->bg_gc, 0, 0, 
+                              s->xgwa.width, s->xgwa.height);
+              s->b = s->ba;
+            }
+        }
+
+      screenhack_handle_event (s->dpy, &event);
+    }
+}
+
+
+\f
+/* Subprocess.
+   (This bit mostly cribbed from phosphor.c)
+ */
+
+static void
+subproc_cb (XtPointer closure, int *source, XtInputId *id)
+{
+  state *s = (state *) closure;
+  s->input_available_p = True;
+}
+
+
+static void
+launch_text_generator (state *s)
+{
+  char *oprogram = get_string_resource ("program", "Program");
+  char *program;
+
+  program = (char *) malloc (strlen (oprogram) + 10);
+  strcpy (program, "( ");
+  strcat (program, oprogram);
+  strcat (program, " ) 2>&1");
+
+  if (s->debug_p)
+    fprintf (stderr, "%s: forking: %s\n", progname, program);
+
+  if ((s->pipe = popen (program, "r")))
+    {
+      s->pipe_id =
+        XtAppAddInput (app, fileno (s->pipe),
+                       (XtPointer) (XtInputReadMask | XtInputExceptMask),
+                       subproc_cb, (XtPointer) s);
+    }
+  else
+    {
+      perror (program);
+    }
+}
+
+
+static void
+relaunch_generator_timer (XtPointer closure, XtIntervalId *id)
+{
+  state *s = (state *) closure;
+  launch_text_generator (s);
+}
+
+
+/* When the subprocess has generated some output, this reads as much as it
+   can into s->buf at s->buf_tail.
+ */
+static void
+drain_input (state *s)
+{
+  /* allow subproc_cb() to run, if the select() down in Xt says that
+     input is available.  This tells us whether we can read() without
+     blocking. */
+  if (! s->input_available_p)
+    if (XtAppPending (app) & (XtIMTimer|XtIMAlternateInput))
+      XtAppProcessEvent (app, XtIMTimer|XtIMAlternateInput);
+
+  if (! s->pipe) return;
+  if (! s->input_available_p) return;
+  s->input_available_p = False;
+
+  if (s->buf_tail < sizeof(s->buf) - 2)
+    {
+      int target = sizeof(s->buf) - s->buf_tail - 2;
+      int n;
+      n = read (fileno (s->pipe),
+                (void *) (s->buf + s->buf_tail),
+                target);
+      if (n > 0)
+        {
+          s->buf_tail += n;
+          s->buf[s->buf_tail] = 0;
+        }
+      else
+        {
+          XtRemoveInput (s->pipe_id);
+          s->pipe_id = 0;
+          pclose (s->pipe);
+          s->pipe = 0;
+
+          /* If the process didn't print a terminating newline, add one. */
+          if (s->buf_tail > 1 &&
+              s->buf[s->buf_tail-1] != '\n')
+            {
+              s->buf[s->buf_tail++] = '\n';
+              s->buf[s->buf_tail] = 0;
+            }
+
+          /* Then add one more, to make sure there's a sentence break at EOF.
+           */
+          s->buf[s->buf_tail++] = '\n';
+          s->buf[s->buf_tail] = 0;
+
+          /* Set up a timer to re-launch the subproc in a bit. */
+          XtAppAddTimeOut (app, s->subproc_relaunch_delay,
+                           relaunch_generator_timer,
+                           (XtPointer) s);
+        }
+    }
+}
+
+\f
+/* Window setup and resource loading */
+
+static state *
+init_fontglide (Display *dpy, Window window)
+{
+  XGCValues gcv;
+  state *s = (state *) calloc (1, sizeof(*s));
+  s->dpy = dpy;
+  s->window = window;
+
+  XGetWindowAttributes (s->dpy, s->window, &s->xgwa);
+
+  s->font_override = get_string_resource ("font", "Font");
+  if (s->font_override && (!*s->font_override || *s->font_override == '('))
+    s->font_override = 0;
+
+  s->charset = get_string_resource ("fontCharset", "FontCharset");
+  s->border_width = get_integer_resource ("fontBorderWidth", "Integer");
+  if (s->border_width < 0 || s->border_width > 20)
+    s->border_width = 1;
+
+  s->speed = get_float_resource ("speed", "Float");
+  if (s->speed <= 0 || s->speed > 200)
+    s->speed = 1;
+
+  s->linger = get_float_resource ("linger", "Float");
+  if (s->linger <= 0 || s->linger > 200)
+    s->linger = 1;
+
+  s->debug_p = get_boolean_resource ("debug", "Debug");
+  s->trails_p = get_boolean_resource ("trails", "Trails");
+
+  s->dbuf = get_boolean_resource ("doubleBuffer", "Boolean");
+  s->dbeclear_p = get_boolean_resource ("useDBEClear", "Boolean");
+
+  if (s->trails_p) s->dbuf = False;  /* don't need it in this case */
+
+  {
+    char *ss = get_string_resource ("mode", "Mode");
+    if (!ss || !*ss || !strcasecmp (ss, "random"))
+      s->mode = ((random() % 2) ? SCROLL : PAGE);
+    else if (!strcasecmp (ss, "scroll"))
+      s->mode = SCROLL;
+    else if (!strcasecmp (ss, "page"))
+      s->mode = PAGE;
+    else
+      {
+        fprintf (stderr,
+                "%s: `mode' must be `scroll', `page', or `random', not `%s'\n",
+                 progname, ss);
+      }
+  }
+
+  if (s->dbuf)
+    {
+#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
+      if (s->dbeclear_p)
+        s->b = xdbe_get_backbuffer (dpy, window, XdbeBackground);
+      else
+        s->b = xdbe_get_backbuffer (dpy, window, XdbeUndefined);
+      s->backb = s->b;
+#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
+
+      if (!s->b)
+        {
+          s->ba = XCreatePixmap (s->dpy, s->window, 
+                                 s->xgwa.width, s->xgwa.height,
+                                 s->xgwa.depth);
+          s->b = s->ba;
+        }
+    }
+  else
+    {
+      s->b = s->window;
+    }
+
+  gcv.foreground = get_pixel_resource ("background", "Background",
+                                       s->dpy, s->xgwa.colormap);
+  s->bg_gc = XCreateGC (s->dpy, s->b, GCForeground, &gcv);
+
+  s->subproc_relaunch_delay = 2 * 1000;
+
+  launch_text_generator (s);
+
+  s->nsentences = 5; /* #### */
+  s->sentences = (sentence **) calloc (s->nsentences, sizeof (sentence *));
+  s->spawn_p = True;
+
+  return s;
+}
+
+
+\f
+char *progclass = "FontGlide";
+
+char *defaults [] = {
+  ".background:                #000000",
+  ".foreground:                #DDDDDD",
+  ".borderColor:       #555555",
+  "*delay:             10000",
+  "*program:         " FORTUNE_PROGRAM,
+  "*mode:               random",
+  ".font:               (default)",
+  "*fontCharset:        iso8859-1",
+  "*fontBorderWidth:    2",
+  "*speed:              1.0",
+  "*linger:             1.0",
+  "*trails:             False",
+  "*debug:              False",
+  "*doubleBuffer:      True",
+#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
+  "*useDBE:            True",
+  "*useDBEClear:       True",
+#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
+  0
+};
+
+XrmOptionDescRec options [] = {
+  { "-scroll",         ".mode",            XrmoptionNoArg, "scroll" },
+  { "-page",           ".mode",            XrmoptionNoArg, "page"   },
+  { "-random",         ".mode",            XrmoptionNoArg, "random" },
+  { "-delay",          ".delay",           XrmoptionSepArg, 0 },
+  { "-speed",          ".speed",           XrmoptionSepArg, 0 },
+  { "-linger",         ".linger",          XrmoptionSepArg, 0 },
+  { "-program",                ".program",         XrmoptionSepArg, 0 },
+  { "-font",           ".font",            XrmoptionSepArg, 0 },
+  { "-fn",             ".font",            XrmoptionSepArg, 0 },
+  { "-bw",             ".fontBorderWidth", XrmoptionSepArg, 0 },
+  { "-trails",         ".trails",          XrmoptionNoArg,  "True"  },
+  { "-no-trails",      ".trails",          XrmoptionNoArg,  "False" },
+  { "-db",             ".doubleBuffer",    XrmoptionNoArg,  "True"  },
+  { "-no-db",          ".doubleBuffer",    XrmoptionNoArg,  "False" },
+  { "-debug",          ".debug",           XrmoptionNoArg,  "True"  },
+  { 0, 0, 0, 0 }
+};
+
+
+void
+screenhack (Display *dpy, Window window)
+{
+  state *s = init_fontglide (dpy, window);
+  int delay = get_integer_resource ("delay", "Integer");
+
+  while (1)
+    {
+      handle_events (s);
+      draw_fontglide (s);
+      XSync (dpy, False);
+      if (delay) usleep (delay);
+    }
+}