http://www.jwz.org/xscreensaver/xscreensaver-5.09.tar.gz
[xscreensaver] / hacks / glx / photopile.c
1 /* photopile, Copyright (c) 2008 Jens Kilian <jjk@acm.org>
2  * Based on carousel, Copyright (c) 2005-2008 Jamie Zawinski <jwz@jwz.org>
3  * Loads a sequence of images and shuffles them into a pile.
4  *
5  * Permission to use, copy, modify, distribute, and sell this software and its
6  * documentation for any purpose is hereby granted without fee, provided that
7  * the above copyright notice appear in all copies and that both that
8  * copyright notice and this permission notice appear in supporting
9  * documentation.  No representations are made about the suitability of this
10  * software for any purpose.  It is provided "as is" without express or
11  * implied warranty.
12  */
13
14 #define DEF_FONT "-*-helvetica-bold-r-normal-*-240-*"
15 #define DEFAULTS  "*count:           7         \n" \
16                   "*delay:           10000     \n" \
17                   "*wireframe:       False     \n" \
18                   "*showFPS:         False     \n" \
19                   "*fpsSolid:        True      \n" \
20                   "*useSHM:          True      \n" \
21                   "*font:          " DEF_FONT "\n" \
22                   "*desktopGrabber:  xscreensaver-getimage -no-desktop %s\n" \
23                   "*grabDesktopImages:   False \n" \
24                   "*chooseRandomImages:  True  \n"
25
26 # define refresh_photopile 0
27 # define release_photopile 0
28 # define photopile_handle_event 0
29
30 #undef countof
31 #define countof(x) (sizeof((x))/sizeof((*x)))
32
33 #ifndef HAVE_COCOA
34 # include <X11/Intrinsic.h>     /* for XrmDatabase in -debug mode */
35 #endif
36 #include <math.h>
37
38 #include "xlockmore.h"
39 #include "grab-ximage.h"
40 #include "texfont.h"
41
42 #ifdef USE_GL
43
44 # define DEF_SCALE          "0.4"
45 # define DEF_MAX_TILT       "50"
46 # define DEF_SPEED          "1.0"
47 # define DEF_DURATION       "5"
48 # define DEF_TITLES         "False"
49 # define DEF_MIPMAP         "True"
50 # define DEF_DEBUG          "False"
51
52 #define BELLRAND(n) ((frand((n)) + frand((n)) + frand((n))) / 3)
53
54 typedef struct {
55   GLfloat x, y;                 /* position on screen */
56   GLfloat angle;                /* rotation angle */
57
58 } position;
59
60 typedef struct {
61   Bool loaded_p;                /* true if image can be drawn */
62
63   char *title;                  /* the filename of this image */
64   int w, h;                     /* size in pixels of the image */
65   int tw, th;                   /* size in pixels of the texture */
66   XRectangle geom;              /* where in the image the bits are */
67
68   position pos[4];              /* control points for calculating position */
69
70   GLuint texid;                 /* GL texture ID */
71
72 } image;
73
74
75 typedef enum { EARLY, IN, NORMAL, LOADING, SHUFFLE } fade_mode;
76 static int fade_ticks = 60;
77
78 typedef struct {
79   ModeInfo *mi;
80   GLXContext *glx_context;
81
82   image *frames;                /* pointer to array of images */
83   int nframe;                   /* image being (resp. next to be) loaded */
84
85   texture_font_data *texfont;
86   int loading_sw, loading_sh;
87
88   time_t last_time, now;
89   int draw_tick;
90   fade_mode mode;
91   int mode_tick;
92
93 } photopile_state;
94
95 static photopile_state *sss = NULL;
96
97
98 /* Command-line arguments
99  */
100 static GLfloat scale;        /* Scale factor for loading images. */
101 static GLfloat max_tilt;     /* Maximum angle from vertical. */
102 static GLfloat speed;        /* Animation speed scale factor. */
103 static int duration;         /* Reload images after this long. */
104 static Bool mipmap_p;        /* Use mipmaps instead of single textures. */
105 static Bool titles_p;        /* Display image titles. */
106 static Bool debug_p;         /* Be loud and do weird things. */
107
108
109 static XrmOptionDescRec opts[] = {
110   {"-scale",        ".scale",         XrmoptionSepArg, 0 },
111   {"-maxTilt",      ".maxTilt",       XrmoptionSepArg, 0 },
112   {"-titles",       ".titles",        XrmoptionNoArg, "True"  },
113   {"-no-titles",    ".titles",        XrmoptionNoArg, "False" },
114   {"-mipmaps",      ".mipmap",        XrmoptionNoArg, "True"  },
115   {"-no-mipmaps",   ".mipmap",        XrmoptionNoArg, "False" },
116   {"-duration",     ".duration",      XrmoptionSepArg, 0 },
117   {"-debug",        ".debug",         XrmoptionNoArg, "True"  },
118   {"-font",         ".font",          XrmoptionSepArg, 0 },
119   {"-speed",        ".speed",         XrmoptionSepArg, 0 },
120 };
121
122 static argtype vars[] = {
123   { &scale,         "scale",        "Scale",        DEF_SCALE,       t_Float},
124   { &max_tilt,      "maxTilt",      "MaxTilt",      DEF_MAX_TILT,    t_Float},
125   { &mipmap_p,      "mipmap",       "Mipmap",       DEF_MIPMAP,      t_Bool},
126   { &debug_p,       "debug",        "Debug",        DEF_DEBUG,       t_Bool},
127   { &titles_p,      "titles",       "Titles",       DEF_TITLES,      t_Bool},
128   { &speed,         "speed",        "Speed",        DEF_SPEED,       t_Float},
129   { &duration,      "duration",     "Duration",     DEF_DURATION,    t_Int},
130 };
131
132 ENTRYPOINT ModeSpecOpt photopile_opts = {countof(opts), opts, countof(vars), vars, NULL};
133
134
135 /* Functions to interpolate between image positions.
136  */
137 static position
138 add_pos(position p, position q)
139 {
140   p.x += q.x;
141   p.y += q.y;
142   p.angle += q.angle;
143   return p;
144 }
145
146 static position
147 scale_pos(GLfloat t, position p)
148 {
149   p.x *= t;
150   p.y *= t;
151   p.angle *= t;
152   return p;
153 }
154
155 static position
156 linear_combination(GLfloat t, position p, position q)
157 {
158   return add_pos(scale_pos(1.0 - t, p), scale_pos(t, q));
159 }
160
161 static position
162 interpolate(GLfloat t, position p[4])
163 {
164   /* de Casteljau's algorithm, 4 control points */
165   position p10 = linear_combination(t, p[0], p[1]);
166   position p11 = linear_combination(t, p[1], p[2]);
167   position p12 = linear_combination(t, p[2], p[3]);
168
169   position p20 = linear_combination(t, p10, p11);
170   position p21 = linear_combination(t, p11, p12);
171
172   return linear_combination(t, p20, p21);
173 }
174
175 static position
176 offset_pos(position p, GLfloat th, GLfloat r)
177 {
178   p.x += cos(th) * r;
179   p.y += sin(th) * r;
180   p.angle = (frand(2.0) - 1.0) * max_tilt;
181   return p;
182 }
183
184 /* Calculate new positions for all images.
185  */
186 static void
187 set_new_positions(photopile_state *ss)
188 {
189   ModeInfo *mi = ss->mi;
190   int i;
191
192   for (i = 0; i < MI_COUNT(mi)+1; ++i)
193     {
194       image *frame = ss->frames + i;
195       GLfloat d = sqrt(frame->w*frame->w + frame->h*frame->h);
196       GLfloat leave = frand(M_PI * 2.0);
197       GLfloat enter = frand(M_PI * 2.0);
198
199       /* start position */
200       frame->pos[0] = frame->pos[3];
201
202       /* end position */
203       frame->pos[3].x = BELLRAND(MI_WIDTH(mi));
204       frame->pos[3].y = BELLRAND(MI_HEIGHT(mi));
205       frame->pos[3].angle = (frand(2.0) - 1.0) * max_tilt;
206
207       /* intermediate points */
208       frame->pos[1] = offset_pos(frame->pos[0], leave, d * (0.5 + frand(1.0)));
209       frame->pos[2] = offset_pos(frame->pos[3], enter, d * (0.5 + frand(1.0)));
210     }
211 }
212
213 /* Callback that tells us that the texture has been loaded.
214  */
215 static void
216 image_loaded_cb (const char *filename, XRectangle *geom,
217                  int image_width, int image_height,
218                  int texture_width, int texture_height,
219                  void *closure)
220 {
221   photopile_state *ss = (photopile_state *) closure;
222   ModeInfo *mi = ss->mi;
223   int wire = MI_IS_WIREFRAME(mi);
224   image *frame = ss->frames + ss->nframe;
225
226   if (wire)
227     {
228       frame->w = (int)(MI_WIDTH(mi)  * scale) - 1;
229       frame->h = (int)(MI_HEIGHT(mi) * scale) - 1;
230       if (frame->w <= 10) frame->w = 10;
231       if (frame->h <= 10) frame->h = 10;
232       frame->geom.width  = frame->w;
233       frame->geom.height = frame->h;
234       goto DONE;
235     }
236
237   if (image_width == 0 || image_height == 0)
238     exit (1);
239
240   glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
241   glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
242                    mipmap_p ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR);
243
244   glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
245   glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
246
247   frame->w  = image_width;
248   frame->h  = image_height;
249   frame->tw = texture_width;
250   frame->th = texture_height;
251   frame->geom = *geom;
252
253   if (frame->title)
254     free (frame->title);
255   frame->title = (filename ? strdup (filename) : 0);
256
257   if (frame->title)   /* strip filename to part after last /. */
258     {
259       char *s = strrchr (frame->title, '/');
260       if (s) strcpy (frame->title, s+1);
261     }
262
263   if (debug_p)
264     fprintf (stderr, "%s:   loaded %4d x %-4d  %4d x %-4d  \"%s\"\n",
265              progname,
266              frame->geom.width, 
267              frame->geom.height, 
268              frame->tw, frame->th,
269              (frame->title ? frame->title : "(null)"));
270
271  DONE:
272   frame->loaded_p = True;
273 }
274
275
276 /* Load a new file.
277  */
278 static void
279 load_image (ModeInfo *mi)
280 {
281   photopile_state *ss = &sss[MI_SCREEN(mi)];
282   int wire = MI_IS_WIREFRAME(mi);
283   image *frame = ss->frames + ss->nframe;
284
285   if (debug_p && !wire && frame->w != 0)
286     fprintf (stderr, "%s:  dropped %4d x %-4d  %4d x %-4d  \"%s\"\n",
287              progname, 
288              frame->geom.width, 
289              frame->geom.height, 
290              frame->tw, frame->th,
291              (frame->title ? frame->title : "(null)"));
292
293   frame->loaded_p = False;
294
295   if (wire)
296     image_loaded_cb (0, 0, 0, 0, 0, 0, ss);
297   else
298     {
299       int w = MI_WIDTH(mi);
300       int h = MI_HEIGHT(mi);
301       int size = (int)((w > h ? w : h) * scale);
302       if (size <= 10) size = 10;
303       load_texture_async (mi->xgwa.screen, mi->window, *ss->glx_context,
304                           size, size,
305                           mipmap_p, frame->texid, 
306                           image_loaded_cb, ss);
307     }
308 }
309
310
311 static void
312 loading_msg (ModeInfo *mi, int n)
313 {
314   photopile_state *ss = &sss[MI_SCREEN(mi)];
315   int wire = MI_IS_WIREFRAME(mi);
316   char text[100];
317   GLfloat scale;
318
319   if (wire) return;
320
321   if (n == 0)
322     sprintf (text, "Loading images...");
323   else
324     sprintf (text, "Loading images...  (%d%%)",
325              (int) (n * 100 / MI_COUNT(mi)));
326
327   if (ss->loading_sw == 0)    /* only do this once, so that the string doesn't move. */
328     ss->loading_sw = texture_string_width (ss->texfont, text, &ss->loading_sh);
329
330   scale = ss->loading_sh / (GLfloat) MI_HEIGHT(mi);
331
332   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
333
334   glMatrixMode(GL_PROJECTION);
335   glPushMatrix();
336   glLoadIdentity();
337
338   glMatrixMode(GL_MODELVIEW);
339   glPushMatrix();
340   glLoadIdentity();
341   gluOrtho2D(0, MI_WIDTH(mi), 0, MI_HEIGHT(mi));
342
343   glTranslatef ((MI_WIDTH(mi)  - ss->loading_sw) / 2,
344                 (MI_HEIGHT(mi) - ss->loading_sh) / 2,
345                 0);
346   glColor3f (1, 1, 0);
347   glEnable (GL_TEXTURE_2D);
348   glDisable (GL_DEPTH_TEST);
349   print_texture_string (ss->texfont, text);
350   glEnable (GL_DEPTH_TEST);
351   glPopMatrix();
352
353   glMatrixMode(GL_PROJECTION);
354   glPopMatrix();
355
356   glMatrixMode(GL_MODELVIEW);
357
358   glFinish();
359   glXSwapBuffers (MI_DISPLAY (mi), MI_WINDOW(mi));
360 }
361
362
363 static Bool
364 load_initial_images (ModeInfo *mi)
365 {
366   photopile_state *ss = &sss[MI_SCREEN(mi)];
367
368   if (ss->frames[ss->nframe].loaded_p)
369     {
370       /* The current image has been fully loaded. */
371       if (++ss->nframe < MI_COUNT(mi))
372         {
373           /* Start the next one loading.  (We run the image loader
374            * asynchronously, but we load them one at a time.)
375            */
376           load_image(mi);
377         }
378       else
379         {
380           /* loaded all initial images, start fading them in */
381           int i;
382
383           for (i = 0; i < ss->nframe; ++i)
384             {
385               ss->frames[i].pos[3].x = MI_WIDTH(mi) * 0.5;
386               ss->frames[i].pos[3].y = MI_HEIGHT(mi) * 0.5;
387               ss->frames[i].pos[3].angle = 0.0;
388             }
389           set_new_positions(ss);
390
391           ss->mode = IN;
392           ss->mode_tick = fade_ticks / speed;
393         }
394     }
395
396   loading_msg(mi, ss->nframe);
397   return (ss->mode != EARLY);
398 }
399
400
401 ENTRYPOINT void
402 reshape_photopile (ModeInfo *mi, int width, int height)
403 {
404   glViewport (0, 0, (GLint) width, (GLint) height);
405
406   glMatrixMode(GL_PROJECTION);
407   glLoadIdentity();
408
409   glMatrixMode(GL_MODELVIEW);
410   glLoadIdentity();
411   gluOrtho2D(0, MI_WIDTH(mi), 0, MI_HEIGHT(mi));
412
413   glClear(GL_COLOR_BUFFER_BIT);
414 }
415
416
417 /* Kludge to add "-v" to invocation of "xscreensaver-getimage" in -debug mode
418  */
419 static void
420 hack_resources (Display *dpy)
421 {
422 # ifndef HAVE_COCOA
423   char *res = "desktopGrabber";
424   char *val = get_string_resource (dpy, res, "DesktopGrabber");
425   char buf1[255];
426   char buf2[255];
427   XrmValue value;
428   XrmDatabase db = XtDatabase (dpy);
429   sprintf (buf1, "%.100s.%.100s", progname, res);
430   sprintf (buf2, "%.200s -v", val);
431   value.addr = buf2;
432   value.size = strlen(buf2);
433   XrmPutResource (&db, buf1, "String", &value);
434 # endif /* !HAVE_COCOA */
435 }
436
437
438 ENTRYPOINT void
439 init_photopile (ModeInfo *mi)
440 {
441   int screen = MI_SCREEN(mi);
442   photopile_state *ss;
443   int wire = MI_IS_WIREFRAME(mi);
444
445   if (sss == NULL) {
446     if ((sss = (photopile_state *)
447          calloc (MI_NUM_SCREENS(mi), sizeof(photopile_state))) == NULL)
448       return;
449   }
450   ss = &sss[screen];
451   ss->mi = mi;
452
453   if ((ss->glx_context = init_GL(mi)) != NULL) {
454     reshape_photopile (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
455   } else {
456     MI_CLEARWINDOW(mi);
457   }
458
459   glDisable (GL_LIGHTING);
460   glEnable (GL_DEPTH_TEST);
461   glDisable (GL_CULL_FACE);
462
463   if (! wire)
464     {
465       glShadeModel (GL_SMOOTH);
466       glEnable (GL_LINE_SMOOTH);
467       /* This gives us a transparent diagonal slice through each image! */
468       /* glEnable (GL_POLYGON_SMOOTH); */
469       glHint (GL_LINE_SMOOTH_HINT, GL_NICEST);
470       glEnable (GL_BLEND);
471       glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
472
473       glEnable (GL_POLYGON_OFFSET_FILL);
474       glPolygonOffset (1.0, 1.0);
475     }
476
477   ss->texfont = load_texture_font (MI_DISPLAY(mi), "font");
478
479   if (debug_p)
480     hack_resources (MI_DISPLAY (mi));
481
482   ss->frames = (image *)calloc (MI_COUNT(mi) + 1, sizeof(image));
483   ss->nframe = 0;
484   if (!wire)
485     {
486       int i;
487       for (i = 0; i < MI_COUNT(mi) + 1; ++i)
488         {
489           glGenTextures(1, &(ss->frames[i].texid));
490           if (ss->frames[i].texid <= 0) abort();
491         }
492     }
493
494   ss->mode = EARLY;
495   load_image(mi); /* start loading the first image */
496 }
497
498
499 static void
500 draw_image (ModeInfo *mi, int i, GLfloat t, GLfloat s, GLfloat z)
501 {
502   int wire = MI_IS_WIREFRAME(mi);
503   photopile_state *ss = &sss[MI_SCREEN(mi)];
504   image *frame = ss->frames + i;
505
506   GLfloat texw  = frame->geom.width  / (GLfloat) frame->tw;
507   GLfloat texh  = frame->geom.height / (GLfloat) frame->th;
508   GLfloat texx1 = frame->geom.x / (GLfloat) frame->tw;
509   GLfloat texy1 = frame->geom.y / (GLfloat) frame->th;
510   GLfloat texx2 = texx1 + texw;
511   GLfloat texy2 = texy1 + texh;
512
513   position pos = interpolate(t, frame->pos);
514   GLfloat w = frame->geom.width * 0.5;
515   GLfloat h = frame->geom.height * 0.5;
516
517   glBindTexture (GL_TEXTURE_2D, frame->texid);
518
519   glPushMatrix();
520
521   /* Position and scale this image.
522    */
523   glTranslatef (pos.x, pos.y, 0);
524   glRotatef (pos.angle, 0, 0, 1);
525   glScalef (s, s, 1);
526
527   /* Draw the image quad. */
528   if (! wire)
529     {
530       glColor3f (1, 1, 1);
531       glEnable (GL_TEXTURE_2D);
532       glBegin (GL_QUADS);
533       glTexCoord2f (texx1, texy2); glVertex3f (-w, -h, z);
534       glTexCoord2f (texx2, texy2); glVertex3f ( w, -h, z);
535       glTexCoord2f (texx2, texy1); glVertex3f ( w,  h, z);
536       glTexCoord2f (texx1, texy1); glVertex3f (-w,  h, z);
537       glEnd();
538     }
539
540   /* Draw a box around it.
541    */
542   glLineWidth (2.0);
543   glColor3f (0.5, 0.5, 0.5);
544   glDisable (GL_TEXTURE_2D);
545   glBegin (GL_LINE_LOOP);
546   glVertex3f (-w, -h, z);
547   glVertex3f ( w, -h, z);
548   glVertex3f ( w,  h, z);
549   glVertex3f (-w,  h, z);
550   glEnd();
551
552   /* Draw a title under the image.
553    */
554   if (titles_p)
555     {
556       int sw, sh;
557       GLfloat scale = 0.5;
558       char *title = frame->title ? frame->title : "(untitled)";
559       sw = texture_string_width (ss->texfont, title, &sh);
560
561       glTranslatef (-sw*scale*0.5, -h - sh*scale, z);
562       glScalef (scale, scale, 1);
563
564       glColor3f (1, 1, 1);
565
566       if (!wire)
567         {
568           glEnable (GL_TEXTURE_2D);
569           print_texture_string (ss->texfont, title);
570         }
571       else
572         {
573           glBegin (GL_LINE_LOOP);
574           glVertex3f (0,  0,  0);
575           glVertex3f (sw, 0,  0);
576           glVertex3f (sw, sh, 0);
577           glVertex3f (0,  sh, 0);
578           glEnd();
579         }
580     }
581
582   glPopMatrix();
583 }
584
585
586 ENTRYPOINT void
587 draw_photopile (ModeInfo *mi)
588 {
589   photopile_state *ss = &sss[MI_SCREEN(mi)];
590   int i;
591
592   if (!ss->glx_context)
593     return;
594
595   glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *(ss->glx_context));
596
597   if (ss->mode == EARLY)
598     if (!load_initial_images (mi))
599       return;
600
601   /* Only check the wall clock every 10 frames */
602   if (ss->now == 0 || ss->draw_tick++ > 10)
603     {
604       ss->now = time((time_t *) 0);
605       if (ss->last_time == 0) ss->last_time = ss->now;
606       ss->draw_tick = 0;
607     }
608
609   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
610   {
611     GLfloat t;
612
613     /* Handle state transitions. */
614     switch (ss->mode)
615       {
616       case IN:
617         if (--ss->mode_tick <= 0)
618           {
619             ss->mode = NORMAL;
620             ss->last_time = time((time_t *) 0);
621           }
622         break;
623       case NORMAL:
624         if (ss->now - ss->last_time > duration)
625           {
626             ss->mode = LOADING;
627             load_image(mi);
628           }
629         break;
630       case LOADING:
631         if (ss->frames[ss->nframe].loaded_p)
632           {
633             set_new_positions(ss);
634             ss->mode = SHUFFLE;
635             ss->mode_tick = fade_ticks / speed;
636           }
637         break;
638       case SHUFFLE:
639         if (--ss->mode_tick <= 0)
640           {
641             ss->nframe = (ss->nframe+1) % (MI_COUNT(mi)+1);
642
643             ss->mode = NORMAL;
644             ss->last_time = time((time_t *) 0);
645           }
646         break;
647       default:
648         abort();
649       }
650
651     t = 1.0 - ss->mode_tick / (fade_ticks / speed);
652     t = 0.5 * (1.0 - cos(M_PI * t));
653
654     /* Draw the images. */
655     for (i = 0; i < MI_COUNT(mi) + (ss->mode == SHUFFLE); ++i)
656       {
657         int j = (ss->nframe + i + 1) % (MI_COUNT(mi) + 1);
658         GLfloat s = 1.0;
659         GLfloat z = (GLfloat)i / (MI_COUNT(mi) + 1);
660
661         switch (ss->mode)
662           {
663           case IN:
664             s *= t;
665             break;
666           case NORMAL:
667           case LOADING:
668             t = 1.0;
669             break;
670           case SHUFFLE:
671             if (i == MI_COUNT(mi))
672               {
673                 s *= t;
674               }
675             else if (i == 0)
676               {
677                 s *= 1.0 - t;
678               }
679             break;
680           default:
681             abort();
682           }
683
684         draw_image(mi, j, t, s, z);
685       }
686   }
687
688   if (mi->fps_p) do_fps (mi);
689   glFinish();
690   glXSwapBuffers (MI_DISPLAY (mi), MI_WINDOW(mi));
691 }
692
693 XSCREENSAVER_MODULE ("Photopile", photopile)
694
695 #endif /* USE_GL */