1 /* carousel, Copyright (c) 2005 Jamie Zawinski <jwz@jwz.org>
2 * Loads a sequence of images and rotates them around.
4 * Permission to use, copy, modify, distribute, and sell this software and its
5 * documentation for any purpose is hereby granted without fee, provided that
6 * the above copyright notice appear in all copies and that both that
7 * copyright notice and this permission notice appear in supporting
8 * documentation. No representations are made about the suitability of this
9 * software for any purpose. It is provided "as is" without express or
12 * Created: 21-Feb-2005
15 #include <X11/Intrinsic.h>
17 # define PROGCLASS "Carousel"
18 # define HACK_INIT init_carousel
19 # define HACK_DRAW draw_carousel
20 # define HACK_RESHAPE reshape_carousel
21 # define HACK_HANDLE_EVENT carousel_handle_event
22 # define EVENT_MASK PointerMotionMask
23 # define carousel_opts xlockmore_opts
25 # define DEF_SPEED "1.0"
26 # define DEF_DURATION "20"
27 # define DEF_TITLES "True"
28 # define DEF_ZOOM "True"
29 # define DEF_TILT "XY"
30 # define DEF_MIPMAP "True"
31 # define DEF_DEBUG "False"
33 #define DEF_FONT "-*-times-bold-r-normal-*-240-*"
35 #define DEFAULTS "*count: 7 \n" \
37 "*wireframe: False \n" \
38 "*showFPS: False \n" \
39 "*fpsSolid: True \n" \
41 "*font: " DEF_FONT "\n" \
42 "*desktopGrabber: xscreensaver-getimage -no-desktop %s\n"
44 # include "xlockmore.h"
47 #define countof(x) (sizeof((x))/sizeof((*x)))
57 #include "gltrackball.h"
58 #include "grab-ximage.h"
61 extern XtAppContext app;
67 typedef enum { NORMAL, OUT, LOADING, IN, DEAD } fade_mode;
68 static int fade_ticks = 60;
73 char *title; /* the filename of this image */
74 int w, h; /* size in pixels of the image */
75 int tw, th; /* size in pixels of the texture */
76 XRectangle geom; /* where in the image the bits are */
77 GLuint texid; /* which texture contains the image */
79 GLfloat r, theta; /* radius and rotation on the tube */
80 rotator *rot; /* for zoomery */
81 Bool from_top_p; /* whether this image drops in or rises up */
82 time_t expires; /* when this image should be replaced */
83 fade_mode mode; /* in/out animation state */
85 Bool loaded_p; /* whether background load is done */
91 GLXContext *glx_context;
93 trackball_state *trackball;
95 time_t button_down_time;
97 int nimages; /* how many images are loaded */
99 image **images; /* pointers to the images */
101 texture_font_data *texfont;
108 static carousel_state *sss = NULL;
111 /* Command-line arguments
113 static GLfloat speed; /* animation speed scale factor */
114 static int duration; /* reload images after this long */
115 static Bool mipmap_p; /* Use mipmaps instead of single textures. */
116 static Bool titles_p; /* Display image titles. */
117 static Bool zoom_p; /* Throb the images in and out as they spin. */
118 static char *tilt_str;
119 static Bool tilt_x_p; /* Tilt axis towards the viewer */
120 static Bool tilt_y_p; /* Tilt axis side to side */
121 static Bool debug_p; /* Be loud and do weird things. */
124 static XrmOptionDescRec opts[] = {
125 {"-zoom", ".zoom", XrmoptionNoArg, "True" },
126 {"-no-zoom", ".zoom", XrmoptionNoArg, "False" },
127 {"-tilt", ".tilt", XrmoptionSepArg, 0 },
128 {"-no-tilt", ".tilt", XrmoptionNoArg, "" },
129 {"-titles", ".titles", XrmoptionNoArg, "True" },
130 {"-no-titles", ".titles", XrmoptionNoArg, "False" },
131 {"-mipmaps", ".mipmap", XrmoptionNoArg, "True" },
132 {"-no-mipmaps", ".mipmap", XrmoptionNoArg, "False" },
133 {"-duration", ".duration", XrmoptionSepArg, 0 },
134 {"-debug", ".debug", XrmoptionNoArg, "True" },
135 {"-font", ".font", XrmoptionSepArg, 0 },
136 {"-speed", ".speed", XrmoptionSepArg, 0 },
139 static argtype vars[] = {
140 { &mipmap_p, "mipmap", "Mipmap", DEF_MIPMAP, t_Bool},
141 { &debug_p, "debug", "Debug", DEF_DEBUG, t_Bool},
142 { &titles_p, "titles", "Titles", DEF_TITLES, t_Bool},
143 { &zoom_p, "zoom", "Zoom", DEF_ZOOM, t_Bool},
144 { &tilt_str, "tilt", "Tilt", DEF_TILT, t_String},
145 { &speed, "speed", "Speed", DEF_SPEED, t_Float},
146 { &duration, "duration", "Duration", DEF_DURATION, t_Int},
149 ModeSpecOpt carousel_opts = {countof(opts), opts, countof(vars), vars, NULL};
152 /* Allocates an image structure and stores it in the list.
155 alloc_image (ModeInfo *mi)
157 carousel_state *ss = &sss[MI_SCREEN(mi)];
158 image *img = (image *) calloc (1, sizeof (*img));
161 img->rot = make_rotator (0, 0, 0, 0, 0.04 * frand(1.0) * speed, False);
163 glGenTextures (1, &img->texid);
164 if (img->texid <= 0) abort();
166 if (ss->images_size <= ss->nimages)
168 ss->images_size = (ss->images_size * 1.2) + ss->nimages;
169 ss->images = (image **)
170 realloc (ss->images, ss->images_size * sizeof(*ss->images));
173 fprintf (stderr, "%s: out of memory (%d images)\n",
174 progname, ss->images_size);
179 ss->images[ss->nimages++] = img;
185 static void image_loaded_cb (const char *filename, XRectangle *geom,
186 int image_width, int image_height,
187 int texture_width, int texture_height,
191 /* Load a new file into the given image struct.
194 load_image (ModeInfo *mi, image *img, Bool force_sync_p)
196 carousel_state *ss = &sss[MI_SCREEN(mi)];
197 int wire = MI_IS_WIREFRAME(mi);
198 Bool async_p = !force_sync_p;
201 if (debug_p && !wire && img->w != 0)
202 fprintf (stderr, "%s: dropped %4d x %-4d %4d x %-4d \"%s\"\n",
203 progname, img->geom.width, img->geom.height, img->tw, img->th,
204 (img->title ? img->title : "(null)"));
215 image_loaded_cb (0, 0, 0, 0, 0, 0, img);
217 screen_to_texture_async (mi->xgwa.screen, mi->window,
220 mipmap_p, img->texid, image_loaded_cb, img);
225 int iw=0, ih=0, tw=0, th=0;
226 time_t start = time((time_t *) 0);
229 glBindTexture (GL_TEXTURE_2D, img->texid);
230 if (! screen_to_texture (mi->xgwa.screen, mi->window,
233 mipmap_p, &filename, &geom, &iw, &ih, &tw, &th))
235 image_loaded_cb (filename, &geom, iw, ih, tw, th, img);
236 if (filename) free (filename);
238 /* Push the expire times of all images forward by the amount of time
239 it took to load *this* image, so that we don't count image-loading
240 time against image duration.
242 end = time((time_t *) 0);
243 for (i = 0; i < ss->nimages; i++)
244 ss->images[i]->expires += end - start;
249 /* Callback that tells us that the texture has been loaded.
252 image_loaded_cb (const char *filename, XRectangle *geom,
253 int image_width, int image_height,
254 int texture_width, int texture_height,
257 image *img = (image *) closure;
258 ModeInfo *mi = img->mi;
259 /* slideshow_state *ss = &sss[MI_SCREEN(mi)]; */
260 int wire = MI_IS_WIREFRAME(mi);
264 img->w = MI_WIDTH (mi) * (0.5 + frand (1.0));
265 img->h = MI_HEIGHT (mi);
266 img->geom.width = img->w;
267 img->geom.height = img->h;
271 if (image_width == 0 || image_height == 0)
274 glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
275 glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
276 mipmap_p ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR);
278 glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
279 glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
281 img->w = image_width;
282 img->h = image_height;
283 img->tw = texture_width;
284 img->th = texture_height;
286 img->title = (filename ? strdup (filename) : 0);
288 if (img->title) /* strip filename to part after last /. */
290 char *s = strrchr (img->title, '/');
291 if (s) strcpy (img->title, s+1);
295 fprintf (stderr, "%s: loaded %4d x %-4d %4d x %-4d \"%s\"\n",
297 img->geom.width, img->geom.height, img->tw, img->th,
298 (img->title ? img->title : "(null)"));
302 /* This image expires N seconds after it finished loading. */
303 img->expires = time((time_t *) 0) + (duration * MI_COUNT(mi));
306 img->mode_tick = fade_ticks * speed;
307 img->from_top_p = random() & 1;
308 img->loaded_p = True;
314 /* Free the image and texture, after nobody is referencing it.
318 destroy_image (ModeInfo *mi, image *img)
320 carousel_state *ss = &sss[MI_SCREEN(mi)];
321 Bool freed_p = False;
325 if (img->texid <= 0) abort();
327 for (i = 0; i < ss->nimages; i++) /* unlink it from the list */
328 if (ss->images[i] == img)
331 for (j = i; j < ss->nimages-1; j++) /* pull remainder forward */
332 ss->images[j] = ss->images[j+1];
339 if (!freed_p) abort();
342 fprintf (stderr, "%s: unloaded %4d x %-4d %4d x %-4d \"%s\"\n",
344 img->geom.width, img->geom.height, img->tw, img->th,
345 (img->title ? img->title : "(null)"));
347 if (img->title) free (img->title);
348 glDeleteTextures (1, &img->texid);
354 static void loading_msg (ModeInfo *mi, int n);
357 load_initial_images (ModeInfo *mi)
359 carousel_state *ss = &sss[MI_SCREEN(mi)];
362 Bool async_p = True; /* computed after first image */
364 for (i = 0; i < MI_COUNT(mi); i++)
367 loading_msg (mi, (async_p ? 0 : i));
368 img = alloc_image (mi);
370 img->theta = i * 360.0 / MI_COUNT(mi);
372 load_image (mi, img, True);
374 async_p = !img->loaded_p;
377 /* Wait for the images to load...
384 for (j = 0; j < MI_COUNT(mi); j++)
385 if (ss->images[j]->loaded_p)
387 if (count == MI_COUNT(mi))
389 loading_msg (mi, count);
391 usleep (100000); /* check every 1/10th sec */
392 if (i++ > 600) abort(); /* if a minute has passed, we're broken */
394 while (XtAppPending (app) & (XtIMTimer|XtIMAlternateInput))
395 XtAppProcessEvent (app, XtIMTimer|XtIMAlternateInput);
398 /* Stagger expire times of the first batch of images.
400 now = time((time_t *) 0);
401 for (i = 0; i < ss->nimages; i++)
403 image *img = ss->images[i];
404 img->expires = now + (duration * (i + 1));
408 /* Instead of always going clockwise, shuffle the expire times
409 of the images so that they drop out in a random order.
411 for (i = 0; i < ss->nimages; i++)
413 image *img1 = ss->images[i];
414 image *img2 = ss->images[random() % ss->nimages];
415 time_t swap = img1->expires;
416 img1->expires = img2->expires;
417 img2->expires = swap;
423 reshape_carousel (ModeInfo *mi, int width, int height)
425 GLfloat h = (GLfloat) height / (GLfloat) width;
427 glViewport (0, 0, (GLint) width, (GLint) height);
429 glMatrixMode(GL_PROJECTION);
431 gluPerspective (60.0, 1/h, 1.0, 8.0);
433 glMatrixMode(GL_MODELVIEW);
435 gluLookAt( 0.0, 0.0, 2.6,
439 glClear(GL_COLOR_BUFFER_BIT);
444 carousel_handle_event (ModeInfo *mi, XEvent *event)
446 carousel_state *ss = &sss[MI_SCREEN(mi)];
448 if (event->xany.type == ButtonPress &&
449 event->xbutton.button == Button1)
451 if (! ss->button_down_p)
452 ss->button_down_time = time((time_t *) 0);
453 ss->button_down_p = True;
454 gltrackball_start (ss->trackball,
455 event->xbutton.x, event->xbutton.y,
456 MI_WIDTH (mi), MI_HEIGHT (mi));
459 else if (event->xany.type == ButtonRelease &&
460 event->xbutton.button == Button1)
462 if (ss->button_down_p)
464 /* Add the time the mouse was held to the expire times of all
465 images, so that mouse-dragging doesn't count against
468 int secs = time((time_t *) 0) - ss->button_down_time;
470 for (i = 0; i < ss->nimages; i++)
471 ss->images[i]->expires += secs;
473 ss->button_down_p = False;
476 else if (event->xany.type == ButtonPress &&
477 (event->xbutton.button == Button4 ||
478 event->xbutton.button == Button5))
480 gltrackball_mousewheel (ss->trackball, event->xbutton.button, 5,
481 !event->xbutton.state);
484 else if (event->xany.type == MotionNotify &&
487 gltrackball_track (ss->trackball,
488 event->xmotion.x, event->xmotion.y,
489 MI_WIDTH (mi), MI_HEIGHT (mi));
497 /* Kludge to add "-v" to invocation of "xscreensaver-getimage" in -debug mode
500 hack_resources (void)
503 char *res = "desktopGrabber";
504 char *val = get_string_resource (res, "DesktopGrabber");
508 sprintf (buf1, "%.100s.%.100s", progclass, res);
509 sprintf (buf2, "%.200s -v", val);
511 value.size = strlen(buf2);
512 XrmPutResource (&db, buf1, "String", &value);
518 loading_msg (ModeInfo *mi, int n)
520 carousel_state *ss = &sss[MI_SCREEN(mi)];
521 int wire = MI_IS_WIREFRAME(mi);
523 static int sw=0, sh=0;
529 sprintf (text, "Loading images...");
531 sprintf (text, "Loading images... (%d%%)",
532 (int) (n * 100 / MI_COUNT(mi)));
534 if (sw == 0) /* only do this once, so that the string doesn't move. */
535 sw = texture_string_width (ss->texfont, text, &sh);
537 scale = sh / (GLfloat) MI_HEIGHT(mi);
539 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
541 glMatrixMode(GL_PROJECTION);
545 glMatrixMode(GL_MODELVIEW);
548 gluOrtho2D(0, MI_WIDTH(mi), 0, MI_HEIGHT(mi));
550 glTranslatef ((MI_WIDTH(mi) - sw) / 2,
551 (MI_HEIGHT(mi) - sh) / 2,
554 glEnable (GL_TEXTURE_2D);
555 print_texture_string (ss->texfont, text);
558 glMatrixMode(GL_PROJECTION);
561 glMatrixMode(GL_MODELVIEW);
564 glXSwapBuffers (MI_DISPLAY (mi), MI_WINDOW(mi));
569 init_carousel (ModeInfo *mi)
571 int screen = MI_SCREEN(mi);
573 int wire = MI_IS_WIREFRAME(mi);
576 if ((sss = (carousel_state *)
577 calloc (MI_NUM_SCREENS(mi), sizeof(carousel_state))) == NULL)
582 if ((ss->glx_context = init_GL(mi)) != NULL) {
583 reshape_carousel (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
588 if (!tilt_str || !*tilt_str)
590 else if (!strcasecmp (tilt_str, "X"))
592 else if (!strcasecmp (tilt_str, "Y"))
594 else if (!strcasecmp (tilt_str, "XY"))
595 tilt_x_p = tilt_y_p = 1;
598 fprintf (stderr, "%s: tilt must be 'X', 'Y', 'XY' or '', not '%s'\n",
604 double spin_speed = speed * 0.2; /* rotation of tube around axis */
605 double spin_accel = speed * 0.1;
606 double wander_speed = speed * 0.001; /* tilting of axis */
608 spin_speed *= 0.9 + frand(0.2);
609 wander_speed *= 0.9 + frand(0.2);
611 ss->rot = make_rotator (spin_speed, spin_speed, spin_speed,
612 spin_accel, wander_speed, True);
614 ss->trackball = gltrackball_init ();
617 glDisable (GL_LIGHTING);
618 glEnable (GL_DEPTH_TEST);
619 glDisable (GL_CULL_FACE);
623 glShadeModel (GL_SMOOTH);
624 glEnable (GL_LINE_SMOOTH);
625 /* This gives us a transparent diagonal slice through each image! */
626 /* glEnable (GL_POLYGON_SMOOTH); */
627 glHint (GL_LINE_SMOOTH_HINT, GL_NICEST);
629 glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
630 glEnable (GL_ALPHA_TEST);
632 glEnable (GL_POLYGON_OFFSET_FILL);
633 glPolygonOffset (1.0, 1.0);
637 ss->texfont = load_texture_font (MI_DISPLAY(mi), "font");
643 ss->images_size = 10;
644 ss->images = (image **) calloc (1, ss->images_size * sizeof(*ss->images));
647 ss->mode_tick = fade_ticks * speed;
649 load_initial_images (mi);
654 draw_img (ModeInfo *mi, image *img, time_t now, Bool body_p)
656 carousel_state *ss = &sss[MI_SCREEN(mi)];
657 int wire = MI_IS_WIREFRAME(mi);
659 GLfloat texw = img->geom.width / (GLfloat) img->tw;
660 GLfloat texh = img->geom.height / (GLfloat) img->th;
661 GLfloat texx1 = img->geom.x / (GLfloat) img->tw;
662 GLfloat texy1 = img->geom.y / (GLfloat) img->th;
663 GLfloat texx2 = texx1 + texw;
664 GLfloat texy2 = texy1 + texh;
665 GLfloat aspect = img->geom.height / (GLfloat) img->geom.width;
667 glBindTexture (GL_TEXTURE_2D, img->texid);
671 /* Position this image on the wheel.
673 glRotatef (img->theta, 0, 1, 0);
674 glTranslatef (0, 0, img->r);
676 /* Scale down the image so that all N images fit on the wheel
677 without bumping in to each other.
683 case 1: t = -1.0; s = 1.7; break;
684 case 2: t = -0.8; s = 1.6; break;
685 case 3: t = -0.4; s = 1.5; break;
686 case 4: t = -0.2; s = 1.3; break;
687 default: t = 0.0; s = 6.0 / ss->nimages; break;
689 glTranslatef (0, 0, t);
693 /* Center this image on the wheel plane.
695 glTranslatef (-0.5, -(aspect/2), 0);
697 /* Move as per the "zoom in and out" setting.
702 /* Only use the Z component of the rotator for in/out position. */
703 get_position (img->rot, &x, &y, &z, !ss->button_down_p);
704 glTranslatef (0, 0, z/2);
707 /* Compute the "drop in and out" state.
712 if (!ss->button_down_p &&
716 img->mode_tick = fade_ticks * speed;
720 if (--img->mode_tick <= 0)
721 load_image (mi, img, False);
724 /* just wait, with the image off screen. */
727 if (--img->mode_tick <= 0)
736 if (img->mode != NORMAL)
738 GLfloat t = (img->mode == LOADING
741 ? img->mode_tick / (fade_ticks * speed)
742 : (((fade_ticks * speed) - img->mode_tick + 1) /
743 (fade_ticks * speed)));
745 if (img->from_top_p) t = -t;
746 glTranslatef (0, t, 0);
749 if (body_p) /* Draw the image quad. */
754 glNormal3f (0, 0, 1);
755 glEnable (GL_TEXTURE_2D);
757 glNormal3f (0, 0, 1);
758 glTexCoord2f (texx1, texy2); glVertex3f (0, 0, 0);
759 glTexCoord2f (texx2, texy2); glVertex3f (1, 0, 0);
760 glTexCoord2f (texx2, texy1); glVertex3f (1, aspect, 0);
761 glTexCoord2f (texx1, texy1); glVertex3f (0, aspect, 0);
765 /* Draw a box around it.
768 glColor3f (0.5, 0.5, 0.5);
769 glDisable (GL_TEXTURE_2D);
770 glBegin (GL_LINE_LOOP);
771 glVertex3f (0, 0, 0);
772 glVertex3f (1, 0, 0);
773 glVertex3f (1, aspect, 0);
774 glVertex3f (0, aspect, 0);
778 else /* Draw a title under the image. */
781 GLfloat scale = 0.05;
782 char *title = img->title ? img->title : "(untitled)";
783 sw = texture_string_width (ss->texfont, title, &sh);
785 glTranslatef (0, -scale, 0);
788 glScalef (scale, scale, scale);
790 glTranslatef (((1/scale) - sw) / 2, 0, 0);
795 glEnable (GL_TEXTURE_2D);
796 print_texture_string (ss->texfont, title);
800 glBegin (GL_LINE_LOOP);
801 glVertex3f (0, 0, 0);
802 glVertex3f (sw, 0, 0);
803 glVertex3f (sw, sh, 0);
804 glVertex3f (0, sh, 0);
814 draw_carousel (ModeInfo *mi)
816 carousel_state *ss = &sss[MI_SCREEN(mi)];
817 static time_t last_time = 0;
818 static time_t now = 0;
821 if (!ss->glx_context)
824 /* Only check the wall clock every 10 frames */
827 if (now == 0 || tick++ > 10)
829 now = time((time_t *) 0);
830 if (last_time == 0) last_time = now;
836 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
841 /* Run the startup "un-shrink" animation.
846 if (--ss->mode_tick <= 0)
849 last_time = time((time_t *) 0);
859 /* Scale as per the startup "un-shrink" animation.
861 if (ss->mode != NORMAL)
863 GLfloat s = (ss->mode == OUT
864 ? ss->mode_tick / (fade_ticks * speed)
865 : (((fade_ticks * speed) - ss->mode_tick + 1) /
866 (fade_ticks * speed)));
870 /* Rotate and tilt as per the user, and the motion modeller.
874 gltrackball_rotate (ss->trackball);
876 /* Tilt the tube up or down by up to 30 degrees */
877 get_position (ss->rot, &x, &y, &z, !ss->button_down_p);
879 glRotatef (15 - (x * 30), 1, 0, 0);
881 glRotatef (7 - (y * 14), 0, 0, 1);
883 /* Only use the Y component of the rotator. */
884 get_rotation (ss->rot, &x, &y, &z, !ss->button_down_p);
885 glRotatef (y * 360, 0, 1, 0);
888 /* First draw each image, then draw the titles. Insists that you
889 draw back-to-front in order to make alpha blending work properly,
890 so we need to draw all of the 100% opaque images before drawing
891 any of the not-100%-opaque titles.
893 for (i = 0; i < ss->nimages; i++)
894 draw_img (mi, ss->images[i], now, True);
896 for (i = 0; i < ss->nimages; i++)
897 draw_img (mi, ss->images[i], now, False);
901 if (mi->fps_p) do_fps (mi);
903 glXSwapBuffers (MI_DISPLAY (mi), MI_WINDOW(mi));