7e6faef93b381234995c1bf03f6f5a8acd6def92
[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 = (int)(MI_WIDTH(mi)  * scale) - 1;
300       int h = (int)(MI_HEIGHT(mi) * scale) - 1;
301       if (w <= 10) w = 10;
302       if (h <= 10) h = 10;
303       load_texture_async (mi->xgwa.screen, mi->window, *ss->glx_context, w, h,
304                           mipmap_p, frame->texid, 
305                           image_loaded_cb, ss);
306     }
307 }
308
309
310 static void
311 loading_msg (ModeInfo *mi, int n)
312 {
313   photopile_state *ss = &sss[MI_SCREEN(mi)];
314   int wire = MI_IS_WIREFRAME(mi);
315   char text[100];
316   GLfloat scale;
317
318   if (wire) return;
319
320   if (n == 0)
321     sprintf (text, "Loading images...");
322   else
323     sprintf (text, "Loading images...  (%d%%)",
324              (int) (n * 100 / MI_COUNT(mi)));
325
326   if (ss->loading_sw == 0)    /* only do this once, so that the string doesn't move. */
327     ss->loading_sw = texture_string_width (ss->texfont, text, &ss->loading_sh);
328
329   scale = ss->loading_sh / (GLfloat) MI_HEIGHT(mi);
330
331   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
332
333   glMatrixMode(GL_PROJECTION);
334   glPushMatrix();
335   glLoadIdentity();
336
337   glMatrixMode(GL_MODELVIEW);
338   glPushMatrix();
339   glLoadIdentity();
340   gluOrtho2D(0, MI_WIDTH(mi), 0, MI_HEIGHT(mi));
341
342   glTranslatef ((MI_WIDTH(mi)  - ss->loading_sw) / 2,
343                 (MI_HEIGHT(mi) - ss->loading_sh) / 2,
344                 0);
345   glColor3f (1, 1, 0);
346   glEnable (GL_TEXTURE_2D);
347   glDisable (GL_DEPTH_TEST);
348   print_texture_string (ss->texfont, text);
349   glEnable (GL_DEPTH_TEST);
350   glPopMatrix();
351
352   glMatrixMode(GL_PROJECTION);
353   glPopMatrix();
354
355   glMatrixMode(GL_MODELVIEW);
356
357   glFinish();
358   glXSwapBuffers (MI_DISPLAY (mi), MI_WINDOW(mi));
359 }
360
361
362 static Bool
363 load_initial_images (ModeInfo *mi)
364 {
365   photopile_state *ss = &sss[MI_SCREEN(mi)];
366
367   if (ss->frames[ss->nframe].loaded_p)
368     {
369       /* The current image has been fully loaded. */
370       if (++ss->nframe < MI_COUNT(mi))
371         {
372           /* Start the next one loading.  (We run the image loader
373            * asynchronously, but we load them one at a time.)
374            */
375           load_image(mi);
376         }
377       else
378         {
379           /* loaded all initial images, start fading them in */
380           int i;
381
382           for (i = 0; i < ss->nframe; ++i)
383             {
384               ss->frames[i].pos[3].x = MI_WIDTH(mi) * 0.5;
385               ss->frames[i].pos[3].y = MI_HEIGHT(mi) * 0.5;
386               ss->frames[i].pos[3].angle = 0.0;
387             }
388           set_new_positions(ss);
389
390           ss->mode = IN;
391           ss->mode_tick = fade_ticks / speed;
392         }
393     }
394
395   loading_msg(mi, ss->nframe);
396   return (ss->mode != EARLY);
397 }
398
399
400 ENTRYPOINT void
401 reshape_photopile (ModeInfo *mi, int width, int height)
402 {
403   glViewport (0, 0, (GLint) width, (GLint) height);
404
405   glMatrixMode(GL_PROJECTION);
406   glLoadIdentity();
407
408   glMatrixMode(GL_MODELVIEW);
409   glLoadIdentity();
410   gluOrtho2D(0, MI_WIDTH(mi), 0, MI_HEIGHT(mi));
411
412   glClear(GL_COLOR_BUFFER_BIT);
413 }
414
415
416 /* Kludge to add "-v" to invocation of "xscreensaver-getimage" in -debug mode
417  */
418 static void
419 hack_resources (Display *dpy)
420 {
421 # ifndef HAVE_COCOA
422   char *res = "desktopGrabber";
423   char *val = get_string_resource (dpy, res, "DesktopGrabber");
424   char buf1[255];
425   char buf2[255];
426   XrmValue value;
427   XrmDatabase db = XtDatabase (dpy);
428   sprintf (buf1, "%.100s.%.100s", progname, res);
429   sprintf (buf2, "%.200s -v", val);
430   value.addr = buf2;
431   value.size = strlen(buf2);
432   XrmPutResource (&db, buf1, "String", &value);
433 # endif /* !HAVE_COCOA */
434 }
435
436
437 ENTRYPOINT void
438 init_photopile (ModeInfo *mi)
439 {
440   int screen = MI_SCREEN(mi);
441   photopile_state *ss;
442   int wire = MI_IS_WIREFRAME(mi);
443
444   if (sss == NULL) {
445     if ((sss = (photopile_state *)
446          calloc (MI_NUM_SCREENS(mi), sizeof(photopile_state))) == NULL)
447       return;
448   }
449   ss = &sss[screen];
450   ss->mi = mi;
451
452   if ((ss->glx_context = init_GL(mi)) != NULL) {
453     reshape_photopile (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
454   } else {
455     MI_CLEARWINDOW(mi);
456   }
457
458   glDisable (GL_LIGHTING);
459   glEnable (GL_DEPTH_TEST);
460   glDisable (GL_CULL_FACE);
461
462   if (! wire)
463     {
464       glShadeModel (GL_SMOOTH);
465       glEnable (GL_LINE_SMOOTH);
466       /* This gives us a transparent diagonal slice through each image! */
467       /* glEnable (GL_POLYGON_SMOOTH); */
468       glHint (GL_LINE_SMOOTH_HINT, GL_NICEST);
469       glEnable (GL_BLEND);
470       glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
471
472       glEnable (GL_POLYGON_OFFSET_FILL);
473       glPolygonOffset (1.0, 1.0);
474     }
475
476   ss->texfont = load_texture_font (MI_DISPLAY(mi), "font");
477
478   if (debug_p)
479     hack_resources (MI_DISPLAY (mi));
480
481   ss->frames = (image *)calloc (MI_COUNT(mi) + 1, sizeof(image));
482   ss->nframe = 0;
483   if (!wire)
484     {
485       int i;
486       for (i = 0; i < MI_COUNT(mi) + 1; ++i)
487         {
488           glGenTextures(1, &(ss->frames[i].texid));
489           if (ss->frames[i].texid <= 0) abort();
490         }
491     }
492
493   ss->mode = EARLY;
494   load_image(mi); /* start loading the first image */
495 }
496
497
498 static void
499 draw_image (ModeInfo *mi, int i, GLfloat t, GLfloat s, GLfloat z)
500 {
501   int wire = MI_IS_WIREFRAME(mi);
502   photopile_state *ss = &sss[MI_SCREEN(mi)];
503   image *frame = ss->frames + i;
504
505   GLfloat texw  = frame->geom.width  / (GLfloat) frame->tw;
506   GLfloat texh  = frame->geom.height / (GLfloat) frame->th;
507   GLfloat texx1 = frame->geom.x / (GLfloat) frame->tw;
508   GLfloat texy1 = frame->geom.y / (GLfloat) frame->th;
509   GLfloat texx2 = texx1 + texw;
510   GLfloat texy2 = texy1 + texh;
511
512   position pos = interpolate(t, frame->pos);
513   GLfloat w = frame->geom.width * 0.5;
514   GLfloat h = frame->geom.height * 0.5;
515
516   glBindTexture (GL_TEXTURE_2D, frame->texid);
517
518   glPushMatrix();
519
520   /* Position and scale this image.
521    */
522   glTranslatef (pos.x, pos.y, 0);
523   glRotatef (pos.angle, 0, 0, 1);
524   glScalef (s, s, 1);
525
526   /* Draw the image quad. */
527   if (! wire)
528     {
529       glColor3f (1, 1, 1);
530       glEnable (GL_TEXTURE_2D);
531       glBegin (GL_QUADS);
532       glTexCoord2f (texx1, texy2); glVertex3f (-w, -h, z);
533       glTexCoord2f (texx2, texy2); glVertex3f ( w, -h, z);
534       glTexCoord2f (texx2, texy1); glVertex3f ( w,  h, z);
535       glTexCoord2f (texx1, texy1); glVertex3f (-w,  h, z);
536       glEnd();
537     }
538
539   /* Draw a box around it.
540    */
541   glLineWidth (2.0);
542   glColor3f (0.5, 0.5, 0.5);
543   glDisable (GL_TEXTURE_2D);
544   glBegin (GL_LINE_LOOP);
545   glVertex3f (-w, -h, z);
546   glVertex3f ( w, -h, z);
547   glVertex3f ( w,  h, z);
548   glVertex3f (-w,  h, z);
549   glEnd();
550
551   /* Draw a title under the image.
552    */
553   if (titles_p)
554     {
555       int sw, sh;
556       GLfloat scale = 0.5;
557       char *title = frame->title ? frame->title : "(untitled)";
558       sw = texture_string_width (ss->texfont, title, &sh);
559
560       glTranslatef (-sw*scale*0.5, -h - sh*scale, z);
561       glScalef (scale, scale, 1);
562
563       glColor3f (1, 1, 1);
564
565       if (!wire)
566         {
567           glEnable (GL_TEXTURE_2D);
568           print_texture_string (ss->texfont, title);
569         }
570       else
571         {
572           glBegin (GL_LINE_LOOP);
573           glVertex3f (0,  0,  0);
574           glVertex3f (sw, 0,  0);
575           glVertex3f (sw, sh, 0);
576           glVertex3f (0,  sh, 0);
577           glEnd();
578         }
579     }
580
581   glPopMatrix();
582 }
583
584
585 ENTRYPOINT void
586 draw_photopile (ModeInfo *mi)
587 {
588   photopile_state *ss = &sss[MI_SCREEN(mi)];
589   int i;
590
591   if (!ss->glx_context)
592     return;
593
594   glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *(ss->glx_context));
595
596   if (ss->mode == EARLY)
597     if (!load_initial_images (mi))
598       return;
599
600   /* Only check the wall clock every 10 frames */
601   if (ss->now == 0 || ss->draw_tick++ > 10)
602     {
603       ss->now = time((time_t *) 0);
604       if (ss->last_time == 0) ss->last_time = ss->now;
605       ss->draw_tick = 0;
606     }
607
608   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
609   {
610     GLfloat t;
611
612     /* Handle state transitions. */
613     switch (ss->mode)
614       {
615       case IN:
616         if (--ss->mode_tick <= 0)
617           {
618             ss->mode = NORMAL;
619             ss->last_time = time((time_t *) 0);
620           }
621         break;
622       case NORMAL:
623         if (ss->now - ss->last_time > duration)
624           {
625             ss->mode = LOADING;
626             load_image(mi);
627           }
628         break;
629       case LOADING:
630         if (ss->frames[ss->nframe].loaded_p)
631           {
632             set_new_positions(ss);
633             ss->mode = SHUFFLE;
634             ss->mode_tick = fade_ticks / speed;
635           }
636         break;
637       case SHUFFLE:
638         if (--ss->mode_tick <= 0)
639           {
640             ss->nframe = (ss->nframe+1) % (MI_COUNT(mi)+1);
641
642             ss->mode = NORMAL;
643             ss->last_time = time((time_t *) 0);
644           }
645         break;
646       default:
647         abort();
648       }
649
650     t = 1.0 - ss->mode_tick / (fade_ticks / speed);
651     t = 0.5 * (1.0 - cos(M_PI * t));
652
653     /* Draw the images. */
654     for (i = 0; i < MI_COUNT(mi) + (ss->mode == SHUFFLE); ++i)
655       {
656         int j = (ss->nframe + i + 1) % (MI_COUNT(mi) + 1);
657         GLfloat s = 1.0;
658         GLfloat z = (GLfloat)i / (MI_COUNT(mi) + 1);
659
660         switch (ss->mode)
661           {
662           case IN:
663             s *= t;
664             break;
665           case NORMAL:
666           case LOADING:
667             t = 1.0;
668             break;
669           case SHUFFLE:
670             if (i == MI_COUNT(mi))
671               {
672                 s *= t;
673               }
674             else if (i == 0)
675               {
676                 s *= 1.0 - t;
677               }
678             break;
679           default:
680             abort();
681           }
682
683         draw_image(mi, j, t, s, z);
684       }
685   }
686
687   if (mi->fps_p) do_fps (mi);
688   glFinish();
689   glXSwapBuffers (MI_DISPLAY (mi), MI_WINDOW(mi));
690 }
691
692 XSCREENSAVER_MODULE ("Photopile", photopile)
693
694 #endif /* USE_GL */