/* photopile, Copyright (c) 2008-2018 Jens Kilian * Based on carousel, Copyright (c) 2005-2008 Jamie Zawinski * Loads a sequence of images and shuffles them into a pile. * * Permission to use, copy, modify, distribute, and sell this software and its * documentation for any purpose is hereby granted without fee, provided that * the above copyright notice appear in all copies and that both that * copyright notice and this permission notice appear in supporting * documentation. No representations are made about the suitability of this * software for any purpose. It is provided "as is" without express or * implied warranty. */ #if defined(HAVE_COCOA) || defined(HAVE_ANDROID) # define DEF_FONT "OCR A Std 48, Lucida Console 48, Monaco 48" #elif 0 /* real X11, XQueryFont() */ # define DEF_FONT "-*-helvetica-bold-r-normal-*-*-480-*-*-*-*-*-*" #else /* real X11, load_font_retry() */ # define DEF_FONT "-*-ocr a std-medium-r-*-*-*-480-*-*-m-*-*-*" #endif #define DEFAULTS "*count: 7 \n" \ "*delay: 10000 \n" \ "*wireframe: False \n" \ "*showFPS: False \n" \ "*fpsSolid: True \n" \ "*useSHM: True \n" \ "*font: " DEF_FONT "\n" \ "*desktopGrabber: xscreensaver-getimage -no-desktop %s\n" \ "*grabDesktopImages: False \n" \ "*chooseRandomImages: True \n" \ "*suppressRotationAnimation: True\n" \ # define release_photopile 0 # define photopile_handle_event xlockmore_no_events #undef countof #define countof(x) (sizeof((x))/sizeof((*x))) #ifndef HAVE_JWXYZ # include /* for XrmDatabase in -debug mode */ #endif #include #include "xlockmore.h" #include "grab-ximage.h" #include "texfont.h" #include "dropshadow.h" #ifdef USE_GL # define DEF_SCALE "0.4" # define DEF_MAX_TILT "50" # define DEF_SPEED "1.0" # define DEF_DURATION "5" # define DEF_MIPMAP "True" # define DEF_TITLES "True" # define DEF_POLAROID "True" # define DEF_CLIP "True" # define DEF_SHADOWS "True" # define DEF_DEBUG "False" #define BELLRAND(n) ((frand((n)) + frand((n)) + frand((n))) / 3) typedef struct { GLfloat x, y; /* position on screen */ GLfloat angle; /* rotation angle */ } position; typedef struct { Bool loaded_p; /* true if image can be drawn */ char *title; /* the filename of this image */ int w, h; /* size in pixels of the image */ int tw, th; /* size in pixels of the texture */ XRectangle geom; /* where in the image the bits are */ position pos[4]; /* control points for calculating position */ GLuint texid; /* GL texture ID */ } image; typedef enum { EARLY, SHUFFLE, NORMAL, LOADING } fade_mode; static int fade_ticks = 60; typedef struct { ModeInfo *mi; GLXContext *glx_context; image *frames; /* pointer to array of images */ int nframe; /* image being (resp. next to be) loaded */ GLuint shadow; texture_font_data *texfont; int loading_sw, loading_sh; time_t last_time, now; int draw_tick; fade_mode mode; int mode_tick; } photopile_state; static photopile_state *sss = NULL; /* Command-line arguments */ static GLfloat scale; /* Scale factor for loading images. */ static GLfloat max_tilt; /* Maximum angle from vertical. */ static GLfloat speed; /* Animation speed scale factor. */ static int duration; /* Reload images after this long. */ static Bool mipmap_p; /* Use mipmaps instead of single textures. */ static Bool titles_p; /* Display image titles. */ static Bool polaroid_p; /* Use instant-film look for images. */ static Bool clip_p; /* Clip images instead of scaling for -polaroid. */ static Bool shadows_p; /* Draw drop shadows. */ static Bool debug_p; /* Be loud and do weird things. */ static XrmOptionDescRec opts[] = { {"-scale", ".scale", XrmoptionSepArg, 0 }, {"-maxTilt", ".maxTilt", XrmoptionSepArg, 0 }, {"-speed", ".speed", XrmoptionSepArg, 0 }, {"-duration", ".duration", XrmoptionSepArg, 0 }, {"-mipmaps", ".mipmap", XrmoptionNoArg, "True" }, {"-no-mipmaps", ".mipmap", XrmoptionNoArg, "False" }, {"-titles", ".titles", XrmoptionNoArg, "True" }, {"-no-titles", ".titles", XrmoptionNoArg, "False" }, {"-polaroid", ".polaroid", XrmoptionNoArg, "True" }, {"-no-polaroid", ".polaroid", XrmoptionNoArg, "False" }, {"-clip", ".clip", XrmoptionNoArg, "True" }, {"-no-clip", ".clip", XrmoptionNoArg, "False" }, {"-shadows", ".shadows", XrmoptionNoArg, "True" }, {"-no-shadows", ".shadows", XrmoptionNoArg, "False" }, {"-debug", ".debug", XrmoptionNoArg, "True" }, {"-font", ".font", XrmoptionSepArg, 0 }, }; static argtype vars[] = { { &scale, "scale", "Scale", DEF_SCALE, t_Float}, { &max_tilt, "maxTilt", "MaxTilt", DEF_MAX_TILT, t_Float}, { &speed, "speed", "Speed", DEF_SPEED, t_Float}, { &duration, "duration", "Duration", DEF_DURATION, t_Int}, { &mipmap_p, "mipmap", "Mipmap", DEF_MIPMAP, t_Bool}, { &titles_p, "titles", "Titles", DEF_TITLES, t_Bool}, { &polaroid_p, "polaroid", "Polaroid", DEF_POLAROID, t_Bool}, { &clip_p, "clip", "Clip", DEF_CLIP, t_Bool}, { &shadows_p, "shadows", "Shadows", DEF_SHADOWS, t_Bool}, { &debug_p, "debug", "Debug", DEF_DEBUG, t_Bool}, }; ENTRYPOINT ModeSpecOpt photopile_opts = {countof(opts), opts, countof(vars), vars, NULL}; /* Functions to interpolate between image positions. */ static position add_pos(position p, position q) { p.x += q.x; p.y += q.y; p.angle += q.angle; return p; } static position scale_pos(GLfloat t, position p) { p.x *= t; p.y *= t; p.angle *= t; return p; } static position linear_combination(GLfloat t, position p, position q) { return add_pos(scale_pos(1.0 - t, p), scale_pos(t, q)); } static position interpolate(GLfloat t, position p[4]) { /* de Casteljau's algorithm, 4 control points */ position p10 = linear_combination(t, p[0], p[1]); position p11 = linear_combination(t, p[1], p[2]); position p12 = linear_combination(t, p[2], p[3]); position p20 = linear_combination(t, p10, p11); position p21 = linear_combination(t, p11, p12); return linear_combination(t, p20, p21); } static position offset_pos(position p, GLfloat th, GLfloat r) { p.x += cos(th) * r; p.y += sin(th) * r; p.angle = (frand(2.0) - 1.0) * max_tilt; return p; } /* Calculate new positions for all images. */ static void set_new_positions(photopile_state *ss) { ModeInfo *mi = ss->mi; int i; for (i = 0; i < MI_COUNT(mi)+1; ++i) { image *frame = ss->frames + i; GLfloat w = frame->w; GLfloat h = frame->h; GLfloat d = sqrt(w*w + h*h); GLfloat leave = frand(M_PI * 2.0); GLfloat enter = frand(M_PI * 2.0); /* start position */ frame->pos[0] = frame->pos[3]; /* end position */ frame->pos[3].x = BELLRAND(MI_WIDTH(mi)); frame->pos[3].y = BELLRAND(MI_HEIGHT(mi)); frame->pos[3].angle = (frand(2.0) - 1.0) * max_tilt; /* Try to keep the images mostly inside the screen bounds */ frame->pos[3].x = MAX(0.5*w, MIN(MI_WIDTH(mi)-0.5*w, frame->pos[3].x)); frame->pos[3].y = MAX(0.5*h, MIN(MI_HEIGHT(mi)-0.5*h, frame->pos[3].y)); /* intermediate points */ frame->pos[1] = offset_pos(frame->pos[0], leave, d * (0.5 + frand(1.0))); frame->pos[2] = offset_pos(frame->pos[3], enter, d * (0.5 + frand(1.0))); } } /* Callback that tells us that the texture has been loaded. */ static void image_loaded_cb (const char *filename, XRectangle *geom, int image_width, int image_height, int texture_width, int texture_height, void *closure) { photopile_state *ss = (photopile_state *) closure; ModeInfo *mi = ss->mi; int wire = MI_IS_WIREFRAME(mi); image *frame = ss->frames + ss->nframe; if (wire) { if (random() % 2) { frame->w = (int)(MI_WIDTH(mi) * scale) - 1; frame->h = (int)(MI_HEIGHT(mi) * scale) - 1; } else { frame->w = (int)(MI_HEIGHT(mi) * scale) - 1; frame->h = (int)(MI_WIDTH(mi) * scale) - 1; } if (frame->w <= 10) frame->w = 10; if (frame->h <= 10) frame->h = 10; frame->geom.width = frame->w; frame->geom.height = frame->h; goto DONE; } if (image_width == 0 || image_height == 0) exit (1); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, mipmap_p ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); frame->w = image_width; frame->h = image_height; frame->tw = texture_width; frame->th = texture_height; frame->geom = *geom; if (frame->title) free (frame->title); frame->title = (filename ? strdup (filename) : 0); /* xscreensaver-getimage returns paths relative to the image directory now, so leave the sub-directory part in. Unless it's an absolute path. */ if (frame->title && frame->title[0] == '/') { /* strip filename to part after last /. */ char *s = strrchr (frame->title, '/'); if (s) strcpy (frame->title, s+1); } if (debug_p) fprintf (stderr, "%s: loaded %4d x %-4d %4d x %-4d \"%s\"\n", progname, frame->geom.width, frame->geom.height, frame->tw, frame->th, (frame->title ? frame->title : "(null)")); DONE: frame->loaded_p = True; } /* Load a new file. */ static void load_image (ModeInfo *mi) { photopile_state *ss = &sss[MI_SCREEN(mi)]; int wire = MI_IS_WIREFRAME(mi); image *frame = ss->frames + ss->nframe; if (debug_p && !wire && frame->w != 0) fprintf (stderr, "%s: dropped %4d x %-4d %4d x %-4d \"%s\"\n", progname, frame->geom.width, frame->geom.height, frame->tw, frame->th, (frame->title ? frame->title : "(null)")); frame->loaded_p = False; if (wire) image_loaded_cb (0, 0, 0, 0, 0, 0, ss); else { int w = MI_WIDTH(mi); int h = MI_HEIGHT(mi); int size = (int)((w > h ? w : h) * scale); if (size <= 10) size = 10; load_texture_async (mi->xgwa.screen, mi->window, *ss->glx_context, size, size, mipmap_p, frame->texid, image_loaded_cb, ss); } } static void loading_msg (ModeInfo *mi) { photopile_state *ss = &sss[MI_SCREEN(mi)]; int wire = MI_IS_WIREFRAME(mi); const char text[] = "Loading..."; if (wire) return; if (ss->loading_sw == 0) /* only do this once */ { XCharStruct e; texture_string_metrics (ss->texfont, text, &e, 0, 0); ss->loading_sw = e.width; ss->loading_sh = e.ascent + e.descent; } glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); glOrtho(0, MI_WIDTH(mi), 0, MI_HEIGHT(mi), -1, 1); glTranslatef ((MI_WIDTH(mi) - ss->loading_sw) / 2, (MI_HEIGHT(mi) - ss->loading_sh) / 2, 0); glColor3f (1, 1, 0); glEnable (GL_TEXTURE_2D); glDisable (GL_DEPTH_TEST); print_texture_string (ss->texfont, text); glEnable (GL_DEPTH_TEST); glPopMatrix(); glMatrixMode(GL_PROJECTION); glPopMatrix(); glMatrixMode(GL_MODELVIEW); glFinish(); glXSwapBuffers (MI_DISPLAY (mi), MI_WINDOW(mi)); } static Bool loading_initial_image (ModeInfo *mi) { photopile_state *ss = &sss[MI_SCREEN(mi)]; if (ss->frames[ss->nframe].loaded_p) { /* The initial image has been fully loaded, start fading it in. */ int i; for (i = 0; i < ss->nframe; ++i) { ss->frames[i].pos[3].x = MI_WIDTH(mi) * 0.5; ss->frames[i].pos[3].y = MI_HEIGHT(mi) * 0.5; ss->frames[i].pos[3].angle = 0.0; } set_new_positions(ss); ss->mode = SHUFFLE; ss->mode_tick = fade_ticks / speed; } else { loading_msg(mi); } return (ss->mode == EARLY); } ENTRYPOINT void reshape_photopile (ModeInfo *mi, int width, int height) { glViewport (0, 0, (GLint) width, (GLint) height); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glOrtho(0, MI_WIDTH(mi), 0, MI_HEIGHT(mi), -1, 1); # ifdef HAVE_MOBILE /* Keep it the same relative size when rotated. */ { GLfloat h = MI_HEIGHT(mi) / (GLfloat) MI_WIDTH(mi); int o = (int) current_device_rotation(); if (o != 0 && o != 180 && o != -180) glScalef (1/h, h, 1); } # endif glClear(GL_COLOR_BUFFER_BIT); } /* Kludge to add "-v" to invocation of "xscreensaver-getimage" in -debug mode */ static void hack_resources (Display *dpy) { # ifndef HAVE_JWXYZ char *res = "desktopGrabber"; char *val = get_string_resource (dpy, res, "DesktopGrabber"); char buf1[255]; char buf2[255]; XrmValue value; XrmDatabase db = XtDatabase (dpy); sprintf (buf1, "%.100s.%.100s", progname, res); sprintf (buf2, "%.200s -v", val); value.addr = buf2; value.size = strlen(buf2); XrmPutResource (&db, buf1, "String", &value); free (val); # endif /* !HAVE_JWXYZ */ } ENTRYPOINT void init_photopile (ModeInfo *mi) { int screen = MI_SCREEN(mi); photopile_state *ss; int wire = MI_IS_WIREFRAME(mi); MI_INIT (mi, sss); ss = &sss[screen]; ss->mi = mi; if ((ss->glx_context = init_GL(mi)) != NULL) { reshape_photopile (mi, MI_WIDTH(mi), MI_HEIGHT(mi)); clear_gl_error(); /* WTF? sometimes "invalid op" from glViewport! */ } else { MI_CLEARWINDOW(mi); } ss->shadow = init_drop_shadow(); ss->texfont = load_texture_font (MI_DISPLAY(mi), "font"); if (debug_p) hack_resources (MI_DISPLAY (mi)); ss->frames = (image *)calloc (MI_COUNT(mi) + 1, sizeof(image)); ss->nframe = 0; if (!wire) { int i; for (i = 0; i < MI_COUNT(mi) + 1; ++i) { glGenTextures (1, &(ss->frames[i].texid)); if (ss->frames[i].texid <= 0) abort(); } } ss->mode = EARLY; load_image(mi); /* start loading the first image */ } static void draw_image (ModeInfo *mi, int i, GLfloat t, GLfloat s, GLfloat z) { int wire = MI_IS_WIREFRAME(mi); photopile_state *ss = &sss[MI_SCREEN(mi)]; image *frame = ss->frames + i; position pos = interpolate(t, frame->pos); GLfloat w = frame->geom.width * 0.5; GLfloat h = frame->geom.height * 0.5; GLfloat z1 = z - 0.25 / (MI_COUNT(mi) + 1); GLfloat z2 = z - 0.5 / (MI_COUNT(mi) + 1); GLfloat w1 = w; GLfloat h1 = h; GLfloat h2 = h; if (polaroid_p) { GLfloat minSize = MIN(w, h); GLfloat maxSize = MAX(w, h); /* Clip or scale image to fit in the frame. */ if (clip_p) { w = h = minSize; } else { GLfloat scale = minSize / maxSize; w *= scale; h *= scale; } w1 = minSize * 1.16; /* enlarge frame border */ h1 = minSize * 1.5; h2 = w1; s /= 1.5; /* compensate for border size */ } glPushMatrix(); /* Position and scale this image. */ glTranslatef (pos.x, pos.y, 0); glRotatef (pos.angle, 0, 0, 1); glScalef (s, s, 1); /* Draw the drop shadow. */ if (shadows_p && !wire) { glColor3f (0, 0, 0); draw_drop_shadow(ss->shadow, -w1, -h1, z2, 2.0 * w1, h1 + h2, 20.0); glDisable (GL_BLEND); } glDisable (GL_LIGHTING); glEnable (GL_DEPTH_TEST); glDisable (GL_CULL_FACE); /* Draw the retro instant-film frame. */ if (polaroid_p) { if (! wire) { glShadeModel (GL_SMOOTH); glEnable (GL_LINE_SMOOTH); glHint (GL_LINE_SMOOTH_HINT, GL_NICEST); glColor3f (1, 1, 1); glBegin (GL_QUADS); glVertex3f (-w1, -h1, z2); glVertex3f ( w1, -h1, z2); glVertex3f ( w1, h2, z2); glVertex3f (-w1, h2, z2); glEnd(); } glLineWidth (1.0); glColor3f (0.5, 0.5, 0.5); glBegin (GL_LINE_LOOP); glVertex3f (-w1, -h1, z); glVertex3f ( w1, -h1, z); glVertex3f ( w1, h2, z); glVertex3f (-w1, h2, z); glEnd(); } /* Draw the image quad. */ if (! wire) { GLfloat texw = w / frame->tw; GLfloat texh = h / frame->th; GLfloat texx = (frame->geom.x + 0.5 * frame->geom.width) / frame->tw; GLfloat texy = (frame->geom.y + 0.5 * frame->geom.height) / frame->th; glBindTexture (GL_TEXTURE_2D, frame->texid); glEnable (GL_TEXTURE_2D); glColor3f (1, 1, 1); glBegin (GL_QUADS); glTexCoord2f (texx - texw, texy + texh); glVertex3f (-w, -h, z1); glTexCoord2f (texx + texw, texy + texh); glVertex3f ( w, -h, z1); glTexCoord2f (texx + texw, texy - texh); glVertex3f ( w, h, z1); glTexCoord2f (texx - texw, texy - texh); glVertex3f (-w, h, z1); glEnd(); glDisable (GL_TEXTURE_2D); } /* Draw a box around it. */ if (! wire) { glShadeModel (GL_SMOOTH); glEnable (GL_LINE_SMOOTH); glHint (GL_LINE_SMOOTH_HINT, GL_NICEST); } glLineWidth (1.0); glColor3f (0.5, 0.5, 0.5); glBegin (GL_LINE_LOOP); glVertex3f (-w, -h, z); glVertex3f ( w, -h, z); glVertex3f ( w, h, z); glVertex3f (-w, h, z); glEnd(); /* Draw a title under the image. */ if (titles_p) { int sw = 0, sh = 0; int ascent, descent; GLfloat tw = w * 2; GLfloat th = h1 - h; GLfloat scale = 1; const char *title = frame->title ? frame->title : "(untitled)"; XCharStruct e; texture_string_metrics (ss->texfont, title, &e, &ascent, &descent); sw = e.width; sh = ascent; /* + descent; */ /* Scale the text to match the pixel size of the photo */ scale *= w / 150.0; # if defined(HAVE_COCOA) scale /= 2; if (MI_WIDTH(mi) > 2560) scale /= 2; /* Retina displays */ # endif # if defined(HAVE_MOBILE) scale /= 2; # endif /* Clip characters off the left end of the string until it fits. */ if (clip_p || polaroid_p) while (sw * scale > tw && strlen (title) > 10) { title++; texture_string_metrics (ss->texfont, title, &e, &ascent, &descent); sw = e.width; } if (th <= 0) /* Non-polaroid */ th = -sh * 1.2; glTranslatef (-w, -h1, 0); glTranslatef ((tw - sw*scale) / 2, (th - sh*scale) / 2, 0); glScalef (scale, scale, 1); if (wire || !polaroid_p) { glColor3f (1, 1, 1); } else { glColor3f (0.5, 0.5, 0.5); } if (!wire) { glEnable (GL_TEXTURE_2D); glEnable (GL_BLEND); glDisable (GL_DEPTH_TEST); print_texture_string (ss->texfont, title); glEnable (GL_DEPTH_TEST); } else { glBegin (GL_LINE_LOOP); glVertex3f (0, 0, 0); glVertex3f (sw, 0, 0); glVertex3f (sw, sh, 0); glVertex3f (0, sh, 0); glEnd(); } } glPopMatrix(); } ENTRYPOINT void draw_photopile (ModeInfo *mi) { photopile_state *ss = &sss[MI_SCREEN(mi)]; int i; if (!ss->glx_context) return; glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *ss->glx_context); if (ss->mode == EARLY) if (loading_initial_image (mi)) return; /* Only check the wall clock every 10 frames */ if (ss->now == 0 || ss->draw_tick++ > 10) { ss->now = time((time_t *) 0); if (ss->last_time == 0) ss->last_time = ss->now; ss->draw_tick = 0; } glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); { GLfloat t; glPushMatrix(); glTranslatef (MI_WIDTH(mi)/2, MI_HEIGHT(mi)/2, 0); glRotatef(current_device_rotation(), 0, 0, 1); glTranslatef (-MI_WIDTH(mi)/2, -MI_HEIGHT(mi)/2, 0); /* Handle state transitions. */ switch (ss->mode) { case SHUFFLE: if (--ss->mode_tick <= 0) { ss->nframe = (ss->nframe+1) % (MI_COUNT(mi)+1); ss->mode = NORMAL; ss->last_time = time((time_t *) 0); } break; case NORMAL: if (ss->now - ss->last_time > duration) { ss->mode = LOADING; load_image(mi); } break; case LOADING: if (ss->frames[ss->nframe].loaded_p) { set_new_positions(ss); ss->mode = SHUFFLE; ss->mode_tick = fade_ticks / speed; } break; default: abort(); } t = 1.0 - ss->mode_tick / (fade_ticks / speed); t = 0.5 * (1.0 - cos(M_PI * t)); /* Draw the images. */ for (i = 0; i < MI_COUNT(mi) + (ss->mode == SHUFFLE); ++i) { int j = (ss->nframe + i + 1) % (MI_COUNT(mi) + 1); if (ss->frames[j].loaded_p) { GLfloat s = 1.0; GLfloat z = (GLfloat)i / (MI_COUNT(mi) + 1); switch (ss->mode) { case SHUFFLE: if (i == MI_COUNT(mi)) { s *= t; } else if (i == 0) { s *= 1.0 - t; } break; case NORMAL: case LOADING: t = 1.0; break; default: abort(); } draw_image(mi, j, t, s, z); } } glPopMatrix(); } if (mi->fps_p) do_fps (mi); glFinish(); glXSwapBuffers (MI_DISPLAY (mi), MI_WINDOW(mi)); } ENTRYPOINT void free_photopile (ModeInfo *mi) { photopile_state *ss = &sss[MI_SCREEN(mi)]; if (!ss->glx_context) return; glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *ss->glx_context); if (ss->frames) { int i; for (i = 0; i < MI_COUNT(mi); i++) { if (ss->frames[i].title) free (ss->frames[i].title); if (ss->frames[i].texid) glDeleteTextures(1, &ss->frames[i].texid); } } if (ss->shadow) glDeleteTextures(1, &ss->shadow); if (ss->texfont) free_texture_font (ss->texfont); } XSCREENSAVER_MODULE ("Photopile", photopile) #endif /* USE_GL */