/* beats, Copyright (c) 2020 David Eccles (gringer) * * 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. */ /* Beats changes the position of objects in time with a * synchronisation signal (or more correctly, based on the time * elapsed since the last synchronisation point). By default, the * system clock is used for this signal, with synchronisation * happening every minute. The location of objects is entirely * dependant on this synchronisation signal; there is no multi-object * state that needs to be stored, although there may be some styling * state required. */ #define DEFAULTS "*count: 30 \n" \ "*delay: 30000 \n" \ "*showFPS: False \n" \ "*wireframe: False \n" \ # define release_beats 0 #include "xlockmore.h" #include "colors.h" #include "sphere.h" #include "hsv.h" #include #include #ifdef USE_GL /* whole file */ #define DEF_CYCLE "-1" #define DEF_TICK "True" #define DEF_BLUR "True" #define SPHERE_SLICES 16 /* how densely to render spheres */ #define SPHERE_STACKS 16 typedef struct { GLXContext *glx_context; Bool button_down_p; GLuint beats_list; GLfloat pos; int ball_count; /* Number of balls */ int preset_cycle; /* Cycle to show (-1 for random) */ Bool use_tick; /* Add tick for clockwise / galaxy */ Bool use_blur; /* Motion blur */ int ncolors; XColor *colors; int ccolor; int color_shift; } beats_configuration; static beats_configuration *bps = NULL; static int cycle_arg; static Bool tick_arg; static Bool blur_arg; static XrmOptionDescRec opts[] = { { "-cycle", ".cycle", XrmoptionSepArg, 0 }, { "-count", ".count", XrmoptionSepArg, 0 }, { "-tick", ".tick", XrmoptionNoArg, "on" }, { "+tick", ".tick", XrmoptionNoArg, "off" }, { "-blur", ".blur", XrmoptionNoArg, "on" }, { "+blur", ".blur", XrmoptionNoArg, "off" } }; static argtype vars[] = { {&cycle_arg, "cycle", "Cycle", DEF_CYCLE, t_Int}, {&tick_arg, "tick", "Tick", DEF_TICK, t_Bool}, {&blur_arg, "blur", "Blur", DEF_BLUR, t_Bool} }; static OptionStruct desc[] = { {"-count num", "number of balls"}, {"-cycle num", "cycle / pattern type"}, {"-/+tick", "enable/disable tick for clockwise and galaxy"}, {"-/+blur", "enable/disable motion blur"} }; ENTRYPOINT ModeSpecOpt beats_opts = {countof(opts), opts, countof(vars), vars, desc}; /* Window management, etc */ ENTRYPOINT void reshape_beats (ModeInfo *mi, int width, int height) { GLfloat h = (GLfloat) height / (GLfloat) width; int y = 0; if (width > height * 5) { /* tiny window: show middle */ height = width * 9/16; y = -height/2; h = height / (GLfloat) width; } glViewport (0, y, (GLint) width, (GLint) height); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective (30.0, 1/h, 1.0, 100.0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); gluLookAt( 0.0, 0.0, 30.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); glClear(GL_COLOR_BUFFER_BIT); } ENTRYPOINT Bool beats_handle_event (ModeInfo *mi, XEvent *event) { return True; } static Bool getFracColour (GLfloat* retVal, float posFrac, float s){ /* top: red, right: yellow, bottom: [dark] green, left: blue */ /* note: fixed point; align to 0.1 degree increments */ int theta, h, v; unsigned short r,g,b; theta = ((int)(posFrac * 3600) % 3600 + 3600) % 3600; v = 100; if ((theta >= 0) && (theta < 900)) { h = (theta * 600) / 900; } else if ((theta >= 900) && (theta < 1800)) { h = ((theta - 900) * 600) / 900 + 600; v = 100 - ((theta - 900) / 18); } else if ((theta >= 1800) && (theta < 2700)) { h = ((theta - 1800) * 1200) / 900 + 1200; v = ((theta - 1800) / 18) + 50; } else /* if ((theta >= 2700) && (theta < 3600))*/ { h = ((theta - 2700) * 1200) / 900 + 2400; } hsv_to_rgb((int)h / 10.0, s, v / 100.0, &r, &g, &b); retVal[0] = r / 65535.0; retVal[1] = g / 65535.0; retVal[2] = b / 65535.0; return True; } ENTRYPOINT void init_beats (ModeInfo *mi) { beats_configuration *bp; int wire = MI_IS_WIREFRAME(mi); MI_INIT (mi, bps); bp = &bps[MI_SCREEN(mi)]; bp->glx_context = init_GL(mi); reshape_beats (mi, MI_WIDTH(mi), MI_HEIGHT(mi)); if (!wire) { GLfloat pos[4] = {1.0, 1.0, 1.0, 0.0}; GLfloat amb[4] = {0.02, 0.02, 0.02, 1.0}; GLfloat dif[4] = {1.0, 1.0, 1.0, 1.0}; GLfloat spc[4] = {0.2, 0.2, 0.2, 0.2}; glEnable(GL_LIGHTING); glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); glLightfv(GL_LIGHT0, GL_POSITION, pos); glLightfv(GL_LIGHT0, GL_AMBIENT, amb); glLightfv(GL_LIGHT0, GL_DIFFUSE, dif); glLightfv(GL_LIGHT0, GL_SPECULAR, spc); glEnable(GL_LIGHT0); } if (cycle_arg > 3) cycle_arg = -1; bp->ball_count = MI_COUNT(mi); if (bp->ball_count < 2) bp->ball_count = 2; bp->preset_cycle = cycle_arg; bp->use_tick = tick_arg; bp->use_blur = blur_arg; # ifdef HAVE_ANDROID bp->use_blur = False; /* Works on iOS but not Android */ # endif bp->ncolors = 128; bp->colors = (XColor *) calloc(bp->ncolors, sizeof(XColor)); make_smooth_colormap (0, 0, 0, bp->colors, &bp->ncolors, False, 0, False); bp->beats_list = glGenLists(1); glNewList (bp->beats_list, GL_COMPILE); glScalef(0.71, 0.71, 0.71); unit_sphere (SPHERE_STACKS, SPHERE_SLICES, wire); glEndList (); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } ENTRYPOINT void draw_beats (ModeInfo *mi) { beats_configuration *bp = &bps[MI_SCREEN(mi)]; Display *dpy = MI_DISPLAY(mi); Window window = MI_WINDOW(mi); unsigned num_objects = bp->ball_count, oi; struct timeval tv, tvOrig; struct tm *now; Bool sineWaveTick = bp->use_tick; Bool motionBlur = bp->use_blur; size_t cycle, dist; unsigned int tmS, tmM, tmH, tmD; unsigned int timeSeed; int timeDelta = 0; size_t blurOffset = 10; /* offset per blur frame, in milliseconds */ size_t framesPerBlur = 20; /* number of sub-frames to blur */ size_t deltaLimit = (motionBlur) ? (blurOffset * framesPerBlur) : 1; float ballAlpha; float secFrac, minFrac, minProp, hourProp, halfDayProp, z, op, mp, m2m, theta, delta, blurFrac, oFP, pathLength; static const GLfloat bspec[4] = {1.0, 1.0, 1.0, 1.0}; static const GLfloat bshiny = 92.0; GLfloat bcolor[4] = {0.85, 0.75, 0.75, 1.0}; if (!bp->glx_context) return; gettimeofday (&tvOrig, NULL); glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *bp->glx_context); glShadeModel(GL_SMOOTH); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glEnable(GL_DEPTH_TEST); glEnable(GL_ALPHA_TEST); glEnable(GL_NORMALIZE); glEnable(GL_CULL_FACE); glEnable(GL_BLEND); glEnable(GL_DEPTH_TEST); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glPushMatrix (); glRotatef(current_device_rotation(), 0, 0, 1); { GLfloat s = (MI_WIDTH(mi) < MI_HEIGHT(mi) ? MI_WIDTH(mi) / (GLfloat) MI_HEIGHT(mi) : 1); glScalef (s, s, s); } /* timeDelta is in milliseconds */ for(timeDelta = 0; timeDelta <= deltaLimit; timeDelta += blurOffset){ if(timeDelta < blurOffset){ /* glEnable(GL_DEPTH_TEST); */ ballAlpha = 1.0; } else { /* glDisable(GL_DEPTH_TEST); */ ballAlpha = 1.0 / framesPerBlur; } blurFrac = sin((1 - (float) timeDelta / deltaLimit) * M_PI_2) * ballAlpha; tv = tvOrig; now = localtime (&tv.tv_sec); /* This seems to be needed for seconds */ tmS = now->tm_sec; tmM = now->tm_min; tmH = now->tm_hour; tmD = now->tm_yday; secFrac = ((tv.tv_usec % 1000000) - (timeDelta * 1000)) / (1e6); if(secFrac < 0){ secFrac += 1; tmS--; if(tmS < 0){ tmS += 60; tmM--; } if(tmM < 0){ tmM += 60; tmH--; } if(tmH < 0){ tmH += 24; tmD--; } if(tmD < 0){ /* note: this won't be accurate for leap years, but the rare event logic is complex enough */ tmD += 365; } } /* pseudo-random generator based on current minute */ timeSeed = (((tmM+1) * (tmM+1) * ((tmH+1) * 37) * ((tmD+1) * 1151) * 1233599) % 653); cycle = timeSeed % 4; if(bp->preset_cycle != -1){ cycle = bp->preset_cycle; } if(sineWaveTick && (cycle == 0 || cycle == 3)){ Bool doTick = (timeSeed % 2 == 0); if(doTick){ /* choose to tick randomly */ /* sine-wave 'tick' motion, converts linear 0..1 to pause/fast/pause 0..1 */ secFrac = (1.0 - sin((0.5-secFrac) * M_PI))/2.0; } } minFrac = tmS / 60.0; /* now we have enough information to calculate our goal statistic, minProp: the position in the synchronisation cycle of one minute */ minProp = (minFrac - trunc(minFrac)) + (secFrac / 60); m2m = minProp * 2 * M_PI; /* change colour based on the minute and hour */ hourProp = tmM / 60.0 + minProp / 60.0; hourProp = hourProp - trunc(hourProp); halfDayProp = tmH / 12.0 + hourProp / 12.0; halfDayProp = halfDayProp - trunc(halfDayProp); mi->polygon_count = 0; for(oi = 0; oi < num_objects; oi++){ glPushMatrix (); glScalef(1.1, 1.1, 1.1); /* Object Fraction Position - 0..1 depending on native Z order */ oFP = oi * 1.0 / (num_objects - 1); /* set Z distance between [-3.5 .. 0.5] (common to all cycles) */ z = (oFP) * 4.0 - 3.5; /* set colour (common to all cycles) */ if(oFP < (1 / 3.0)){ /* "second" objects */ getFracColour(bcolor, minProp, 1.0); } else if(oFP < (2 / 3.0)) { /* "minute" objects */ getFracColour(bcolor, hourProp, 1.0); } else { /* "hour" objects */ getFracColour(bcolor, halfDayProp, 1.0); } /* set x/y location */ if(cycle == 0){ /* clockwise */ glRotatef(-minProp * 360 * (oi + 1), 0, 0, 1); glTranslatef(0, 5, 0); } else if(cycle == 1){ /* rain dance */ float y = 10 * cos(m2m * (oi + 1.0))/2; /* rotate around Y axis */ glTranslatef(0, 0, -20); glRotatef(minProp * 360, 0, 1, 0); glTranslatef(0, y, 20); } else if(cycle == 2){ /* metronome */ theta = sin(-m2m * (oi + 1.0)) * 90; /* rotate around z axis at (-5, 0, 0) */ glTranslatef(0, -5, 0); glRotatef(theta, 0, 0, 1); glTranslatef(0, 10, 0); } else if (cycle == 3){ /* galaxy */ mp = (num_objects - 1.0) / 2; op = mp - oi; dist = (int)(fabs(op)+0.5); /* dist from centre */ /* make sure each object travels an integer number of loops in a path through one cycle */ pathLength = (int)((60.0 / dist) + 0.5) * 720.0; delta = pathLength / 2; theta = -minProp * delta - 180; /* rotate around X axis after translating (0,-5,0) */ glTranslatef(0, 0, -20); glRotatef(minProp * 360 - 180, 1, 0, 0); glTranslatef(0, 0, 20); glTranslatef(0, -5, 0); /* rotate around Y axis */ glTranslatef(0, 0, -20); glRotatef(theta, 0, 1, 0); glTranslatef(0, 0, 20); } /* spread out based on Z position */ glTranslatef(0, 0, (z - 0.5) * 10); /* set up colours */ glMaterialfv (GL_FRONT, GL_SPECULAR, bspec); glMateriali (GL_FRONT, GL_SHININESS, bshiny); if(motionBlur){ bcolor[3] = (timeDelta == 0) ? 1.0 : blurFrac; /* was ballAlpha */ } glMaterialfv (GL_FRONT, GL_AMBIENT_AND_DIFFUSE, bcolor); glCallList (bp->beats_list); /* draw sphere */ mi->polygon_count += (SPHERE_SLICES * SPHERE_STACKS); glPopMatrix(); } } glPopMatrix(); if (mi->fps_p) do_fps (mi); glFinish(); glXSwapBuffers(dpy, window); } ENTRYPOINT void free_beats (ModeInfo *mi) { beats_configuration *bp = &bps[MI_SCREEN(mi)]; if (!bp->glx_context) return; glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *bp->glx_context); if (bp->colors) free (bp->colors); if (glIsList(bp->beats_list)) glDeleteLists(bp->beats_list, 1); } XSCREENSAVER_MODULE ("Beats", beats) #endif /* USE_GL */