1 /* carousel, Copyright (c) 2005-2006 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 #define DEF_FONT "-*-times-bold-r-normal-*-240-*"
16 #define DEFAULTS "*count: 7 \n" \
18 "*wireframe: False \n" \
19 "*showFPS: False \n" \
20 "*fpsSolid: True \n" \
22 "*font: " DEF_FONT "\n" \
23 "*desktopGrabber: xscreensaver-getimage -no-desktop %s\n" \
24 "*grabDesktopImages: False \n" \
25 "*chooseRandomImages: True \n"
27 # define refresh_carousel 0
28 # define release_carousel 0
29 # include "xlockmore.h"
32 #define countof(x) (sizeof((x))/sizeof((*x)))
36 # define DEF_SPEED "1.0"
37 # define DEF_DURATION "20"
38 # define DEF_TITLES "True"
39 # define DEF_ZOOM "True"
40 # define DEF_TILT "XY"
41 # define DEF_MIPMAP "True"
42 # define DEF_DEBUG "False"
45 #include "gltrackball.h"
46 #include "grab-ximage.h"
50 # include <X11/Intrinsic.h> /* for XrmDatabase in -debug mode */
57 typedef enum { EARLY, NORMAL, LOADING, OUT, IN, DEAD } fade_mode;
58 static int fade_ticks = 60;
61 char *title; /* the filename of this image */
62 int w, h; /* size in pixels of the image */
63 int tw, th; /* size in pixels of the texture */
64 XRectangle geom; /* where in the image the bits are */
70 image current, loading;
71 GLfloat r, theta; /* radius and rotation on the tube */
72 rotator *rot; /* for zoomery */
73 Bool from_top_p; /* whether this image drops in or rises up */
74 time_t expires; /* when this image should be replaced */
75 fade_mode mode; /* in/out animation state */
77 Bool loaded_p; /* whether background load is done */
82 GLXContext *glx_context;
84 trackball_state *trackball;
86 time_t button_down_time;
88 int nframes; /* how many frames are loaded */
90 image_frame **frames; /* pointers to the frames */
92 Bool awaiting_first_images_p;
93 int loads_in_progress;
95 texture_font_data *texfont;
100 int loading_sw, loading_sh;
102 time_t last_time, now;
107 static carousel_state *sss = NULL;
110 /* Command-line arguments
112 static GLfloat speed; /* animation speed scale factor */
113 static int duration; /* reload images after this long */
114 static Bool mipmap_p; /* Use mipmaps instead of single textures. */
115 static Bool titles_p; /* Display image titles. */
116 static Bool zoom_p; /* Throb the images in and out as they spin. */
117 static char *tilt_str;
118 static Bool tilt_x_p; /* Tilt axis towards the viewer */
119 static Bool tilt_y_p; /* Tilt axis side to side */
120 static Bool debug_p; /* Be loud and do weird things. */
123 static XrmOptionDescRec opts[] = {
124 {"-zoom", ".zoom", XrmoptionNoArg, "True" },
125 {"-no-zoom", ".zoom", XrmoptionNoArg, "False" },
126 {"-tilt", ".tilt", XrmoptionSepArg, 0 },
127 {"-no-tilt", ".tilt", XrmoptionNoArg, "" },
128 {"-titles", ".titles", XrmoptionNoArg, "True" },
129 {"-no-titles", ".titles", XrmoptionNoArg, "False" },
130 {"-mipmaps", ".mipmap", XrmoptionNoArg, "True" },
131 {"-no-mipmaps", ".mipmap", XrmoptionNoArg, "False" },
132 {"-duration", ".duration", XrmoptionSepArg, 0 },
133 {"-debug", ".debug", XrmoptionNoArg, "True" },
134 {"-font", ".font", XrmoptionSepArg, 0 },
135 {"-speed", ".speed", XrmoptionSepArg, 0 },
138 static argtype vars[] = {
139 { &mipmap_p, "mipmap", "Mipmap", DEF_MIPMAP, t_Bool},
140 { &debug_p, "debug", "Debug", DEF_DEBUG, t_Bool},
141 { &titles_p, "titles", "Titles", DEF_TITLES, t_Bool},
142 { &zoom_p, "zoom", "Zoom", DEF_ZOOM, t_Bool},
143 { &tilt_str, "tilt", "Tilt", DEF_TILT, t_String},
144 { &speed, "speed", "Speed", DEF_SPEED, t_Float},
145 { &duration, "duration", "Duration", DEF_DURATION, t_Int},
148 ENTRYPOINT ModeSpecOpt carousel_opts = {countof(opts), opts, countof(vars), vars, NULL};
151 /* Allocates a frame structure and stores it in the list.
154 alloc_frame (ModeInfo *mi)
156 carousel_state *ss = &sss[MI_SCREEN(mi)];
157 image_frame *frame = (image_frame *) calloc (1, sizeof (*frame));
161 frame->rot = make_rotator (0, 0, 0, 0, 0.04 * frand(1.0) * speed, False);
163 glGenTextures (1, &frame->current.texid);
164 glGenTextures (1, &frame->loading.texid);
165 if (frame->current.texid <= 0) abort();
166 if (frame->loading.texid <= 0) abort();
168 if (ss->frames_size <= ss->nframes)
170 ss->frames_size = (ss->frames_size * 1.2) + ss->nframes;
171 ss->frames = (image_frame **)
172 realloc (ss->frames, ss->frames_size * sizeof(*ss->frames));
175 fprintf (stderr, "%s: out of memory (%d images)\n",
176 progname, ss->frames_size);
181 ss->frames[ss->nframes++] = frame;
187 static void image_loaded_cb (const char *filename, XRectangle *geom,
188 int image_width, int image_height,
189 int texture_width, int texture_height,
193 /* Load a new file into the given image struct.
196 load_image (ModeInfo *mi, image_frame *frame)
198 carousel_state *ss = &sss[MI_SCREEN(mi)];
199 int wire = MI_IS_WIREFRAME(mi);
201 if (debug_p && !wire && frame->current.w != 0)
202 fprintf (stderr, "%s: dropped %4d x %-4d %4d x %-4d \"%s\"\n",
204 frame->current.geom.width,
205 frame->current.geom.height,
206 frame->current.tw, frame->current.th,
207 (frame->current.title ? frame->current.title : "(null)"));
212 case NORMAL: frame->mode = LOADING; break;
216 ss->loads_in_progress++;
219 image_loaded_cb (0, 0, 0, 0, 0, 0, frame);
222 int w = (MI_WIDTH(mi) / 2) - 1;
223 int h = (MI_HEIGHT(mi) / 2) - 1;
226 load_texture_async (mi->xgwa.screen, mi->window, *ss->glx_context, w, h,
227 mipmap_p, frame->loading.texid,
228 image_loaded_cb, frame);
233 /* Callback that tells us that the texture has been loaded.
236 image_loaded_cb (const char *filename, XRectangle *geom,
237 int image_width, int image_height,
238 int texture_width, int texture_height,
241 image_frame *frame = (image_frame *) closure;
242 ModeInfo *mi = frame->mi;
243 carousel_state *ss = &sss[MI_SCREEN(mi)];
244 int wire = MI_IS_WIREFRAME(mi);
248 frame->loading.w = MI_WIDTH (mi) * (0.5 + frand (1.0));
249 frame->loading.h = MI_HEIGHT (mi);
250 frame->loading.geom.width = frame->loading.w;
251 frame->loading.geom.height = frame->loading.h;
255 if (image_width == 0 || image_height == 0)
258 glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
259 glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
260 mipmap_p ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR);
262 glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
263 glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
265 frame->loading.w = image_width;
266 frame->loading.h = image_height;
267 frame->loading.tw = texture_width;
268 frame->loading.th = texture_height;
269 frame->loading.geom = *geom;
271 if (frame->loading.title)
272 free (frame->loading.title);
273 frame->loading.title = (filename ? strdup (filename) : 0);
275 if (frame->loading.title) /* strip filename to part after last /. */
277 char *s = strrchr (frame->loading.title, '/');
278 if (s) strcpy (frame->loading.title, s+1);
282 fprintf (stderr, "%s: loaded %4d x %-4d %4d x %-4d \"%s\"\n",
284 frame->loading.geom.width,
285 frame->loading.geom.height,
286 frame->loading.tw, frame->loading.th,
287 (frame->loading.title ? frame->loading.title : "(null)"));
291 frame->loaded_p = True;
293 if (ss->loads_in_progress <= 0) abort();
294 ss->loads_in_progress--;
296 /* This image expires N seconds after it finished loading. */
297 frame->expires = time((time_t *) 0) + (duration * MI_COUNT(mi));
301 case EARLY: /* part of the initial batch of images */
303 image swap = frame->current;
304 frame->current = frame->loading;
305 frame->loading = swap;
308 case LOADING: /* start dropping the old image out */
311 frame->mode_tick = fade_ticks / speed;
312 frame->from_top_p = random() & 1;
321 static void loading_msg (ModeInfo *mi, int n);
324 load_initial_images (ModeInfo *mi)
326 carousel_state *ss = &sss[MI_SCREEN(mi)];
328 Bool all_loaded_p = True;
329 for (i = 0; i < ss->nframes; i++)
330 if (! ss->frames[i]->loaded_p)
331 all_loaded_p = False;
335 if (ss->nframes < MI_COUNT (mi))
337 /* The frames currently on the list are fully loaded.
338 Start the next one loading. (We run the image loader
339 asynchronously, but we load them one at a time.)
341 load_image (mi, alloc_frame (mi));
345 /* The first batch of images are now all loaded!
346 Stagger the expire times so that they don't all drop out at once.
348 time_t now = time((time_t *) 0);
351 for (i = 0; i < ss->nframes; i++)
353 image_frame *frame = ss->frames[i];
355 frame->theta = i * 360.0 / ss->nframes;
356 frame->expires = now + (duration * (i + 1));
357 frame->mode = NORMAL;
360 /* Instead of always going clockwise, shuffle the expire times
361 of the frames so that they drop out in a random order.
363 for (i = 0; i < ss->nframes; i++)
365 image_frame *frame1 = ss->frames[i];
366 image_frame *frame2 = ss->frames[random() % ss->nframes];
367 time_t swap = frame1->expires;
368 frame1->expires = frame2->expires;
369 frame2->expires = swap;
372 ss->awaiting_first_images_p = False;
376 loading_msg (mi, ss->nframes-1);
378 return !ss->awaiting_first_images_p;
383 reshape_carousel (ModeInfo *mi, int width, int height)
385 GLfloat h = (GLfloat) height / (GLfloat) width;
387 glViewport (0, 0, (GLint) width, (GLint) height);
389 glMatrixMode(GL_PROJECTION);
391 gluPerspective (60.0, 1/h, 1.0, 8.0);
393 glMatrixMode(GL_MODELVIEW);
395 gluLookAt( 0.0, 0.0, 2.6,
399 glClear(GL_COLOR_BUFFER_BIT);
404 carousel_handle_event (ModeInfo *mi, XEvent *event)
406 carousel_state *ss = &sss[MI_SCREEN(mi)];
408 if (event->xany.type == ButtonPress &&
409 event->xbutton.button == Button1)
411 if (! ss->button_down_p)
412 ss->button_down_time = time((time_t *) 0);
413 ss->button_down_p = True;
414 gltrackball_start (ss->trackball,
415 event->xbutton.x, event->xbutton.y,
416 MI_WIDTH (mi), MI_HEIGHT (mi));
419 else if (event->xany.type == ButtonRelease &&
420 event->xbutton.button == Button1)
422 if (ss->button_down_p)
424 /* Add the time the mouse was held to the expire times of all
425 frames, so that mouse-dragging doesn't count against
428 int secs = time((time_t *) 0) - ss->button_down_time;
430 for (i = 0; i < ss->nframes; i++)
431 ss->frames[i]->expires += secs;
433 ss->button_down_p = False;
436 else if (event->xany.type == ButtonPress &&
437 (event->xbutton.button == Button4 ||
438 event->xbutton.button == Button5))
440 gltrackball_mousewheel (ss->trackball, event->xbutton.button, 5,
441 !event->xbutton.state);
444 else if (event->xany.type == MotionNotify &&
447 gltrackball_track (ss->trackball,
448 event->xmotion.x, event->xmotion.y,
449 MI_WIDTH (mi), MI_HEIGHT (mi));
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_COCOA */
479 loading_msg (ModeInfo *mi, int n)
481 carousel_state *ss = &sss[MI_SCREEN(mi)];
482 int wire = MI_IS_WIREFRAME(mi);
489 sprintf (text, "Loading images...");
491 sprintf (text, "Loading images... (%d%%)",
492 (int) (n * 100 / MI_COUNT(mi)));
494 if (ss->loading_sw == 0) /* only do this once, so that the string doesn't move. */
495 ss->loading_sw = texture_string_width (ss->texfont, text, &ss->loading_sh);
497 scale = ss->loading_sh / (GLfloat) MI_HEIGHT(mi);
499 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
501 glMatrixMode(GL_PROJECTION);
505 glMatrixMode(GL_MODELVIEW);
508 gluOrtho2D(0, MI_WIDTH(mi), 0, MI_HEIGHT(mi));
510 glTranslatef ((MI_WIDTH(mi) - ss->loading_sw) / 2,
511 (MI_HEIGHT(mi) - ss->loading_sh) / 2,
514 glEnable (GL_TEXTURE_2D);
515 glDisable (GL_DEPTH_TEST);
516 print_texture_string (ss->texfont, text);
517 glEnable (GL_DEPTH_TEST);
520 glMatrixMode(GL_PROJECTION);
523 glMatrixMode(GL_MODELVIEW);
526 glXSwapBuffers (MI_DISPLAY (mi), MI_WINDOW(mi));
531 init_carousel (ModeInfo *mi)
533 int screen = MI_SCREEN(mi);
535 int wire = MI_IS_WIREFRAME(mi);
538 if ((sss = (carousel_state *)
539 calloc (MI_NUM_SCREENS(mi), sizeof(carousel_state))) == NULL)
544 if ((ss->glx_context = init_GL(mi)) != NULL) {
545 reshape_carousel (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
550 if (!tilt_str || !*tilt_str)
552 else if (!strcasecmp (tilt_str, "0"))
554 else if (!strcasecmp (tilt_str, "X"))
556 else if (!strcasecmp (tilt_str, "Y"))
558 else if (!strcasecmp (tilt_str, "XY"))
559 tilt_x_p = tilt_y_p = 1;
562 fprintf (stderr, "%s: tilt must be 'X', 'Y', 'XY' or '', not '%s'\n",
568 double spin_speed = speed * 0.2; /* rotation of tube around axis */
569 double spin_accel = speed * 0.1;
570 double wander_speed = speed * 0.001; /* tilting of axis */
572 spin_speed *= 0.9 + frand(0.2);
573 wander_speed *= 0.9 + frand(0.2);
575 ss->rot = make_rotator (spin_speed, spin_speed, spin_speed,
576 spin_accel, wander_speed, True);
578 ss->trackball = gltrackball_init ();
581 glDisable (GL_LIGHTING);
582 glEnable (GL_DEPTH_TEST);
583 glDisable (GL_CULL_FACE);
587 glShadeModel (GL_SMOOTH);
588 glEnable (GL_LINE_SMOOTH);
589 /* This gives us a transparent diagonal slice through each image! */
590 /* glEnable (GL_POLYGON_SMOOTH); */
591 glHint (GL_LINE_SMOOTH_HINT, GL_NICEST);
593 glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
594 glEnable (GL_ALPHA_TEST);
596 glEnable (GL_POLYGON_OFFSET_FILL);
597 glPolygonOffset (1.0, 1.0);
601 ss->texfont = load_texture_font (MI_DISPLAY(mi), "font");
604 hack_resources (MI_DISPLAY (mi));
607 ss->frames_size = 10;
608 ss->frames = (image_frame **)
609 calloc (1, ss->frames_size * sizeof(*ss->frames));
612 ss->mode_tick = fade_ticks / speed;
614 ss->awaiting_first_images_p = True;
619 draw_frame (ModeInfo *mi, image_frame *frame, time_t now, Bool body_p)
621 carousel_state *ss = &sss[MI_SCREEN(mi)];
622 int wire = MI_IS_WIREFRAME(mi);
624 GLfloat texw = frame->current.geom.width / (GLfloat) frame->current.tw;
625 GLfloat texh = frame->current.geom.height / (GLfloat) frame->current.th;
626 GLfloat texx1 = frame->current.geom.x / (GLfloat) frame->current.tw;
627 GLfloat texy1 = frame->current.geom.y / (GLfloat) frame->current.th;
628 GLfloat texx2 = texx1 + texw;
629 GLfloat texy2 = texy1 + texh;
630 GLfloat aspect = ((GLfloat) frame->current.geom.height /
631 (GLfloat) frame->current.geom.width);
633 glBindTexture (GL_TEXTURE_2D, frame->current.texid);
637 /* Position this image on the wheel.
639 glRotatef (frame->theta, 0, 1, 0);
640 glTranslatef (0, 0, frame->r);
642 /* Scale down the image so that all N frames fit on the wheel
643 without bumping in to each other.
649 case 1: t = -1.0; s = 1.7; break;
650 case 2: t = -0.8; s = 1.6; break;
651 case 3: t = -0.4; s = 1.5; break;
652 case 4: t = -0.2; s = 1.3; break;
653 default: t = 0.0; s = 6.0 / ss->nframes; break;
655 glTranslatef (0, 0, t);
659 /* Center this image on the wheel plane.
661 glTranslatef (-0.5, -(aspect/2), 0);
663 /* Move as per the "zoom in and out" setting.
668 /* Only use the Z component of the rotator for in/out position. */
669 get_position (frame->rot, &x, &y, &z, !ss->button_down_p);
670 glTranslatef (0, 0, z/2);
673 /* Compute the "drop in and out" state.
681 if (!ss->button_down_p &&
682 now >= frame->expires &&
683 ss->loads_in_progress == 0) /* only load one at a time */
684 load_image (mi, frame);
689 if (--frame->mode_tick <= 0) {
690 image swap = frame->current;
691 frame->current = frame->loading;
692 frame->loading = swap;
695 frame->mode_tick = fade_ticks / speed;
699 if (--frame->mode_tick <= 0)
700 frame->mode = NORMAL;
706 /* Now translate for current in/out state.
708 if (frame->mode == OUT || frame->mode == IN)
710 GLfloat t = (frame->mode == OUT
711 ? frame->mode_tick / (fade_ticks / speed)
712 : (((fade_ticks / speed) - frame->mode_tick + 1) /
713 (fade_ticks / speed)));
715 if (frame->from_top_p) t = -t;
716 glTranslatef (0, t, 0);
719 if (body_p) /* Draw the image quad. */
724 glNormal3f (0, 0, 1);
725 glEnable (GL_TEXTURE_2D);
727 glNormal3f (0, 0, 1);
728 glTexCoord2f (texx1, texy2); glVertex3f (0, 0, 0);
729 glTexCoord2f (texx2, texy2); glVertex3f (1, 0, 0);
730 glTexCoord2f (texx2, texy1); glVertex3f (1, aspect, 0);
731 glTexCoord2f (texx1, texy1); glVertex3f (0, aspect, 0);
735 /* Draw a box around it.
738 glColor3f (0.5, 0.5, 0.5);
739 glDisable (GL_TEXTURE_2D);
740 glBegin (GL_LINE_LOOP);
741 glVertex3f (0, 0, 0);
742 glVertex3f (1, 0, 0);
743 glVertex3f (1, aspect, 0);
744 glVertex3f (0, aspect, 0);
748 else /* Draw a title under the image. */
751 GLfloat scale = 0.05;
752 char *title = frame->current.title ? frame->current.title : "(untitled)";
753 sw = texture_string_width (ss->texfont, title, &sh);
755 glTranslatef (0, -scale, 0);
758 glScalef (scale, scale, scale);
760 glTranslatef (((1/scale) - sw) / 2, 0, 0);
765 glEnable (GL_TEXTURE_2D);
766 print_texture_string (ss->texfont, title);
770 glBegin (GL_LINE_LOOP);
771 glVertex3f (0, 0, 0);
772 glVertex3f (sw, 0, 0);
773 glVertex3f (sw, sh, 0);
774 glVertex3f (0, sh, 0);
784 draw_carousel (ModeInfo *mi)
786 carousel_state *ss = &sss[MI_SCREEN(mi)];
789 if (!ss->glx_context)
792 glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *(ss->glx_context));
794 if (ss->awaiting_first_images_p)
795 if (!load_initial_images (mi))
798 /* Only check the wall clock every 10 frames */
800 if (ss->now == 0 || ss->draw_tick++ > 10)
802 ss->now = time((time_t *) 0);
803 if (ss->last_time == 0) ss->last_time = ss->now;
809 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
814 /* Run the startup "un-shrink" animation.
819 if (--ss->mode_tick <= 0)
822 ss->last_time = time((time_t *) 0);
832 /* Scale as per the startup "un-shrink" animation.
834 if (ss->mode != NORMAL)
836 GLfloat s = (ss->mode == OUT
837 ? ss->mode_tick / (fade_ticks / speed)
838 : (((fade_ticks / speed) - ss->mode_tick + 1) /
839 (fade_ticks / speed)));
843 /* Rotate and tilt as per the user, and the motion modeller.
847 gltrackball_rotate (ss->trackball);
849 /* Tilt the tube up or down by up to 30 degrees */
850 get_position (ss->rot, &x, &y, &z, !ss->button_down_p);
852 glRotatef (15 - (x * 30), 1, 0, 0);
854 glRotatef (7 - (y * 14), 0, 0, 1);
856 /* Only use the Y component of the rotator. */
857 get_rotation (ss->rot, &x, &y, &z, !ss->button_down_p);
858 glRotatef (y * 360, 0, 1, 0);
861 /* First draw each image, then draw the titles. GL insists that you
862 draw back-to-front in order to make alpha blending work properly,
863 so we need to draw all of the 100% opaque images before drawing
864 any of the not-100%-opaque titles.
866 for (i = 0; i < ss->nframes; i++)
867 draw_frame (mi, ss->frames[i], ss->now, True);
869 for (i = 0; i < ss->nframes; i++)
870 draw_frame (mi, ss->frames[i], ss->now, False);
874 if (mi->fps_p) do_fps (mi);
876 glXSwapBuffers (MI_DISPLAY (mi), MI_WINDOW(mi));
879 XSCREENSAVER_MODULE ("Carousel", carousel)