1 /* glslideshow, Copyright (c) 2003 Jamie Zawinski <jwz@jwz.org>
2 * Loads a sequence of images and smoothly pans around them; crossfades
3 * when loading new images.
5 * First version Copyright (c) 2002, 2003 Mike Oliphant (oliphant@gtk.org)
6 * based on flipscreen3d, Copyright (C) 2001 Ben Buxton (bb@cactii.net).
8 * Almost entirely rewritten by jwz, 21-Jun-2003.
10 * Permission to use, copy, modify, distribute, and sell this software and its
11 * documentation for any purpose is hereby granted without fee, provided that
12 * the above copyright notice appear in all copies and that both that
13 * copyright notice and this permission notice appear in supporting
14 * documentation. No representations are made about the suitability of this
15 * software for any purpose. It is provided "as is" without express or
20 #include <X11/Intrinsic.h>
23 # define PROGCLASS "GLSlideshow"
24 # define HACK_INIT init_slideshow
25 # define HACK_DRAW draw_slideshow
26 # define HACK_RESHAPE reshape_slideshow
27 # define HACK_HANDLE_EVENT glslideshow_handle_event
28 # define EVENT_MASK (ExposureMask|VisibilityChangeMask)
29 # define slideshow_opts xlockmore_opts
31 # define DEF_FADE_DURATION "2"
32 # define DEF_PAN_DURATION "6"
33 # define DEF_IMAGE_DURATION "30"
34 # define DEF_ZOOM "75"
35 # define DEF_FPS_CUTOFF "5"
36 # define DEF_DEBUG "False"
38 #define DEFAULTS "*delay: 20000 \n" \
39 "*fadeDuration: " DEF_FADE_DURATION "\n" \
40 "*panDuration: " DEF_PAN_DURATION "\n" \
41 "*imageDuration: " DEF_IMAGE_DURATION "\n" \
42 "*zoom: " DEF_ZOOM "\n" \
43 "*FPScutoff: " DEF_FPS_CUTOFF "\n" \
44 "*debug : " DEF_DEBUG "\n" \
45 "*wireframe: False \n" \
46 "*showFPS: False \n" \
47 "*fpsSolid: True \n" \
48 "*desktopGrabber: xscreensaver-getimage -no-desktop %s\n"
50 # include "xlockmore.h"
53 #define countof(x) (sizeof((x))/sizeof((*x)))
62 #include "grab-ximage.h"
69 GLuint texid; /* which texture to draw */
70 enum { IN, OUT, DEAD } state; /* how to draw it */
71 rect from, to; /* the journey this quad is taking */
76 GLXContext *glx_context;
77 time_t start_time; /* when we started displaying this image */
79 int motion_frames; /* how many frames each pan takes */
80 int fade_frames; /* how many frames fading in/out takes */
82 quad quads[2]; /* the (up to) 2 quads we animate */
83 GLuint texids[2]; /* textures: "old" and "new" */
84 GLuint current_texid; /* the "new" one */
86 int img_w, img_h; /* Size (pixels) of currently-loaded image */
88 double now; /* current time in seconds */
89 double pan_start_time; /* when this pan began */
90 double image_start_time; /* when this image was loaded */
91 double dawn_of_time; /* when the program launched */
93 Bool redisplay_needed_p; /* Sometimes we can get away with not
94 re-painting. Tick this if a redisplay
97 GLfloat fps; /* approximate frame rate we're achieving */
98 int pan_frame_count; /* More frame-rate stats */
100 Bool low_fps_p; /* Whether we have compensated for a low
105 static slideshow_state *sss = NULL;
108 /* Command-line arguments
110 int fade_seconds; /* Duration in seconds of fade transitions.
111 If 0, jump-cut instead of fading. */
112 int pan_seconds; /* Duration of each pan through an image. */
113 int image_seconds; /* How many seconds until loading a new image. */
114 int zoom; /* How far in to zoom when panning, in percent of image
115 size: that is, 75 means "when zoomed all the way in,
116 75% of the image will be on screen." */
117 int fps_cutoff; /* If the frame-rate falls below this, turn off zooming.*/
118 Bool debug_p; /* Be loud and do weird things. */
121 static XrmOptionDescRec opts[] = {
122 {"-fade", ".slideshow.fadeDuration", XrmoptionSepArg, 0 },
123 {"-pan", ".slideshow.panDuration", XrmoptionSepArg, 0 },
124 {"-duration", ".slideshow.imageDuration", XrmoptionSepArg, 0 },
125 {"-zoom", ".slideshow.zoom", XrmoptionSepArg, 0 },
126 {"-cutoff", ".slideshow.FPScutoff", XrmoptionSepArg, 0 },
127 {"-debug", ".slideshow.debug", XrmoptionNoArg, "True" },
130 static argtype vars[] = {
131 { &fade_seconds, "fadeDuration", "FadeDuration", DEF_FADE_DURATION, t_Int},
132 { &pan_seconds, "panDuration", "PanDuration", DEF_PAN_DURATION, t_Int},
133 { &image_seconds, "imageDuration","ImageDuration",DEF_IMAGE_DURATION, t_Int},
134 { &zoom, "zoom", "Zoom", DEF_ZOOM, t_Int},
135 { &fps_cutoff, "FPScutoff", "FPSCutoff", DEF_FPS_CUTOFF, t_Int},
136 { &debug_p, "debug", "Debug", DEF_DEBUG, t_Bool},
139 ModeSpecOpt slideshow_opts = {countof(opts), opts, countof(vars), vars, NULL};
142 /* Returns the current time in seconds as a double.
148 # ifdef GETTIMEOFDAY_TWO_ARGS
150 gettimeofday(&now, &tzp);
155 return (now.tv_sec + ((double) now.tv_usec * 0.000001));
160 draw_quad (ModeInfo *mi, quad *q)
162 slideshow_state *ss = &sss[MI_SCREEN(mi)];
163 int wire = MI_IS_WIREFRAME(mi);
171 if (q->state == DEAD)
174 secs = ss->now - ss->pan_start_time;
179 ratio = secs / (pan_seconds + fade_seconds);
181 current.x = q->from.x + ratio * (q->to.x - q->from.x);
182 current.y = q->from.y + ratio * (q->to.y - q->from.y);
183 current.w = q->from.w + ratio * (q->to.w - q->from.w);
184 current.h = q->from.h + ratio * (q->to.h - q->from.h);
186 if (secs < fade_seconds)
187 opacity = secs / (GLfloat) fade_seconds; /* fading in or out... */
188 else if (secs < pan_seconds)
189 opacity = 1; /* panning opaquely. */
191 opacity = 1 - ((secs - pan_seconds) /
192 (GLfloat) fade_seconds); /* fading in or out... */
194 if (q->state == OUT && opacity < 0.0001)
199 glTranslatef (current.x, current.y, 0);
200 glScalef (current.w, current.h, 1);
204 texw = mi->xgwa.width / (GLfloat) ss->img_w;
205 texh = mi->xgwa.height / (GLfloat) ss->img_h;
207 glEnable (GL_TEXTURE_2D);
209 glBindTexture (GL_TEXTURE_2D, q->texid);
210 glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
211 glDepthMask (GL_FALSE);
213 /* Draw the texture quad
215 glColor4f (1, 1, 1, opacity);
216 glNormal3f (0, 0, 1);
218 glTexCoord2f (0, 0); glVertex3f (0, 0, 0);
219 glTexCoord2f (0, texh); glVertex3f (0, 1, 0);
220 glTexCoord2f (texw, texh); glVertex3f (1, 1, 0);
221 glTexCoord2f (texw, 0); glVertex3f (1, 0, 0);
224 glDisable (GL_TEXTURE_2D);
225 glDisable (GL_BLEND);
229 glColor4f ((q->texid == ss->texids[0] ? opacity : 0), 0,
230 (q->texid == ss->texids[0] ? 0 : opacity),
233 glColor4f (1, 1, 1, opacity);
236 /* Draw a grid inside the box
243 glVertex3f (0, 0, 0); glVertex3f (1, 1, 0);
244 glVertex3f (1, 0, 0); glVertex3f (0, 1, 0);
246 for (y = 0; y < 1+d; y += d)
247 for (x = 0; x < 1+d; x += d)
249 glVertex3f (0, y, 0); glVertex3f (1, y, 0);
250 glVertex3f (x, 0, 0); glVertex3f (x, 1, 0);
259 /* Draw the "from" and "to" boxes
261 glColor4f ((q->texid == ss->texids[0] ? opacity : 0), 0,
262 (q->texid == ss->texids[0] ? 0 : opacity),
265 glBegin (GL_LINE_LOOP);
266 glVertex3f (q->from.x, q->from.y, 0);
267 glVertex3f (q->from.x + q->from.w, q->from.y, 0);
268 glVertex3f (q->from.x + q->from.w, q->from.y + q->from.h, 0);
269 glVertex3f (q->from.x, q->from.y + q->from.h, 0);
272 glBegin (GL_LINE_LOOP);
273 glVertex3f (q->to.x, q->to.y, 0);
274 glVertex3f (q->to.x + q->to.w, q->to.y, 0);
275 glVertex3f (q->to.x + q->to.w, q->to.y + q->to.h, 0);
276 glVertex3f (q->to.x, q->to.y + q->to.h, 0);
283 draw_quads (ModeInfo *mi)
285 slideshow_state *ss = &sss[MI_SCREEN(mi)];
289 glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
295 glTranslatef (o, o, 0);
298 for (i = 0; i < countof(ss->quads); i++)
299 draw_quad (mi, &ss->quads[i]);
305 glColor4f (1, 1, 1, 1);
306 glBegin (GL_LINE_LOOP);
307 glVertex3f (0, 0, 0);
308 glVertex3f (0, 1, 0);
309 glVertex3f (1, 1, 0);
310 glVertex3f (1, 0, 0);
316 /* Re-randomize the state of the given quad.
319 reset_quad (ModeInfo *mi, quad *q)
321 /* slideshow_state *ss = &sss[MI_SCREEN(mi)];*/
323 GLfloat mid_w = (zoom / 100.0);
324 GLfloat mid_h = (zoom / 100.0);
325 GLfloat mid_x = (1 - mid_w) / 2;
326 GLfloat mid_y = (1 - mid_h) / 2;
328 GLfloat small = mid_w + frand ((1 - mid_w) * 0.3);
330 GLfloat large = small + frand ((1 - small) / 2) + ((1 - small) / 2);
332 GLfloat large = small + frand (1 - small);
335 if (q->state != DEAD)
336 abort(); /* we should only be resetting a quad when it's not visible. */
338 /* Possible box sizes range between "zoom" and "100%".
339 Pick a small box size, and a large box size.
340 Assign each a random position within the 1x1 box,
341 such that they encompass the middle "zoom" percentage.
342 One of those is the start, and one is the end.
343 Each frame will transition between one and the other.
348 q->from.w = small; q->from.h = small;
349 q->to.w = large; q->to.h = large;
353 q->from.w = large; q->from.h = large;
354 q->to.w = small; q->to.h = small;
357 q->from.x = mid_x - frand (q->from.w - mid_w);
358 q->from.y = mid_y - frand (q->from.h - mid_h);
359 q->to.x = mid_x - frand (q->to.w - mid_w);
360 q->to.y = mid_y - frand (q->to.w - mid_h);
366 /* Shrinks the XImage by a factor of two.
369 shrink_image (ModeInfo *mi, XImage *ximage)
371 int w2 = ximage->width/2;
372 int h2 = ximage->height/2;
376 if (w2 <= 32 || h2 <= 32) /* let's not go crazy here, man. */
380 fprintf (stderr, "%s: debug: shrinking image %dx%d -> %dx%d\n",
381 progname, ximage->width, ximage->height, w2, h2);
383 ximage2 = XCreateImage (MI_DISPLAY (mi), mi->xgwa.visual,
386 ximage2->data = (char *) calloc (h2, ximage2->bytes_per_line);
389 fprintf (stderr, "%s: out of memory (scaling %dx%d image to %dx%d)\n",
390 progname, ximage->width, ximage->height, w2, h2);
393 for (y = 0; y < h2; y++)
394 for (x = 0; x < w2; x++)
395 XPutPixel (ximage2, x, y, XGetPixel (ximage, x*2, y*2));
403 /* Load a new image into a texture for the given quad.
406 load_quad (ModeInfo *mi, quad *q)
408 slideshow_state *ss = &sss[MI_SCREEN(mi)];
411 int max_reduction = 7;
413 int wire = MI_IS_WIREFRAME(mi);
415 if (q->state != DEAD) abort();
417 /* Figure out which texid is currently in use, and pick the other one.
422 if (ss->current_texid == 0)
425 for (i = 0; i < countof(ss->texids); i++)
426 if (ss->texids[i] != ss->current_texid)
432 if (tid == 0) abort(); /* both textures in use by visible quads? */
434 ss->current_texid = tid;
438 fprintf (stderr, "%s: debug: loading image %d (%dx%d)\n",
439 progname, q->texid, mi->xgwa.width, mi->xgwa.height);
444 ximage = screen_to_ximage (mi->xgwa.screen, mi->window);
446 glBindTexture (GL_TEXTURE_2D, q->texid);
447 glPixelStorei (GL_UNPACK_ALIGNMENT, 1);
448 glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
449 GL_LINEAR_MIPMAP_LINEAR);
451 ss->img_w = ximage->width;
452 ss->img_h = ximage->height;
457 status = gluBuild2DMipmaps (GL_TEXTURE_2D, 3,
458 ximage->width, ximage->height,
459 GL_RGBA, GL_UNSIGNED_BYTE, ximage->data);
461 if(!status && glGetError())
462 /* Some implementations of gluBuild2DMipmaps(), but set a GL error anyway.
463 We could just call check_gl_error(), but that would exit. */
469 const char *s = gluErrorString (status);
473 sprintf (buf, "unknown error %d", status);
479 if (++err_count > max_reduction)
483 "%s: %dx%d texture failed, even after reducing to %dx%d:\n"
484 "%s: GLU said: \"%s\".\n"
485 "%s: probably this means "
486 "\"your video card is worthless and weak\"?\n\n",
487 progname, MI_WIDTH(mi), MI_HEIGHT(mi),
488 ximage->width, ximage->height,
496 fprintf (stderr, "%s: debug: mipmap error (%dx%d): %s\n",
497 progname, ximage->width, ximage->height, s);
498 shrink_image (mi, ximage);
503 check_gl_error("mipmapping"); /* should get a return code instead of a
504 GL error, but just in case... */
508 XDestroyImage(ximage);
512 /* Re-set "now" so that time spent loading the image file does not count
513 against the time remaining in this stage of the animation: image loading,
514 if it takes a perceptible amount of time, will cause the animation to
515 pause, but will not cause it to drop frames.
517 ss->now = double_time ();
518 ss->image_start_time = ss->now;
520 ss->redisplay_needed_p = True;
526 reshape_slideshow (ModeInfo *mi, int width, int height)
528 slideshow_state *ss = &sss[MI_SCREEN(mi)];
530 glViewport (0, 0, width, height);
531 glMatrixMode (GL_PROJECTION);
533 glMatrixMode (GL_MODELVIEW);
540 s *= (zoom / 100.0) * 0.75;
541 if (s < 0.1) s = 0.1;
545 glTranslatef (-0.5, -0.5, 0);
547 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
549 ss->redisplay_needed_p = True;
554 glslideshow_handle_event (ModeInfo *mi, XEvent *event)
556 slideshow_state *ss = &sss[MI_SCREEN(mi)];
558 if (event->xany.type == Expose ||
559 event->xany.type == GraphicsExpose ||
560 event->xany.type == VisibilityNotify)
563 fprintf (stderr, "%s: debug: exposure\n", progname);
564 ss->redisplay_needed_p = True;
571 /* Do some sanity checking on various user-supplied values, and make
572 sure they are all internally consistent.
575 sanity_check (ModeInfo *mi)
577 if (zoom < 1) zoom = 1; /* zoom is a positive percentage */
578 else if (zoom > 100) zoom = 100;
580 if (pan_seconds < fade_seconds) /* pan is inclusive of fade */
581 pan_seconds = fade_seconds;
583 if (pan_seconds == 0) /* no zero-length cycles, please... */
586 if (image_seconds < pan_seconds) /* we only change images at fade-time */
587 image_seconds = pan_seconds;
589 /* If we're not panning/zooming within the image, then there's no point
590 in crossfading the image with itself -- only do crossfades when changing
592 if (zoom == 100 && pan_seconds < image_seconds)
593 pan_seconds = image_seconds;
595 if (fps_cutoff < 0) fps_cutoff = 0;
596 else if (fps_cutoff > 30) fps_cutoff = 30;
601 init_slideshow (ModeInfo *mi)
603 int screen = MI_SCREEN(mi);
605 int wire = MI_IS_WIREFRAME(mi);
609 if ((sss = (slideshow_state *)
610 calloc (MI_NUM_SCREENS(mi), sizeof(slideshow_state))) == NULL)
615 if ((ss->glx_context = init_GL(mi)) != NULL) {
616 reshape_slideshow (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
624 fprintf (stderr, "%s: debug: pan: %d; fade: %d; img: %d; zoom: %d%%\n",
625 progname, pan_seconds, fade_seconds, image_seconds, zoom);
629 glShadeModel (GL_SMOOTH);
630 glPolygonMode (GL_FRONT_AND_BACK,GL_FILL);
631 glEnable (GL_DEPTH_TEST);
632 glEnable (GL_CULL_FACE);
633 glCullFace (GL_FRONT);
634 glDisable (GL_LIGHTING);
637 ss->now = double_time ();
638 ss->dawn_of_time = ss->now;
640 if (debug_p) glLineWidth (3);
642 ss->pan_start_time = ss->now;
643 ss->image_start_time = ss->now;
645 for (i = 0; i < countof(ss->texids); i++)
646 glGenTextures (1, &ss->texids[i]);
647 ss->current_texid = 0;
649 for (i = 0; i < countof(ss->quads); i++)
651 quad *q = &ss->quads[i];
652 q->texid = ss->current_texid;
658 load_quad (mi, &ss->quads[0]);
659 ss->quads[0].state = IN;
661 ss->redisplay_needed_p = True;
665 /* Call this each time we change from one state to another.
666 It gathers statistics on the frame rate of the previous state,
667 and if it's bad, turn things off (under the assumption that
668 we're running on sucky hardware.)
671 ponder_state_change (ModeInfo *mi)
673 slideshow_state *ss = &sss[MI_SCREEN(mi)];
678 if (ss->fade_frame_count && ss->pan_frame_count)
679 abort(); /* one of these should be zero! */
680 else if (ss->fade_frame_count) /* just finished fading */
684 frames = ss->fade_frame_count;
685 ss->fade_frame_count = 0;
687 else if (ss->pan_frame_count) /* just finished panning */
691 frames = ss->pan_frame_count;
692 ss->pan_frame_count = 0;
695 abort(); /* one of these should be non-zero! */
697 fps = frames / (GLfloat) secs;
700 fprintf (stderr, "%s: debug: %s %3d frames %2d sec %4.1f fps\n",
701 progname, which, frames, secs, fps);
704 if (fps < fps_cutoff && !ss->low_fps_p) /* oops, this computer sucks! */
709 "%s: frame rate is only %.1f! "
710 "Turning off pan/fade to compensate...\n",
714 ss->low_fps_p = True;
718 /* Reset all quads, and mark only #0 as active. */
719 for (i = 0; i < countof(ss->quads); i++)
721 quad *q = &ss->quads[i];
724 q->texid = ss->current_texid;
725 q->state = (i == 0 ? IN : DEAD);
728 ss->pan_start_time = ss->now;
729 ss->redisplay_needed_p = True;
731 /* Need this in case zoom changed. */
732 reshape_slideshow (mi, mi->xgwa.width, mi->xgwa.height);
738 draw_slideshow (ModeInfo *mi)
740 slideshow_state *ss = &sss[MI_SCREEN(mi)];
741 Window w = MI_WINDOW(mi);
744 if (!ss->glx_context)
748 ss->redisplay_needed_p = True;
751 0: - A invisible, B invisible
752 - A fading in, B invisible
754 1: - A opaque, B invisible
755 - A fading out, B fading in
756 - A invisible, gets reset
757 - A invisible, B opaque
759 2: - A invisible, B opaque
760 - A fading in, B fading out
761 - B invisible, gets reset
762 - A opaque, B invisible (goto 1)
765 ss->now = double_time();
767 secs = ss->now - ss->pan_start_time;
769 if (secs < fade_seconds)
771 /* We are in the midst of a fade:
772 one quad is fading in, the other is fading out.
773 (If this is the very first time, then the one
774 fading out is already out.)
776 ss->redisplay_needed_p = True;
777 ss->fade_frame_count++;
779 if (! ((ss->quads[0].state == IN && ss->quads[1].state == OUT) ||
780 (ss->quads[1].state == IN && ss->quads[0].state == OUT) ||
781 (ss->quads[0].state == IN && ss->quads[1].state == DEAD)))
784 else if (secs < pan_seconds)
786 /* One quad is visible and in motion, the other is not.
788 if (ss->fade_frame_count != 0) /* we just switched from fade to pan */
789 ponder_state_change (mi);
790 ss->pan_frame_count++;
794 /* One quad is visible and in motion, the other is not.
795 It's time to begin fading the visible one out, and the
796 invisible one in. (Reset the invisible one first.)
800 ponder_state_change (mi);
802 if (ss->quads[0].state == IN)
813 if (vq->state != IN) abort();
815 /* I don't understand why sometimes iq is still OUT and not DEAD. */
816 if (iq->state == OUT) iq->state = DEAD;
817 if (iq->state != DEAD) abort();
821 if (ss->image_start_time + image_seconds <= ss->now)
824 reset_quad (mi, iq); /* fade invisible in */
825 iq->texid = ss->current_texid; /* make sure we're using latest img */
827 ss->pan_start_time = ss->now;
829 if (! ((ss->quads[0].state == IN && ss->quads[1].state == OUT) ||
830 (ss->quads[1].state == IN && ss->quads[0].state == OUT)))
834 ss->fps = fps_1 (mi);
836 if (!ss->redisplay_needed_p)
838 else if (debug_p && zoom == 100)
839 fprintf (stderr, "%s: debug: drawing (%d)\n", progname,
840 (int) (ss->now - ss->dawn_of_time));
843 ss->redisplay_needed_p = False;
845 if (mi->fps_p) fps_2(mi);
848 glXSwapBuffers (MI_DISPLAY (mi), w);