1 /* photopile, Copyright (c) 2008-2018 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.
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
14 #if defined(HAVE_COCOA) || defined(HAVE_ANDROID)
15 # define DEF_FONT "OCR A Std 48, Lucida Console 48, Monaco 48"
16 #elif 0 /* real X11, XQueryFont() */
17 # define DEF_FONT "-*-helvetica-bold-r-normal-*-*-480-*-*-*-*-*-*"
18 #else /* real X11, load_font_retry() */
19 # define DEF_FONT "-*-ocr a std-medium-r-*-*-*-480-*-*-m-*-*-*"
22 #define DEFAULTS "*count: 7 \n" \
24 "*wireframe: False \n" \
25 "*showFPS: False \n" \
26 "*fpsSolid: True \n" \
28 "*font: " DEF_FONT "\n" \
29 "*desktopGrabber: xscreensaver-getimage -no-desktop %s\n" \
30 "*grabDesktopImages: False \n" \
31 "*chooseRandomImages: True \n" \
32 "*suppressRotationAnimation: True\n" \
34 # define free_photopile 0
35 # define release_photopile 0
36 # define photopile_handle_event xlockmore_no_events
39 #define countof(x) (sizeof((x))/sizeof((*x)))
42 # include <X11/Intrinsic.h> /* for XrmDatabase in -debug mode */
46 #include "xlockmore.h"
47 #include "grab-ximage.h"
49 #include "dropshadow.h"
53 # define DEF_SCALE "0.4"
54 # define DEF_MAX_TILT "50"
55 # define DEF_SPEED "1.0"
56 # define DEF_DURATION "5"
57 # define DEF_MIPMAP "True"
58 # define DEF_TITLES "True"
59 # define DEF_POLAROID "True"
60 # define DEF_CLIP "True"
61 # define DEF_SHADOWS "True"
62 # define DEF_DEBUG "False"
64 #define BELLRAND(n) ((frand((n)) + frand((n)) + frand((n))) / 3)
67 GLfloat x, y; /* position on screen */
68 GLfloat angle; /* rotation angle */
73 Bool loaded_p; /* true if image can be drawn */
75 char *title; /* the filename of this image */
76 int w, h; /* size in pixels of the image */
77 int tw, th; /* size in pixels of the texture */
78 XRectangle geom; /* where in the image the bits are */
80 position pos[4]; /* control points for calculating position */
82 GLuint texid; /* GL texture ID */
87 typedef enum { EARLY, SHUFFLE, NORMAL, LOADING } fade_mode;
88 static int fade_ticks = 60;
92 GLXContext *glx_context;
94 image *frames; /* pointer to array of images */
95 int nframe; /* image being (resp. next to be) loaded */
98 texture_font_data *texfont;
99 int loading_sw, loading_sh;
101 time_t last_time, now;
108 static photopile_state *sss = NULL;
111 /* Command-line arguments
113 static GLfloat scale; /* Scale factor for loading images. */
114 static GLfloat max_tilt; /* Maximum angle from vertical. */
115 static GLfloat speed; /* Animation speed scale factor. */
116 static int duration; /* Reload images after this long. */
117 static Bool mipmap_p; /* Use mipmaps instead of single textures. */
118 static Bool titles_p; /* Display image titles. */
119 static Bool polaroid_p; /* Use instant-film look for images. */
120 static Bool clip_p; /* Clip images instead of scaling for -polaroid. */
121 static Bool shadows_p; /* Draw drop shadows. */
122 static Bool debug_p; /* Be loud and do weird things. */
125 static XrmOptionDescRec opts[] = {
126 {"-scale", ".scale", XrmoptionSepArg, 0 },
127 {"-maxTilt", ".maxTilt", XrmoptionSepArg, 0 },
128 {"-speed", ".speed", XrmoptionSepArg, 0 },
129 {"-duration", ".duration", XrmoptionSepArg, 0 },
130 {"-mipmaps", ".mipmap", XrmoptionNoArg, "True" },
131 {"-no-mipmaps", ".mipmap", XrmoptionNoArg, "False" },
132 {"-titles", ".titles", XrmoptionNoArg, "True" },
133 {"-no-titles", ".titles", XrmoptionNoArg, "False" },
134 {"-polaroid", ".polaroid", XrmoptionNoArg, "True" },
135 {"-no-polaroid", ".polaroid", XrmoptionNoArg, "False" },
136 {"-clip", ".clip", XrmoptionNoArg, "True" },
137 {"-no-clip", ".clip", XrmoptionNoArg, "False" },
138 {"-shadows", ".shadows", XrmoptionNoArg, "True" },
139 {"-no-shadows", ".shadows", XrmoptionNoArg, "False" },
140 {"-debug", ".debug", XrmoptionNoArg, "True" },
141 {"-font", ".font", XrmoptionSepArg, 0 },
144 static argtype vars[] = {
145 { &scale, "scale", "Scale", DEF_SCALE, t_Float},
146 { &max_tilt, "maxTilt", "MaxTilt", DEF_MAX_TILT, t_Float},
147 { &speed, "speed", "Speed", DEF_SPEED, t_Float},
148 { &duration, "duration", "Duration", DEF_DURATION, t_Int},
149 { &mipmap_p, "mipmap", "Mipmap", DEF_MIPMAP, t_Bool},
150 { &titles_p, "titles", "Titles", DEF_TITLES, t_Bool},
151 { &polaroid_p, "polaroid", "Polaroid", DEF_POLAROID, t_Bool},
152 { &clip_p, "clip", "Clip", DEF_CLIP, t_Bool},
153 { &shadows_p, "shadows", "Shadows", DEF_SHADOWS, t_Bool},
154 { &debug_p, "debug", "Debug", DEF_DEBUG, t_Bool},
157 ENTRYPOINT ModeSpecOpt photopile_opts = {countof(opts), opts, countof(vars), vars, NULL};
160 /* Functions to interpolate between image positions.
163 add_pos(position p, position q)
172 scale_pos(GLfloat t, position p)
181 linear_combination(GLfloat t, position p, position q)
183 return add_pos(scale_pos(1.0 - t, p), scale_pos(t, q));
187 interpolate(GLfloat t, position p[4])
189 /* de Casteljau's algorithm, 4 control points */
190 position p10 = linear_combination(t, p[0], p[1]);
191 position p11 = linear_combination(t, p[1], p[2]);
192 position p12 = linear_combination(t, p[2], p[3]);
194 position p20 = linear_combination(t, p10, p11);
195 position p21 = linear_combination(t, p11, p12);
197 return linear_combination(t, p20, p21);
201 offset_pos(position p, GLfloat th, GLfloat r)
205 p.angle = (frand(2.0) - 1.0) * max_tilt;
209 /* Calculate new positions for all images.
212 set_new_positions(photopile_state *ss)
214 ModeInfo *mi = ss->mi;
217 for (i = 0; i < MI_COUNT(mi)+1; ++i)
219 image *frame = ss->frames + i;
220 GLfloat w = frame->w;
221 GLfloat h = frame->h;
222 GLfloat d = sqrt(w*w + h*h);
223 GLfloat leave = frand(M_PI * 2.0);
224 GLfloat enter = frand(M_PI * 2.0);
227 frame->pos[0] = frame->pos[3];
230 frame->pos[3].x = BELLRAND(MI_WIDTH(mi));
231 frame->pos[3].y = BELLRAND(MI_HEIGHT(mi));
232 frame->pos[3].angle = (frand(2.0) - 1.0) * max_tilt;
234 /* Try to keep the images mostly inside the screen bounds */
235 frame->pos[3].x = MAX(0.5*w, MIN(MI_WIDTH(mi)-0.5*w, frame->pos[3].x));
236 frame->pos[3].y = MAX(0.5*h, MIN(MI_HEIGHT(mi)-0.5*h, frame->pos[3].y));
238 /* intermediate points */
239 frame->pos[1] = offset_pos(frame->pos[0], leave, d * (0.5 + frand(1.0)));
240 frame->pos[2] = offset_pos(frame->pos[3], enter, d * (0.5 + frand(1.0)));
244 /* Callback that tells us that the texture has been loaded.
247 image_loaded_cb (const char *filename, XRectangle *geom,
248 int image_width, int image_height,
249 int texture_width, int texture_height,
252 photopile_state *ss = (photopile_state *) closure;
253 ModeInfo *mi = ss->mi;
254 int wire = MI_IS_WIREFRAME(mi);
255 image *frame = ss->frames + ss->nframe;
261 frame->w = (int)(MI_WIDTH(mi) * scale) - 1;
262 frame->h = (int)(MI_HEIGHT(mi) * scale) - 1;
266 frame->w = (int)(MI_HEIGHT(mi) * scale) - 1;
267 frame->h = (int)(MI_WIDTH(mi) * scale) - 1;
269 if (frame->w <= 10) frame->w = 10;
270 if (frame->h <= 10) frame->h = 10;
271 frame->geom.width = frame->w;
272 frame->geom.height = frame->h;
276 if (image_width == 0 || image_height == 0)
279 glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
280 glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
281 mipmap_p ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR);
283 glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
284 glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
286 frame->w = image_width;
287 frame->h = image_height;
288 frame->tw = texture_width;
289 frame->th = texture_height;
294 frame->title = (filename ? strdup (filename) : 0);
296 /* xscreensaver-getimage returns paths relative to the image directory
297 now, so leave the sub-directory part in. Unless it's an absolute path.
299 if (frame->title && frame->title[0] == '/')
301 /* strip filename to part after last /. */
302 char *s = strrchr (frame->title, '/');
303 if (s) strcpy (frame->title, s+1);
307 fprintf (stderr, "%s: loaded %4d x %-4d %4d x %-4d \"%s\"\n",
311 frame->tw, frame->th,
312 (frame->title ? frame->title : "(null)"));
315 frame->loaded_p = True;
322 load_image (ModeInfo *mi)
324 photopile_state *ss = &sss[MI_SCREEN(mi)];
325 int wire = MI_IS_WIREFRAME(mi);
326 image *frame = ss->frames + ss->nframe;
328 if (debug_p && !wire && frame->w != 0)
329 fprintf (stderr, "%s: dropped %4d x %-4d %4d x %-4d \"%s\"\n",
333 frame->tw, frame->th,
334 (frame->title ? frame->title : "(null)"));
336 frame->loaded_p = False;
339 image_loaded_cb (0, 0, 0, 0, 0, 0, ss);
342 int w = MI_WIDTH(mi);
343 int h = MI_HEIGHT(mi);
344 int size = (int)((w > h ? w : h) * scale);
345 if (size <= 10) size = 10;
346 load_texture_async (mi->xgwa.screen, mi->window, *ss->glx_context,
348 mipmap_p, frame->texid,
349 image_loaded_cb, ss);
355 loading_msg (ModeInfo *mi)
357 photopile_state *ss = &sss[MI_SCREEN(mi)];
358 int wire = MI_IS_WIREFRAME(mi);
359 const char text[] = "Loading...";
363 if (ss->loading_sw == 0) /* only do this once */
366 texture_string_metrics (ss->texfont, text, &e, 0, 0);
367 ss->loading_sw = e.width;
368 ss->loading_sh = e.ascent + e.descent;
371 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
373 glMatrixMode(GL_PROJECTION);
377 glMatrixMode(GL_MODELVIEW);
380 glOrtho(0, MI_WIDTH(mi), 0, MI_HEIGHT(mi), -1, 1);
382 glTranslatef ((MI_WIDTH(mi) - ss->loading_sw) / 2,
383 (MI_HEIGHT(mi) - ss->loading_sh) / 2,
386 glEnable (GL_TEXTURE_2D);
387 glDisable (GL_DEPTH_TEST);
388 print_texture_string (ss->texfont, text);
389 glEnable (GL_DEPTH_TEST);
392 glMatrixMode(GL_PROJECTION);
395 glMatrixMode(GL_MODELVIEW);
398 glXSwapBuffers (MI_DISPLAY (mi), MI_WINDOW(mi));
403 loading_initial_image (ModeInfo *mi)
405 photopile_state *ss = &sss[MI_SCREEN(mi)];
407 if (ss->frames[ss->nframe].loaded_p)
409 /* The initial image has been fully loaded, start fading it in. */
412 for (i = 0; i < ss->nframe; ++i)
414 ss->frames[i].pos[3].x = MI_WIDTH(mi) * 0.5;
415 ss->frames[i].pos[3].y = MI_HEIGHT(mi) * 0.5;
416 ss->frames[i].pos[3].angle = 0.0;
418 set_new_positions(ss);
421 ss->mode_tick = fade_ticks / speed;
428 return (ss->mode == EARLY);
433 reshape_photopile (ModeInfo *mi, int width, int height)
435 glViewport (0, 0, (GLint) width, (GLint) height);
437 glMatrixMode(GL_PROJECTION);
440 glMatrixMode(GL_MODELVIEW);
442 glOrtho(0, MI_WIDTH(mi), 0, MI_HEIGHT(mi), -1, 1);
444 # ifdef HAVE_MOBILE /* Keep it the same relative size when rotated. */
446 GLfloat h = MI_HEIGHT(mi) / (GLfloat) MI_WIDTH(mi);
447 int o = (int) current_device_rotation();
448 if (o != 0 && o != 180 && o != -180)
449 glScalef (1/h, h, 1);
453 glClear(GL_COLOR_BUFFER_BIT);
457 /* Kludge to add "-v" to invocation of "xscreensaver-getimage" in -debug mode
460 hack_resources (Display *dpy)
463 char *res = "desktopGrabber";
464 char *val = get_string_resource (dpy, res, "DesktopGrabber");
468 XrmDatabase db = XtDatabase (dpy);
469 sprintf (buf1, "%.100s.%.100s", progname, res);
470 sprintf (buf2, "%.200s -v", val);
472 value.size = strlen(buf2);
473 XrmPutResource (&db, buf1, "String", &value);
474 # endif /* !HAVE_JWXYZ */
479 init_photopile (ModeInfo *mi)
481 int screen = MI_SCREEN(mi);
483 int wire = MI_IS_WIREFRAME(mi);
489 if ((ss->glx_context = init_GL(mi)) != NULL) {
490 reshape_photopile (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
491 clear_gl_error(); /* WTF? sometimes "invalid op" from glViewport! */
496 ss->shadow = init_drop_shadow();
497 ss->texfont = load_texture_font (MI_DISPLAY(mi), "font");
500 hack_resources (MI_DISPLAY (mi));
502 ss->frames = (image *)calloc (MI_COUNT(mi) + 1, sizeof(image));
507 for (i = 0; i < MI_COUNT(mi) + 1; ++i)
509 glGenTextures (1, &(ss->frames[i].texid));
510 if (ss->frames[i].texid <= 0) abort();
515 load_image(mi); /* start loading the first image */
520 draw_image (ModeInfo *mi, int i, GLfloat t, GLfloat s, GLfloat z)
522 int wire = MI_IS_WIREFRAME(mi);
523 photopile_state *ss = &sss[MI_SCREEN(mi)];
524 image *frame = ss->frames + i;
526 position pos = interpolate(t, frame->pos);
527 GLfloat w = frame->geom.width * 0.5;
528 GLfloat h = frame->geom.height * 0.5;
529 GLfloat z1 = z - 0.25 / (MI_COUNT(mi) + 1);
530 GLfloat z2 = z - 0.5 / (MI_COUNT(mi) + 1);
537 GLfloat minSize = MIN(w, h);
538 GLfloat maxSize = MAX(w, h);
540 /* Clip or scale image to fit in the frame.
548 GLfloat scale = minSize / maxSize;
553 w1 = minSize * 1.16; /* enlarge frame border */
556 s /= 1.5; /* compensate for border size */
561 /* Position and scale this image.
563 glTranslatef (pos.x, pos.y, 0);
564 glRotatef (pos.angle, 0, 0, 1);
567 /* Draw the drop shadow. */
568 if (shadows_p && !wire)
571 draw_drop_shadow(ss->shadow, -w1, -h1, z2, 2.0 * w1, h1 + h2, 20.0);
572 glDisable (GL_BLEND);
575 glDisable (GL_LIGHTING);
576 glEnable (GL_DEPTH_TEST);
577 glDisable (GL_CULL_FACE);
579 /* Draw the retro instant-film frame.
585 glShadeModel (GL_SMOOTH);
586 glEnable (GL_LINE_SMOOTH);
587 glHint (GL_LINE_SMOOTH_HINT, GL_NICEST);
591 glVertex3f (-w1, -h1, z2);
592 glVertex3f ( w1, -h1, z2);
593 glVertex3f ( w1, h2, z2);
594 glVertex3f (-w1, h2, z2);
599 glColor3f (0.5, 0.5, 0.5);
600 glBegin (GL_LINE_LOOP);
601 glVertex3f (-w1, -h1, z);
602 glVertex3f ( w1, -h1, z);
603 glVertex3f ( w1, h2, z);
604 glVertex3f (-w1, h2, z);
608 /* Draw the image quad.
612 GLfloat texw = w / frame->tw;
613 GLfloat texh = h / frame->th;
614 GLfloat texx = (frame->geom.x + 0.5 * frame->geom.width) / frame->tw;
615 GLfloat texy = (frame->geom.y + 0.5 * frame->geom.height) / frame->th;
617 glBindTexture (GL_TEXTURE_2D, frame->texid);
618 glEnable (GL_TEXTURE_2D);
621 glTexCoord2f (texx - texw, texy + texh); glVertex3f (-w, -h, z1);
622 glTexCoord2f (texx + texw, texy + texh); glVertex3f ( w, -h, z1);
623 glTexCoord2f (texx + texw, texy - texh); glVertex3f ( w, h, z1);
624 glTexCoord2f (texx - texw, texy - texh); glVertex3f (-w, h, z1);
626 glDisable (GL_TEXTURE_2D);
629 /* Draw a box around it.
633 glShadeModel (GL_SMOOTH);
634 glEnable (GL_LINE_SMOOTH);
635 glHint (GL_LINE_SMOOTH_HINT, GL_NICEST);
638 glColor3f (0.5, 0.5, 0.5);
639 glBegin (GL_LINE_LOOP);
640 glVertex3f (-w, -h, z);
641 glVertex3f ( w, -h, z);
642 glVertex3f ( w, h, z);
643 glVertex3f (-w, h, z);
646 /* Draw a title under the image.
655 const char *title = frame->title ? frame->title : "(untitled)";
658 texture_string_metrics (ss->texfont, title, &e, &ascent, &descent);
660 sh = ascent; /* + descent; */
662 /* Scale the text to match the pixel size of the photo */
665 # if defined(HAVE_COCOA)
667 if (MI_WIDTH(mi) > 2560) scale /= 2; /* Retina displays */
670 # if defined(HAVE_MOBILE)
674 /* Clip characters off the left end of the string until it fits. */
675 if (clip_p || polaroid_p)
676 while (sw * scale > tw && strlen (title) > 10)
679 texture_string_metrics (ss->texfont, title, &e, &ascent, &descent);
683 if (th <= 0) /* Non-polaroid */
686 glTranslatef (-w, -h1, 0);
687 glTranslatef ((tw - sw*scale) / 2, (th - sh*scale) / 2, 0);
689 glScalef (scale, scale, 1);
691 if (wire || !polaroid_p)
697 glColor3f (0.5, 0.5, 0.5);
702 glEnable (GL_TEXTURE_2D);
704 glDisable (GL_DEPTH_TEST);
705 print_texture_string (ss->texfont, title);
706 glEnable (GL_DEPTH_TEST);
710 glBegin (GL_LINE_LOOP);
711 glVertex3f (0, 0, 0);
712 glVertex3f (sw, 0, 0);
713 glVertex3f (sw, sh, 0);
714 glVertex3f (0, sh, 0);
725 draw_photopile (ModeInfo *mi)
727 photopile_state *ss = &sss[MI_SCREEN(mi)];
730 if (!ss->glx_context)
733 glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *(ss->glx_context));
735 if (ss->mode == EARLY)
736 if (loading_initial_image (mi))
739 /* Only check the wall clock every 10 frames */
740 if (ss->now == 0 || ss->draw_tick++ > 10)
742 ss->now = time((time_t *) 0);
743 if (ss->last_time == 0) ss->last_time = ss->now;
747 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
752 glTranslatef (MI_WIDTH(mi)/2, MI_HEIGHT(mi)/2, 0);
753 glRotatef(current_device_rotation(), 0, 0, 1);
754 glTranslatef (-MI_WIDTH(mi)/2, -MI_HEIGHT(mi)/2, 0);
756 /* Handle state transitions. */
760 if (--ss->mode_tick <= 0)
762 ss->nframe = (ss->nframe+1) % (MI_COUNT(mi)+1);
765 ss->last_time = time((time_t *) 0);
769 if (ss->now - ss->last_time > duration)
776 if (ss->frames[ss->nframe].loaded_p)
778 set_new_positions(ss);
780 ss->mode_tick = fade_ticks / speed;
787 t = 1.0 - ss->mode_tick / (fade_ticks / speed);
788 t = 0.5 * (1.0 - cos(M_PI * t));
790 /* Draw the images. */
791 for (i = 0; i < MI_COUNT(mi) + (ss->mode == SHUFFLE); ++i)
793 int j = (ss->nframe + i + 1) % (MI_COUNT(mi) + 1);
795 if (ss->frames[j].loaded_p)
798 GLfloat z = (GLfloat)i / (MI_COUNT(mi) + 1);
803 if (i == MI_COUNT(mi))
820 draw_image(mi, j, t, s, z);
826 if (mi->fps_p) do_fps (mi);
828 glXSwapBuffers (MI_DISPLAY (mi), MI_WINDOW(mi));
831 XSCREENSAVER_MODULE ("Photopile", photopile)