http://www.jwz.org/xscreensaver/xscreensaver-5.08.tar.gz
[xscreensaver] / hacks / glx / jigsaw.c
diff --git a/hacks/glx/jigsaw.c b/hacks/glx/jigsaw.c
new file mode 100644 (file)
index 0000000..0d9a314
--- /dev/null
@@ -0,0 +1,1165 @@
+/* xscreensaver, Copyright (c) 1997-2008 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.
+ *
+ * Written as an Xlib program some time in 1997.
+ * Rewritten as an OpenGL program 24-Aug-2008.
+ */
+
+/*
+  Currently, we do this:
+
+    - start pieces off screen and move them in.
+    - when they land, they fill the puzzle grid with a shuffled
+      puzzle (pieces are rotated, too).
+    - swap random pairs of pieces until the puzzle is solved.
+    - scatter the pieces off screen (resulting in black).
+    - load new image and repeat.
+
+  Another idea would be to show the puzzle being solved the way
+  a person would do it:
+
+    - start off with black screen.
+    - assume knowledge of size of grid (position of corners).
+    - find a corner piece, and place it.
+    - while puzzle unsolved:
+      - pick a random piece;
+      - if it is the correct piece for any open edge, place it;
+      - if it fits physically in any rotation at any open edge,
+        place it, then toss it back (show the fake-out).
+      - if it doesn't fit at all, don't animate it at all.
+
+   This would take a long time to solve, I think...
+
+   An even harder idea would involve building up completed "clumps"
+   and sliding them around (a coral growth / accretion approach)
+ */
+
+#define DEF_SPEED        "1.0"
+#define DEF_COMPLEXITY   "1.0"
+#define DEF_RESOLUTION   "100"
+#define DEF_THICKNESS    "0.06"
+#define DEF_WOBBLE       "True"
+#define DEF_DEBUG        "False"
+
+#define DEFAULTS  "*delay:             20000   \n" \
+                 "*showFPS:            False   \n" \
+                 "*wireframe:          False   \n" \
+                 "*desktopGrabber:   xscreensaver-getimage -no-desktop %s\n" \
+                 "*grabDesktopImages:  False   \n" \
+                 "*chooseRandomImages: True    \n"
+
+
+# define refresh_jigsaw 0
+# define release_jigsaw 0
+#undef countof
+#define countof(x) (sizeof((x))/sizeof((*x)))
+
+#include "xlockmore.h"
+#include "rotator.h"
+#include "gltrackball.h"
+#include "spline.h"
+#include "normals.h"
+#include "grab-ximage.h"
+
+#undef BELLRAND
+#define BELLRAND(n) ((frand((n)) + frand((n)) + frand((n))) / 3)
+
+#ifdef USE_GL /* whole file */
+
+#define TOP    0
+#define RIGHT  1
+#define BOTTOM 2
+#define LEFT   3
+
+#define IN    -1
+#define FLAT   0
+#define OUT    1
+
+typedef struct jigsaw_configuration jigsaw_configuration;
+
+typedef struct {
+  double x,y,z,r;      /* position and Z rotation (in degrees) */
+} XYZR;
+
+
+typedef struct {
+  jigsaw_configuration *jc;
+  int edge[4];
+  GLuint dlist;
+  int polys;
+
+  XYZR home;           /* correct position in puzzle */
+  XYZR current;                /* where it is right now */
+  XYZR from, to;       /* in transit from A to B */
+  double tick;         /* 0-1.0, how far from A to B */
+  double arc_height;   /* height of apex of curved path from A to B */
+  double tilt, max_tilt;
+
+} puzzle_piece;
+
+
+struct jigsaw_configuration {
+  GLXContext *glx_context;
+  trackball_state *trackball;
+  rotator *rot;
+  Bool button_down_p;
+
+  int puzzle_width;
+  int puzzle_height;
+  puzzle_piece *puzzle;
+
+  enum { PUZZLE_LOADING,
+         PUZZLE_UNSCATTER,
+         PUZZLE_SOLVE, 
+         PUZZLE_SCATTER } state;
+  double pausing;
+  double tick_speed;
+
+  GLuint texid;
+  GLfloat tex_x, tex_y, tex_width, tex_height, aspect;
+
+  GLuint line_thickness;
+};
+
+static jigsaw_configuration *sps = NULL;
+
+static GLfloat speed;
+static GLfloat complexity_arg;
+static int resolution_arg;
+static GLfloat thickness_arg;
+static Bool wobble_p;
+static Bool debug_p;
+
+static XrmOptionDescRec opts[] = {
+  { "-speed",      ".speed",       XrmoptionSepArg, 0 },
+  { "-complexity", ".complexity",  XrmoptionSepArg, 0 },
+  { "-resolution", ".resolution",  XrmoptionSepArg, 0 },
+  { "-thickness",  ".thickness",   XrmoptionSepArg, 0 },
+  { "-wobble",     ".wobble",      XrmoptionNoArg, "True" },
+  { "+wobble",     ".wobble",      XrmoptionNoArg, "False" },
+  { "-debug",      ".debug",       XrmoptionNoArg, "True" },
+};
+
+static argtype vars[] = {
+  {&speed,          "speed",       "Speed",       DEF_SPEED,      t_Float},
+  {&complexity_arg, "complexity",  "Complexity",  DEF_COMPLEXITY, t_Float},
+  {&resolution_arg, "resolution",  "Resolution",  DEF_RESOLUTION, t_Int},
+  {&thickness_arg,  "thickness",   "Thickness",   DEF_THICKNESS,  t_Float},
+  {&wobble_p,       "wobble",      "Wobble",      DEF_WOBBLE,     t_Bool},
+  {&debug_p,        "debug",       "Debug",       DEF_DEBUG,      t_Bool},
+};
+
+ENTRYPOINT ModeSpecOpt jigsaw_opts = {countof(opts), opts, countof(vars), vars, NULL};
+
+
+/* Returns a spline describing one edge of a puzzle piece of the given length.
+ */
+static spline *
+make_puzzle_curve (int pixels)
+{
+  double x0 = 0.0000, y0 =  0.0000;
+  double x1 = 0.3333, y1 =  0.1000;
+  double x2 = 0.4333, y2 =  0.0333;
+  double x3 = 0.4666, y3 = -0.0666;
+  double x4 = 0.3333, y4 = -0.1666;
+  double x5 = 0.3666, y5 = -0.2900;
+  double x6 = 0.5000, y6 = -0.3333;
+
+  spline *s = make_spline(20);
+  s->n_controls = 0;
+
+# define PT(x,y) \
+    s->control_x[s->n_controls] = pixels * (x); \
+    s->control_y[s->n_controls] = pixels * (y); \
+    s->n_controls++
+  PT (  x0, y0);
+  PT (  x1, y1);
+  PT (  x2, y2);
+  PT (  x3, y3);
+  PT (  x4, y4);
+  PT (  x5, y5);
+  PT (  x6, y6);
+  PT (1-x5, y5);
+  PT (1-x4, y4);
+  PT (1-x3, y3);
+  PT (1-x2, y2);
+  PT (1-x1, y1);
+  PT (1-x0, y0);
+# undef PT
+
+  compute_spline (s);
+  return s;
+}
+
+
+static void
+tess_error_cb (GLenum errorCode)
+{
+  fprintf (stderr, "%s: tesselation error: %s\n",
+           progname, gluErrorString(errorCode));
+  exit (0);
+}
+
+
+static void
+tess_combine_cb (GLdouble coords[3], GLdouble *d[4], GLfloat w[4], 
+                 GLdouble **dataOut)
+{
+  GLdouble *new = (GLdouble *) malloc (3 * sizeof(*new));
+  new[0] = coords[0];
+  new[1] = coords[1];
+  new[2] = coords[2];
+  *dataOut = new;
+}
+
+
+static void
+tess_vertex_cb (void *vertex_data, void *closure)
+{
+  puzzle_piece *p = (puzzle_piece *) closure;
+  GLdouble *v = (GLdouble *) vertex_data;
+  GLdouble x = v[0];
+  GLdouble y = v[1];
+  GLdouble z = v[2];
+
+  if (p)
+    {
+      GLfloat pw = p->jc->puzzle_width;
+      GLfloat ph = p->jc->puzzle_height;
+
+      GLfloat xx = x / (GLfloat) resolution_arg;  /* 0-1 from piece origin */
+      GLfloat yy = y / (GLfloat) resolution_arg;
+      GLdouble tx = (p->home.x + xx)      / pw;   /* 0-1 from puzzle origin */
+      GLdouble ty = (ph - p->home.y - yy) / ph;
+
+      tx = p->jc->tex_x + (tx * p->jc->tex_width);
+      ty = p->jc->tex_y + (ty * p->jc->tex_height);
+
+      glTexCoord2d (tx, ty);
+    }
+
+  glVertex3d (x, y, z);
+}
+
+
+
+/* Draws a puzzle piece.  The top/right/bottom/left_type args
+   indicate the direction the tabs point: 1 for out, -1 for in, 0 for flat.
+ */
+static int
+draw_piece (jigsaw_configuration *jc, puzzle_piece *p,
+            int resolution, GLfloat thickness,
+            int top_type, int right_type,
+            int bottom_type, int left_type,
+            Bool wire)
+{
+  spline *s = make_puzzle_curve (resolution);
+  GLdouble *pts = (GLdouble *) malloc (s->n_points * 4 * 3 * sizeof(*pts));
+  int polys = 0;
+  int i, o;
+  GLdouble z = resolution * thickness;
+
+  o = 0;
+  if (top_type == 0) {
+    pts[o++] = 0;
+    pts[o++] = 0; 
+    pts[o++] = z;
+
+    pts[o++] = resolution;
+    pts[o++] = 0;
+    pts[o++] = z;
+  } else {
+    for (i = 0; i < s->n_points; i++) {
+      pts[o++] = s->points[i].x;
+      pts[o++] = s->points[i].y * top_type;
+      pts[o++] = z;
+    }
+  }
+
+  if (right_type == 0) {
+    pts[o++] = resolution;
+    pts[o++] = resolution;
+    pts[o++] = z;
+  } else {
+    for (i = 1; i < s->n_points; i++) {
+      pts[o++] = resolution + s->points[i].y * (-right_type);
+      pts[o++] = s->points[i].x;
+      pts[o++] = z;
+    }
+  }
+
+  if (bottom_type == 0) {
+    pts[o++] = 0;
+    pts[o++] = resolution;
+    pts[o++] = z;
+  } else {
+    for (i = 1; i < s->n_points; i++) {
+      pts[o++] = s->points[s->n_points-i-1].x;
+      pts[o++] = resolution + s->points[s->n_points-i-1].y * (-bottom_type);
+      pts[o++] = z;
+    }
+  }
+
+  if (left_type == 0) {
+    pts[o++] = 0;
+    pts[o++] = 0;
+    pts[o++] = z;
+  } else {
+    for (i = 1; i < s->n_points; i++) {
+      pts[o++] = s->points[s->n_points-i-1].y * left_type;
+      pts[o++] = s->points[s->n_points-i-1].x;
+      pts[o++] = z;
+    }
+  }
+
+  free_spline (s);
+
+  { GLfloat s = 1.0 / resolution; glScalef (s, s, s); }
+
+  glPolygonMode (GL_FRONT_AND_BACK, wire ? GL_LINE : GL_FILL);
+
+  if (wire)
+    {
+      glDisable (GL_TEXTURE_2D);
+      glDisable (GL_BLEND);
+      glDisable (GL_LIGHTING);
+    }
+  else
+    {
+# ifndef  _GLUfuncptr
+#  define _GLUfuncptr void(*)(void)
+# endif
+      GLUtesselator *tess = gluNewTess();
+      gluTessCallback(tess, GLU_TESS_BEGIN,      (_GLUfuncptr)glBegin);
+      gluTessCallback(tess, GLU_TESS_VERTEX_DATA,(_GLUfuncptr)tess_vertex_cb);
+      gluTessCallback(tess, GLU_TESS_END,        (_GLUfuncptr)glEnd);
+      gluTessCallback(tess, GLU_TESS_COMBINE,    (_GLUfuncptr)tess_combine_cb);
+      gluTessCallback(tess, GLU_TESS_ERROR,      (_GLUfuncptr)tess_error_cb);
+
+      /* front face */
+      glEnable (GL_TEXTURE_2D);
+      glEnable (GL_BLEND);
+      glEnable (GL_LIGHTING);
+      glBindTexture(GL_TEXTURE_2D, jc->texid);
+      glFrontFace (GL_CCW);
+      glNormal3f (0, 0, 1);
+      gluTessBeginPolygon (tess, p);
+      gluTessBeginContour (tess);
+      for (i = 0; i < o; i += 3)
+        {
+          GLdouble *p = pts + i;
+          gluTessVertex (tess, p, p);
+          polys++;  /* not quite right but close */
+        }
+      gluTessEndContour(tess);
+      gluTessEndPolygon(tess);
+
+      /* back face */
+      glDisable (GL_TEXTURE_2D);
+      glFrontFace (GL_CW);
+      glNormal3f (0, 0, -1);
+      gluTessBeginPolygon (tess, 0);
+      gluTessBeginContour (tess);
+      for (i = 0; i < o; i += 3)
+        {
+          GLdouble *p = pts + i;
+          p[2] = -p[2];
+          gluTessVertex (tess, p, p);
+          polys++;  /* not quite right but close */
+        }
+      gluTessEndContour(tess);
+      gluTessEndPolygon(tess);
+      gluDeleteTess(tess);
+    }
+
+  /* side faces */
+  glFrontFace (GL_CCW);
+  glBegin (wire ? GL_LINES : GL_QUAD_STRIP);
+  for (i = 0; i < o; i += 3)
+    {
+      int j = (i+o-3) % o;
+      int k = (i+3)   % o;
+      GLdouble *p  = pts + i;
+      GLdouble *pj = pts + j;
+      GLdouble *pk = pts + k;
+
+      do_normal  (pj[0], pj[1], -pj[2],
+                  pj[0], pj[1],  pj[2],
+                  pk[0], pk[1],  pk[2]);
+
+      glVertex3f (p[0], p[1], -p[2]);
+      glVertex3f (p[0], p[1],  p[2]);
+      polys++;
+    }
+  glEnd();
+
+  if (! wire)
+    glColor3f (0.3, 0.3, 0.3);
+
+  glDisable (GL_TEXTURE_2D);
+  glDisable (GL_LIGHTING);
+  glLineWidth (jc->line_thickness);
+
+  glBegin (GL_LINE_LOOP);
+  for (i = 0; i < o; i += 3)
+    glVertex3f (pts[i], pts[i+1], pts[i+2]);
+  glEnd();
+  polys += o/3;
+
+  glBegin (GL_LINE_LOOP);
+  for (i = 0; i < o; i += 3)
+    glVertex3f (pts[i], pts[i+1], -pts[i+2]);
+  glEnd();
+  polys += o/3;
+
+  free (pts);
+
+  return polys;
+}
+
+
+static void
+free_puzzle_grid (jigsaw_configuration *jc)
+{
+  int i;
+  for (i = 0; i < jc->puzzle_width * jc->puzzle_height; i++)
+    glDeleteLists (jc->puzzle[i].dlist,  1);
+  free (jc->puzzle);
+  jc->puzzle = 0;
+  jc->puzzle_width = 0;
+  jc->puzzle_height = 0;
+}
+
+
+static void
+make_puzzle_grid (ModeInfo *mi)
+{
+  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
+  int wire = MI_IS_WIREFRAME(mi);
+  int x, y;
+  GLfloat size = (8 + (random() % 8)) * complexity_arg;
+
+  if (jc->puzzle)
+    free_puzzle_grid (jc);
+
+  if (wire)
+    jc->aspect = MI_WIDTH(mi) / (float) MI_HEIGHT(mi);
+
+  if (jc->aspect >= 1.0)
+    {
+      jc->puzzle_width  = size;
+      jc->puzzle_height = (size + 0.5) / jc->aspect;
+    }
+  else
+    {
+      jc->puzzle_width  = (size + 0.5) * jc->aspect;
+      jc->puzzle_height = size;
+    }
+
+  if (jc->puzzle_width  < 1) jc->puzzle_width  = 1;
+  if (jc->puzzle_height < 1) jc->puzzle_height = 1;
+
+  if (debug_p)
+    fprintf (stderr, "%s: grid  %4d x %-4d               (%.2f)\n", progname,
+             jc->puzzle_width, jc->puzzle_height,
+             ((float) jc->puzzle_width / jc->puzzle_height));
+
+  jc->puzzle = (puzzle_piece *) 
+    calloc (jc->puzzle_width * (jc->puzzle_height+1), sizeof(*jc->puzzle));
+
+  /* Randomize the right and bottom edge of each piece.
+     Match the left edge of the piece to the right to our right edge.
+     Match the top edge of the piece to the bottom to our bottom edge.
+   */
+  for (y = 0; y < jc->puzzle_height; y++)
+    for (x = 0; x < jc->puzzle_width; x++)
+      {
+        puzzle_piece *p = &jc->puzzle [y     * jc->puzzle_width + x];
+        puzzle_piece *r = &jc->puzzle [y     * jc->puzzle_width + x+1];
+        puzzle_piece *b = &jc->puzzle [(y+1) * jc->puzzle_width + x];
+        p->edge[RIGHT]  = (random() & 1) ? IN : OUT;
+        p->edge[BOTTOM] = (random() & 1) ? IN : OUT;
+        r->edge[LEFT]   = p->edge[RIGHT]  == IN ? OUT : IN;
+        b->edge[TOP]    = p->edge[BOTTOM] == IN ? OUT : IN;
+      }
+
+  /* tell each piece where it belongs. */
+  for (y = 0; y < jc->puzzle_height; y++)
+    for (x = 0; x < jc->puzzle_width; x++)
+      {
+        puzzle_piece *p = &jc->puzzle [y * jc->puzzle_width + x];
+        p->jc = jc;
+        p->home.x = x;
+        p->home.y = y;
+        p->current = p->home;
+
+        /* make sure the outer border is flat */
+        if (p->home.x == 0)  p->edge[LEFT] = FLAT;
+        if (p->home.y == 0)  p->edge[TOP]  = FLAT;
+        if (p->home.x == jc->puzzle_width-1)  p->edge[RIGHT]  = FLAT;
+        if (p->home.y == jc->puzzle_height-1) p->edge[BOTTOM] = FLAT;
+
+        /* generate the polygons */
+        p->dlist = glGenLists (1);
+        check_gl_error ("generating lists");
+        if (p->dlist <= 0) abort();
+
+        glNewList (p->dlist, GL_COMPILE);
+        p->polys += draw_piece (jc, p,
+                                resolution_arg, thickness_arg,
+                                p->edge[TOP], p->edge[RIGHT],
+                                p->edge[BOTTOM], p->edge[LEFT], 
+                                wire);
+        glEndList();
+      }
+}
+
+
+static void shuffle_grid (ModeInfo *mi);
+
+
+static void
+image_loaded_cb (const char *filename, XRectangle *geometry,
+                 int image_width, int image_height, 
+                 int texture_width, int texture_height,
+                 void *closure)
+{
+  ModeInfo *mi = (ModeInfo *) closure;
+  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
+
+  jc->tex_x      = geometry->x      / (float) texture_width;
+  jc->tex_y      = geometry->y      / (float) texture_height;
+  jc->tex_width  = geometry->width  / (float) texture_width; 
+  jc->tex_height = geometry->height / (float) texture_height;
+  jc->aspect     = geometry->width  / (float) geometry->height;
+
+  if (debug_p)
+    {
+      fprintf (stderr, "%s: image %s\n", progname, 
+               (filename ? filename : "(null)"));
+      fprintf (stderr, "%s: image %4d x %-4d + %4d + %-4d (%.2f)\n", progname,
+               geometry->width, geometry->height, geometry->x, geometry->y,
+               (float) geometry->width / geometry->height);
+      fprintf (stderr, "%s: tex   %4d x %-4d\n", progname,
+               texture_width, texture_height);
+      fprintf (stderr, "%s: tex   %4.2f x %4.2f + %4.2f + %4.2f (%.2f)\n",
+               progname,
+               jc->tex_width, jc->tex_height, jc->tex_x, jc->tex_y,
+               (jc->tex_width / jc->tex_height) *
+               (texture_width / texture_height));
+    }
+
+  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
+  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
+
+  make_puzzle_grid (mi);
+}
+
+
+static void
+load_image (ModeInfo *mi)
+{
+  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
+  load_texture_async (mi->xgwa.screen, mi->window,
+                      *jc->glx_context, 0, 0, 
+                      False, jc->texid,
+                      image_loaded_cb, mi);
+}
+
+
+/* Whether the two pieces are the same shape, when the second piece
+   is rotated by the given degrees.
+ */
+static Bool
+same_shape (puzzle_piece *p0, puzzle_piece *p1, int rotated_by)
+{
+  switch (rotated_by) 
+    {
+    case 0:
+      return (p0->edge[0] == p1->edge[0] &&
+              p0->edge[1] == p1->edge[1] &&
+              p0->edge[2] == p1->edge[2] &&
+              p0->edge[3] == p1->edge[3]);
+    case 90:
+      return (p0->edge[0] == p1->edge[1] &&
+              p0->edge[1] == p1->edge[2] &&
+              p0->edge[2] == p1->edge[3] &&
+              p0->edge[3] == p1->edge[0]);
+    case 180:
+      return (p0->edge[0] == p1->edge[2] &&
+              p0->edge[1] == p1->edge[3] &&
+              p0->edge[2] == p1->edge[0] &&
+              p0->edge[3] == p1->edge[1]);
+    case 270:
+      return (p0->edge[0] == p1->edge[3] &&
+              p0->edge[1] == p1->edge[0] &&
+              p0->edge[2] == p1->edge[1] &&
+              p0->edge[3] == p1->edge[2]);
+    default:
+      abort();
+    }
+}
+
+
+/* Returns the proper rotation for the piece at the given position. 
+ */
+static int
+proper_rotation (jigsaw_configuration *jc, puzzle_piece *p, 
+                 double x, double y)
+{
+  puzzle_piece *p1;
+  int cx = x;
+  int cy = y;
+  if (cx != x) abort();  /* must be in integral position! */
+  if (cy != y) abort();
+  p1 = &jc->puzzle [cy * jc->puzzle_width + cx];
+  if (same_shape (p, p1, 0))   return 0;
+  if (same_shape (p, p1, 90))  return 90;
+  if (same_shape (p, p1, 180)) return 180;
+  if (same_shape (p, p1, 270)) return 270;
+  abort();             /* these two pieces don't match in any rotation! */
+}
+
+
+/* Returns the piece currently at the given position. 
+ */
+static puzzle_piece *
+piece_at (jigsaw_configuration *jc, double x, double y)
+{
+  int npieces = jc->puzzle_width * jc->puzzle_height;
+  int i;
+  int cx = x;
+  int cy = y;
+  if (cx != x) abort();  /* must be in integral position! */
+  if (cy != y) abort();
+
+  for (i = 0; i < npieces; i++)
+    {
+      puzzle_piece *p = &jc->puzzle [i];
+      if (p->current.x == cx &&
+          p->current.y == cy)
+        return p;
+    }
+  abort();   /* no piece at that position? */
+}
+
+
+static void
+shuffle_grid (ModeInfo *mi)
+{
+  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
+  int max_tries = jc->puzzle_width * jc->puzzle_height;
+  int npieces = jc->puzzle_width * jc->puzzle_height;
+  int i;
+
+  for (i = 0; i < npieces; i++)
+    {
+      puzzle_piece *p0 = &jc->puzzle [i];
+      puzzle_piece *p1 = 0;
+      int k;
+
+      for (k = 0; k < max_tries; k++)
+        {
+          p1 = &jc->puzzle [random() % npieces];
+          if (same_shape (p0, p1, 0))   break;
+          if (same_shape (p0, p1, 90))  break;
+          if (same_shape (p0, p1, 180)) break;
+          if (same_shape (p0, p1, 270)) break;
+          p1 = 0;  /* mismatch */
+        }
+      if (p1 && p0 != p1)
+        {
+          XYZR s;
+          s = p0->current; p0->current = p1->current; p1->current = s;
+          p0->current.r = 
+            proper_rotation (jc, p0, p0->current.x, p0->current.y);
+          p1->current.r = 
+            proper_rotation (jc, p1, p1->current.x, p1->current.y);
+        }
+    }
+}
+
+
+/* We tend to accumulate floating point errors, e.g., z being 0.000001
+   after a move.  This makes sure float values that should be integral are.
+ */
+static void
+smooth_grid (ModeInfo *mi)
+{
+  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
+  int npieces = jc->puzzle_width * jc->puzzle_height;
+  int i;
+
+  for (i = 0; i < npieces; i++)
+    {
+      puzzle_piece *p = &jc->puzzle [i];
+# define SMOOTH(P) \
+        P.x = (int) (P.x + 0.5);                  \
+        P.y = (int) (P.y + 0.5);                  \
+        P.z = (int) (P.z + 0.5);                  \
+        P.r = (int) (P.r + 0.5)
+      SMOOTH(p->home);
+      SMOOTH(p->current);
+      SMOOTH(p->from);
+      SMOOTH(p->to);
+      if (p->tick <= 0.0001) p->tick = 0.0;
+      if (p->tick >= 0.9999) p->tick = 1.0;
+    }
+}
+
+
+static void
+begin_scatter (ModeInfo *mi, Bool unscatter_p)
+{
+  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
+  int npieces = jc->puzzle_width * jc->puzzle_height;
+  int i;
+  XYZR ctr = { 0, };
+  ctr.x = jc->puzzle_width  / 2;
+  ctr.y = jc->puzzle_height / 2;
+
+  for (i = 0; i < npieces; i++)
+    {
+      puzzle_piece *p = &jc->puzzle [i];
+      XYZ a;
+      double d, r, th;
+      p->tick = -frand(1.0);
+      p->from = p->current;
+
+      a.x = p->from.x - ctr.x; /* position relative to center */
+      a.y = p->from.y - ctr.y;
+
+      r = sqrt (a.x*a.x + a.y*a.y);
+      th = atan2 (a.x, a.y);
+
+      d = MAX (jc->puzzle_width, jc->puzzle_height) * 2;
+      r = r*r + d;
+
+      p->to.x = ctr.x + (r * sin (th));
+      p->to.y = ctr.y + (r * cos (th));
+      p->to.z = p->from.z;
+      p->to.r = ((int) p->from.r + (random() % 180)) % 360;
+      p->arc_height = frand(10.0);
+
+      if (unscatter_p)
+        {
+          XYZR s = p->to; p->to = p->from; p->from = s;
+          p->current = p->from;
+        }
+    }
+}
+
+
+static Bool
+solved_p (ModeInfo *mi)
+{
+  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
+  int npieces = jc->puzzle_width * jc->puzzle_height;
+  int i;
+
+  for (i = 0; i < npieces; i++)
+    {
+      puzzle_piece *p = &jc->puzzle [i];
+      if (p->current.x != p->home.x ||
+          p->current.y != p->home.y ||
+          p->current.z != p->home.z)
+        return False;
+    }
+  return True;
+}
+
+
+static void
+move_one_piece (ModeInfo *mi)
+{
+  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
+  int npieces = jc->puzzle_width * jc->puzzle_height;
+  int i;
+
+  for (i = 0; i < npieces * 100; i++)  /* shouldn't take that long */
+    {
+      int i = random() % npieces;
+      puzzle_piece *p0 = &jc->puzzle [i];
+      puzzle_piece *p1;
+
+      if (p0->current.x == p0->home.x &&
+          p0->current.y == p0->home.y &&
+          p0->current.z == p0->home.z)
+        continue;              /* piece already solved - try again */
+
+      /* swap with the piece occupying p0's home cell. */
+      p1 = piece_at (jc, p0->home.x, p0->home.y);
+
+      if (p0 == p1) abort();    /* should have caught this above */
+
+      p0->tick = 0;
+      p0->from = p0->current;
+      p0->to   = p1->current;
+      p0->to.r = proper_rotation (jc, p0, p0->to.x, p0->to.y);
+
+      p1->tick = 0;
+      p1->from = p1->current;
+      p1->to   = p0->current;
+      p1->to.r = proper_rotation (jc, p1, p1->to.x, p1->to.y);
+
+      p0->arc_height = 0.5 + frand(3.0);
+      p1->arc_height = 1.0 + frand(3.0);
+
+# define RTILT(V) \
+         V = 90 - BELLRAND(180);       \
+         if (! (random() % 5)) V *= 2; \
+         if (! (random() % 5)) V *= 2; \
+         if (! (random() % 5)) V *= 2
+      RTILT (p0->max_tilt);
+      RTILT (p1->max_tilt);
+# undef RTILT
+
+      if (debug_p)
+        fprintf (stderr, "%s: swapping %2d,%-2d with %2d,%d\n", progname,
+                 (int) p0->from.x, (int) p0->from.y,
+                 (int) p1->from.x, (int) p1->from.y);
+      return;
+    }
+
+  abort();  /* infinite loop! */
+}
+
+
+static Bool
+anim_tick (ModeInfo *mi)
+{
+  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
+  int npieces = jc->puzzle_width * jc->puzzle_height;
+  int i;
+  Bool finished_p = True;
+
+  if (jc->pausing > 0)
+    {
+      jc->pausing -= jc->tick_speed * speed;
+      if (debug_p && jc->pausing <= 0)
+        fprintf (stderr, "%s:   (done pausing)\n", progname);
+      return False;
+    }
+
+  for (i = 0; i < npieces; i++)
+    {
+      puzzle_piece *p = &jc->puzzle [i];
+      double tt;
+
+      if (p->tick >= 1.0) continue;    /* this piece is done */
+      finished_p = False;              /* not done */
+
+      p->tick += jc->tick_speed * speed;
+      if (p->tick > 1.0) p->tick = 1.0;
+
+      if (p->tick < 0.0) continue;     /* not yet started */
+
+      tt = 1 - sin (M_PI/2 - p->tick * M_PI/2);
+
+      p->current.x = p->from.x + ((p->to.x - p->from.x) * tt);
+      p->current.y = p->from.y + ((p->to.y - p->from.y) * tt);
+      p->current.z = p->from.z + ((p->to.z - p->from.z) * tt);
+      p->current.r = p->from.r + ((p->to.r - p->from.r) * tt);
+
+      p->current.z += p->arc_height * sin (p->tick * M_PI);
+
+      p->tilt = p->max_tilt * sin (p->tick * M_PI);
+
+    }
+
+  return finished_p;
+}
+
+
+static void
+animate (ModeInfo *mi)
+{
+  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
+  double slow = 0.01;
+  double fast = 0.04;
+
+  if (jc->button_down_p) return;
+
+  switch (jc->state)
+    {
+    case PUZZLE_LOADING:
+      if (!jc->puzzle) break;  /* still loading */
+      jc->tick_speed = slow;
+      shuffle_grid (mi);
+      smooth_grid (mi);
+      begin_scatter (mi, True);
+      jc->pausing = 0;
+      jc->state = PUZZLE_UNSCATTER;
+      if (debug_p) fprintf (stderr, "%s: unscattering\n", progname);
+      break;
+
+    case PUZZLE_UNSCATTER:
+      jc->tick_speed = slow;
+      if (anim_tick (mi))
+        {
+          smooth_grid (mi);
+          jc->pausing = 1.0;
+          jc->state = PUZZLE_SOLVE;
+          if (debug_p) fprintf (stderr, "%s: solving\n", progname);
+        }
+      break;
+
+    case PUZZLE_SOLVE:
+      jc->tick_speed = fast;
+      if (anim_tick (mi))
+        {
+          smooth_grid (mi);
+          if (solved_p (mi))
+            {
+              if (debug_p) fprintf (stderr, "%s: solved!\n", progname);
+              begin_scatter (mi, False);
+              jc->state = PUZZLE_SCATTER;
+              jc->pausing = 3.0;
+              if (debug_p) fprintf (stderr, "%s: scattering\n", progname);
+            }
+          else
+            {
+              move_one_piece (mi);
+              jc->pausing = 0.3;
+            }
+        }
+      break;
+
+    case PUZZLE_SCATTER:
+      jc->tick_speed = slow;
+      if (anim_tick (mi))
+        {
+          free_puzzle_grid (jc);
+          load_image (mi);
+          jc->state = PUZZLE_LOADING;
+          jc->pausing = 1.0;
+          if (debug_p) fprintf (stderr, "%s: loading\n", progname);
+        }
+      break;
+
+    default:
+      abort();
+    }
+}
+
+
+/* Window management, etc
+ */
+ENTRYPOINT void
+reshape_jigsaw (ModeInfo *mi, int width, int height)
+{
+  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
+  GLfloat h = (GLfloat) height / (GLfloat) width;
+
+  glViewport (0, 0, (GLint) width, (GLint) height);
+
+  glMatrixMode(GL_PROJECTION);
+  glLoadIdentity();
+  gluPerspective (30.0, 1/h, 1.0, 100.0);
+
+  glMatrixMode(GL_MODELVIEW);
+  glLoadIdentity();
+  gluLookAt( 0.0, 0.0, 30.0,
+             0.0, 0.0, 0.0,
+             0.0, 1.0, 0.0);
+
+  glClear(GL_COLOR_BUFFER_BIT);
+
+  jc->line_thickness = (MI_IS_WIREFRAME (mi) ? 1 : MAX (1, height / 300.0));
+}
+
+
+ENTRYPOINT Bool
+jigsaw_handle_event (ModeInfo *mi, XEvent *event)
+{
+  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
+
+  if (event->xany.type == ButtonPress &&
+      event->xbutton.button == Button1)
+    {
+      jc->button_down_p = True;
+      gltrackball_start (jc->trackball,
+                         event->xbutton.x, event->xbutton.y,
+                         MI_WIDTH (mi), MI_HEIGHT (mi));
+      return True;
+    }
+  else if (event->xany.type == ButtonRelease &&
+           event->xbutton.button == Button1)
+    {
+      jc->button_down_p = False;
+      return True;
+    }
+  else if (event->xany.type == ButtonPress &&
+           (event->xbutton.button == Button4 ||
+            event->xbutton.button == Button5 ||
+            event->xbutton.button == Button6 ||
+            event->xbutton.button == Button7))
+    {
+      gltrackball_mousewheel (jc->trackball, event->xbutton.button, 10,
+                              !!event->xbutton.state);
+      return True;
+    }
+  else if (event->xany.type == MotionNotify &&
+           jc->button_down_p)
+    {
+      gltrackball_track (jc->trackball,
+                         event->xmotion.x, event->xmotion.y,
+                         MI_WIDTH (mi), MI_HEIGHT (mi));
+      return True;
+    }
+  else if (event->xany.type == KeyPress)
+    {
+      KeySym keysym;
+      char c = 0;
+      XLookupString (&event->xkey, &c, 1, &keysym, 0);
+
+      if (c == ' ' || c == '\t' || c == '\r' || c == '\n')
+        {
+          begin_scatter (mi, False);
+          jc->state = PUZZLE_SCATTER;
+          return True;
+        }
+    }
+
+
+  return False;
+}
+
+
+ENTRYPOINT void 
+init_jigsaw (ModeInfo *mi)
+{
+  jigsaw_configuration *jc;
+  int wire = MI_IS_WIREFRAME(mi);
+
+  if (!sps) {
+    sps = (jigsaw_configuration *)
+      calloc (MI_NUM_SCREENS(mi), sizeof (jigsaw_configuration));
+    if (!sps) {
+      fprintf(stderr, "%s: out of memory\n", progname);
+      exit(1);
+    }
+  }
+  jc = &sps[MI_SCREEN(mi)];
+  jc->glx_context = init_GL(mi);
+
+  reshape_jigsaw (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
+
+  if (!wire)
+    {
+      GLfloat pos[4] = {0.05, 0.07, 1.00, 0.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] = {0.0, 1.0, 1.0, 1.0};
+
+      glLightfv(GL_LIGHT0, GL_POSITION, pos);
+      glLightfv(GL_LIGHT0, GL_AMBIENT,  amb);
+      glLightfv(GL_LIGHT0, GL_DIFFUSE,  dif);
+      glLightfv(GL_LIGHT0, GL_SPECULAR, spc);
+    }
+
+  jc->trackball = gltrackball_init ();
+  jc->rot = make_rotator (0, 0, 0, 0, speed * 0.002, True);
+
+  jc->state = PUZZLE_LOADING;
+
+  resolution_arg /= complexity_arg;
+
+  if (wire)
+    make_puzzle_grid (mi);
+  else
+    load_image (mi);
+}
+
+
+ENTRYPOINT void
+draw_jigsaw (ModeInfo *mi)
+{
+  jigsaw_configuration *jc = &sps[MI_SCREEN(mi)];
+  Display *dpy = MI_DISPLAY(mi);
+  Window window = MI_WINDOW(mi);
+  int wire = MI_IS_WIREFRAME(mi);
+
+  if (!jc->glx_context)
+    return;
+
+  animate (mi);
+
+  glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *(jc->glx_context));
+
+  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+  glPushMatrix ();
+
+  gltrackball_rotate (jc->trackball);
+
+  if (wobble_p && jc->puzzle)
+    {
+      double x, y, z;
+      double max = 60;
+      get_position (jc->rot, &x, &y, &z, !jc->button_down_p);
+      x = 1; /* always lean back */
+      glRotatef (max/2 - x*max, 1, 0, 0);
+      glRotatef (max/2 - z*max, 0, 1, 0);
+    }
+
+  mi->polygon_count = 0;
+
+  glEnable(GL_CULL_FACE);
+  glEnable(GL_DEPTH_TEST);
+  glEnable(GL_NORMALIZE);
+  glEnable(GL_LINE_SMOOTH);
+
+  if (jc->puzzle)
+    {
+      GLfloat s = 14.0 / jc->puzzle_height;
+      int x, y;
+
+      glScalef (s, s, s);
+      glTranslatef (-jc->puzzle_width / 2.0, -jc->puzzle_height / 2.0, 0);
+
+      if (! wire)
+        {
+          glEnable (GL_TEXTURE_2D);
+          glEnable (GL_BLEND);
+          glEnable (GL_LIGHTING);
+          glEnable (GL_LIGHT0);
+          glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+        }
+
+      for (y = 0; y < jc->puzzle_height; y++)
+        for (x = 0; x < jc->puzzle_width; x++)
+          {
+            puzzle_piece *p = &jc->puzzle [y * jc->puzzle_width + x];
+            glPushMatrix();
+            glTranslatef (p->current.x, p->current.y, p->current.z);
+            glTranslatef (0.5, 0.5, 0);
+            glRotatef (p->current.r, 0, 0, 1);
+            glRotatef (p->tilt, 0, 1, 0);
+            glTranslatef (-0.5, -0.5, 0);
+            glCallList(p->dlist);
+            mi->polygon_count += p->polys;
+            glPopMatrix();
+          }
+    }
+
+  glPopMatrix ();
+
+  if (mi->fps_p) do_fps (mi);
+  glFinish();
+
+  glXSwapBuffers(dpy, window);
+}
+
+XSCREENSAVER_MODULE ("Jigsaw", jigsaw)
+
+#endif /* USE_GL */