* Ported to X11 and xscreensaver by jwz, 27-Feb-2002.
*
* http://astronomy.swin.edu.au/~pbourke/modelling/fluid/
+ *
+ * Some physics improvements by Steven Barker <steve@blckknght.org>
*/
-/* cjb notes
- *
- * Future ideas:
+/* Future ideas:
* Specifying a distribution in the ball sizes (with a gamma curve, possibly).
* Brownian motion, for that extra touch of realism.
+ *
+ * It would be nice to detect when there are more balls than fit in
+ * the window, and scale the number of balls back. Useful for the
+ * xscreensaver-demo preview, which is often too tight by default.
*/
#include <math.h>
Display *dpy;
Window window;
XWindowAttributes xgwa;
+ int delay;
Pixmap b, ba; /* double-buffer to reduce flicker */
#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
XdbeBackBuffer backb;
+ Bool dbeclear_p;
#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
GC draw_gc; /* most of the balls */
float *r; /* ball radiuses */
float *m; /* ball mass, precalculated */
- float e; /* coefficient of friction, I think? */
+ float e; /* coeficient of elasticity */
float max_radius; /* largest radius of any ball */
Bool random_sizes_p; /* Whether balls should be various sizes up to max. */
Bool shake_p; /* Whether to mess with gravity when things settle. */
Bool dbuf; /* Whether we're using double buffering. */
- Bool dbeclear_p; /* ? */
float shake_threshold;
int time_since_shake;
int collision_count;
char fps_str[1024];
+ int time_tick;
+ struct timeval last_time;
+
} b_state;
draw_fps_string (b_state *state)
{
XFillRectangle (state->dpy, state->b, state->erase_gc,
- 0, state->xgwa.height - state->font_height,
- state->xgwa.width, state->font_height);
+ 0, state->xgwa.height - state->font_height*3 - 20,
+ state->xgwa.width, state->font_height*3 + 20);
XDrawImageString (state->dpy, state->b, state->font_gc,
- 0, state->xgwa.height - state->font_baseline,
+ 10, state->xgwa.height - state->font_height*2 -
+ state->font_baseline - 10,
state->fps_str, strlen(state->fps_str));
}
static void
window_origin (Display *dpy, Window window, int *x, int *y)
{
- Window root, parent, *kids;
- unsigned int nkids;
- XWindowAttributes xgwa;
- int wx, wy;
- XGetWindowAttributes (dpy, window, &xgwa);
-
- wx = xgwa.x;
- wy = xgwa.y;
-
- kids = 0;
- *x = 0;
- *y = 0;
-
- if (XQueryTree (dpy, window, &root, &parent, &kids, &nkids))
- {
- if (parent && parent != root)
- {
- int px, py;
- window_origin (dpy, parent, &px, &py);
- wx += px;
- wy += py;
- }
- }
- if (kids) XFree (kids);
- *x = wx;
- *y = wy;
+ XTranslateCoordinates (dpy, window, RootWindow (dpy, DefaultScreen (dpy)),
+ 0, 0, x, y, &window);
}
state->xmin = wx;
state->ymin = wy;
state->xmax = state->xmin + state->xgwa.width;
- state->ymax = state->ymin + state->xgwa.height - state->font_height;
+ state->ymax = state->ymin + state->xgwa.height - (state->font_height*3) -
+ (state->font_height ? 22 : 0);
if (state->dbuf && (state->ba))
{
XClearWindow (state->dpy, state->window);
else if (state->fps_p && oymax != state->ymax)
XFillRectangle (state->dpy, state->b, state->erase_gc,
- 0, state->xgwa.height - state->font_height,
- state->xgwa.width, state->font_height);
+ 0, state->xgwa.height - state->font_height*3,
+ state->xgwa.width, state->font_height*3);
}
}
/* Initialize the state structure and various X data.
*/
-static b_state *
-init_balls (Display *dpy, Window window)
+static void *
+fluidballs_init (Display *dpy, Window window)
{
int i;
float extx, exty;
XGCValues gcv;
state->dpy = dpy;
-
state->window = window;
+ state->delay = get_integer_resource (dpy, "delay", "Integer");
check_window_moved (state);
- state->dbuf = get_boolean_resource ("doubleBuffer", "Boolean");
- state->dbeclear_p = get_boolean_resource ("useDBEClear", "Boolean");
+ state->dbuf = get_boolean_resource (dpy, "doubleBuffer", "Boolean");
+
+# ifdef HAVE_COCOA /* Don't second-guess Quartz's double-buffering */
+ state->dbuf = False;
+# endif
if (state->dbuf)
{
#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
+ state->dbeclear_p = get_boolean_resource (dpy, "useDBEClear", "Boolean");
if (state->dbeclear_p)
state->b = xdbe_get_backbuffer (dpy, window, XdbeBackground);
else
/* Select ButtonRelease events on the external window, if no other app has
already selected it (only one app can select it at a time: BadAccess. */
+#if 0
if (! (state->xgwa.all_event_masks & ButtonReleaseMask))
XSelectInput (state->dpy, state->window,
state->xgwa.your_event_mask | ButtonReleaseMask);
+#endif
- gcv.foreground = get_pixel_resource("foreground", "Foreground",
- state->dpy, state->xgwa.colormap);
- gcv.background = get_pixel_resource("background", "Background",
- state->dpy, state->xgwa.colormap);
+ gcv.foreground = get_pixel_resource(state->dpy, state->xgwa.colormap,
+ "foreground", "Foreground");
+ gcv.background = get_pixel_resource(state->dpy, state->xgwa.colormap,
+ "background", "Background");
state->draw_gc = XCreateGC (state->dpy, state->b,
GCForeground|GCBackground, &gcv);
- gcv.foreground = get_pixel_resource("mouseForeground", "MouseForeground",
- state->dpy, state->xgwa.colormap);
+ gcv.foreground = get_pixel_resource(state->dpy, state->xgwa.colormap,
+ "mouseForeground", "MouseForeground");
state->draw_gc2 = XCreateGC (state->dpy, state->b,
GCForeground|GCBackground, &gcv);
extx = state->xmax - state->xmin;
exty = state->ymax - state->ymin;
- state->count = get_integer_resource ("count", "Count");
+ state->count = get_integer_resource (dpy, "count", "Count");
if (state->count < 1) state->count = 20;
- state->max_radius = get_float_resource ("size", "Size") / 2;
+ state->max_radius = get_float_resource (dpy, "size", "Size") / 2;
if (state->max_radius < 1.0) state->max_radius = 1.0;
- state->random_sizes_p = get_boolean_resource ("random", "Random");
+ state->random_sizes_p = get_boolean_resource (dpy, "random", "Random");
- state->accx = get_float_resource ("wind", "Wind");
+ /* If the initial window size is too small to hold all these balls,
+ make fewer of them...
+ */
+ {
+ float r = (state->random_sizes_p
+ ? state->max_radius * 0.7
+ : state->max_radius);
+ float ball_area = M_PI * r * r;
+ float balls_area = state->count * ball_area;
+ float window_area = state->xgwa.width * state->xgwa.height;
+ window_area *= 0.75; /* don't pack it completely full */
+ if (balls_area > window_area)
+ state->count = window_area / ball_area;
+ }
+
+ state->accx = get_float_resource (dpy, "wind", "Wind");
if (state->accx < -1.0 || state->accx > 1.0) state->accx = 0;
- state->accy = get_float_resource ("gravity", "Gravity");
+ state->accy = get_float_resource (dpy, "gravity", "Gravity");
if (state->accy < -1.0 || state->accy > 1.0) state->accy = 0.01;
- state->e = get_float_resource ("friction", "Friction");
+ state->e = get_float_resource (dpy, "elasticity", "Elacitcity");
if (state->e < 0.2 || state->e > 1.0) state->e = 0.97;
- state->tc = get_float_resource ("timeScale", "TimeScale");
+ state->tc = get_float_resource (dpy, "timeScale", "TimeScale");
if (state->tc <= 0 || state->tc > 10) state->tc = 1.0;
- state->shake_p = get_boolean_resource ("shake", "Shake");
- state->shake_threshold = get_float_resource ("shakeThreshold",
+ state->shake_p = get_boolean_resource (dpy, "shake", "Shake");
+ state->shake_threshold = get_float_resource (dpy, "shakeThreshold",
"ShakeThreshold");
+ state->time_tick = 999999;
- state->fps_p = get_boolean_resource ("doFPS", "DoFPS");
+# ifdef USE_IPHONE /* Always obey real-world gravity */
+ state->shake_p = False;
+# endif
+
+
+ state->fps_p = get_boolean_resource (dpy, "doFPS", "DoFPS");
if (state->fps_p)
{
XFontStruct *font;
- char *fontname = get_string_resource ("font", "Font");
- const char *def_font = "fixed";
- if (!fontname || !*fontname) fontname = (char *)def_font;
+ char *fontname = get_string_resource (dpy, "fpsFont", "Font");
+ if (!fontname) fontname = "-*-courier-bold-r-normal-*-180-*";
font = XLoadQueryFont (dpy, fontname);
- if (!font) font = XLoadQueryFont (dpy, def_font);
+ if (!font) font = XLoadQueryFont (dpy, "fixed");
if (!font) exit(-1);
gcv.font = font->fid;
- gcv.foreground = get_pixel_resource("textColor", "Foreground",
- state->dpy, state->xgwa.colormap);
+ gcv.foreground = get_pixel_resource(state->dpy, state->xgwa.colormap,
+ "textColor", "Foreground");
state->font_gc = XCreateGC(dpy, state->b,
GCFont|GCForeground|GCBackground, &gcv);
state->font_height = font->ascent + font->descent;
state->m[i] = pow(state->r[i],3) * M_PI * 1.3333;
}
+ memcpy (state->opx, state->px, sizeof (*state->opx) * (state->count + 1));
+ memcpy (state->opy, state->py, sizeof (*state->opx) * (state->count + 1));
+
return state;
}
static void
check_wall_clock (b_state *state, float max_d)
{
- static int tick = 0;
state->frame_count++;
- if (tick++ > 20) /* don't call gettimeofday() too often -- it's slow. */
+ if (state->time_tick++ > 20) /* don't call gettimeofday() too often -- it's slow. */
{
struct timeval now;
- static struct timeval last = {0, };
# ifdef GETTIMEOFDAY_TWO_ARGS
struct timezone tzp;
gettimeofday(&now, &tzp);
gettimeofday(&now);
# endif
- if (last.tv_sec == 0)
- last = now;
+ if (state->last_time.tv_sec == 0)
+ state->last_time = now;
- tick = 0;
- if (now.tv_sec == last.tv_sec)
+ state->time_tick = 0;
+ if (now.tv_sec == state->last_time.tv_sec)
return;
- state->time_since_shake += (now.tv_sec - last.tv_sec);
+ state->time_since_shake += (now.tv_sec - state->last_time.tv_sec);
+
+# ifdef USE_IPHONE /* Always obey real-world gravity */
+ {
+ float a = fabs (fabs(state->accx) > fabs(state->accy)
+ ? state->accx : state->accy);
+ switch ((int) current_device_rotation ()) {
+ case 0: case 360: state->accx = 0; state->accy = a; break;
+ case -90: state->accx = -a; state->accy = 0; break;
+ case 90: state->accx = a; state->accy = 0; break;
+ case 180: case -180: state->accx = 0; state->accy = -a; break;
+ default: break;
+ }
+ }
+# endif /* USE_IPHONE */
if (state->fps_p)
{
float elapsed = ((now.tv_sec + (now.tv_usec / 1000000.0)) -
- (last.tv_sec + (last.tv_usec / 1000000.0)));
+ (state->last_time.tv_sec + (state->last_time.tv_usec / 1000000.0)));
float fps = state->frame_count / elapsed;
float cps = state->collision_count / elapsed;
- sprintf (state->fps_str,
- " FPS: %.2f Collisions: %.3f/frame Max motion: %.3f",
- fps, cps/fps, max_d);
+ sprintf (state->fps_str, "Collisions: %.3f/frame Max motion: %.3f",
+ cps/fps, max_d);
draw_fps_string(state);
}
state->frame_count = 0;
state->collision_count = 0;
- last = now;
+ state->last_time = now;
}
}
int x1b, x2b, y1b, y2b;
float max_d = 0;
+#ifdef HAVE_COCOA /* Don't second-guess Quartz's double-buffering */
+ XClearWindow (state->dpy, state->b);
+#endif
+
for (a=1; a <= state->count; a++)
{
GC gc;
x2b = (state->px[a] + state->r[a] - state->xmin);
y2b = (state->py[a] + state->r[a] - state->ymin);
- if (!state->dbeclear_p ||
+#ifndef HAVE_COCOA /* Don't second-guess Quartz's double-buffering */
#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
- !state->backb
+ if (!state->dbeclear_p || !state->backb)
#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
- )
{
/* if (x1a != x1b || y1a != y1b) -- leaves turds if we optimize this */
{
0, 360*64);
}
}
+#endif /* !HAVE_COCOA */
+
if (state->mouse_ball == a)
gc = state->draw_gc2;
else
state->opy[a] = state->py[a];
}
- if (state->fps_p && state->dbeclear_p)
+ if (state->fps_p
+#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
+ && (state->backb ? state->dbeclear_p : 1)
+#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
+ )
draw_fps_string(state);
#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
{
int a, b;
float d, vxa, vya, vxb, vyb, dd, cdx, cdy;
- float ma, mb, vela, velb, vela1, velb1;
+ float ma, mb, vca, vcb, dva, dvb;
float dee2;
check_window_moved (state);
}
/* For each ball, compute the influence of every other ball. */
- for (a=1; a <= state->count; a++)
- if (a != state->mouse_ball)
- for (b=1; b <= state->count; b++)
- if (a != b)
- {
- d = ((state->px[a] - state->px[b]) *
- (state->px[a] - state->px[b]) +
- (state->py[a] - state->py[b]) *
- (state->py[a] - state->py[b]));
- dee2 = (state->r[a] + state->r[b]) *
- (state->r[a] + state->r[b]);
- if (d < dee2)
- {
- state->collision_count++;
- d = sqrt(d);
- dd = state->r[a] + state->r[b] - d;
- /* A pair of balls that have already collided in this
- * current frame (and therefore touching each other)
- * should not have another collision calculated, hence
- * the fallthru if "dd ~= 0.0".
- */
- if ((dd < -0.01) || (dd > 0.01))
- {
- cdx = (state->px[b] - state->px[a]) / d;
- cdy = (state->py[b] - state->py[a]) / d;
-
- /* Move each ball apart from the other by half the
- * 'collision' distance.
- */
- state->px[a] -= 0.5 * dd * cdx;
- state->py[a] -= 0.5 * dd * cdy;
- state->px[b] += 0.5 * dd * cdx;
- state->py[b] += 0.5 * dd * cdy;
-
- ma = state->m[a];
- mb = state->m[b];
- vxa = state->vx[a];
- vya = state->vy[a];
- vxb = state->vx[b];
- vyb = state->vy[b];
-
- vela = sqrt((vxa * vxa) + (vya * vya));
- velb = sqrt((vxb * vxb) + (vyb * vyb));
-
- vela1 = vela * ((ma - mb) / (ma + mb)) +
- velb * ((2 * mb) / (ma + mb));
- velb1 = vela * ((2 * ma) / (ma + mb)) +
- velb * ((mb - ma) / (ma + mb));
-
- vela1 *= state->e; /* "air resistance" */
- velb1 *= state->e;
+ for (a=1; a <= state->count - 1; a++)
+ for (b=a + 1; b <= state->count; b++)
+ {
+ d = ((state->px[a] - state->px[b]) *
+ (state->px[a] - state->px[b]) +
+ (state->py[a] - state->py[b]) *
+ (state->py[a] - state->py[b]));
+ dee2 = (state->r[a] + state->r[b]) *
+ (state->r[a] + state->r[b]);
+ if (d < dee2)
+ {
+ state->collision_count++;
+ d = sqrt(d);
+ dd = state->r[a] + state->r[b] - d;
+
+ cdx = (state->px[b] - state->px[a]) / d;
+ cdy = (state->py[b] - state->py[a]) / d;
+
+ /* Move each ball apart from the other by half the
+ * 'collision' distance.
+ */
+ state->px[a] -= 0.5 * dd * cdx;
+ state->py[a] -= 0.5 * dd * cdy;
+ state->px[b] += 0.5 * dd * cdx;
+ state->py[b] += 0.5 * dd * cdy;
+
+ ma = state->m[a];
+ mb = state->m[b];
+
+ vxa = state->vx[a];
+ vya = state->vy[a];
+ vxb = state->vx[b];
+ vyb = state->vy[b];
+
+ vca = vxa * cdx + vya * cdy; /* the component of each velocity */
+ vcb = vxb * cdx + vyb * cdy; /* along the axis of the collision */
+
+ /* elastic collison */
+ dva = (vca * (ma - mb) + vcb * 2 * mb) / (ma + mb) - vca;
+ dvb = (vcb * (mb - ma) + vca * 2 * ma) / (ma + mb) - vcb;
+
+ dva *= state->e; /* some energy lost to inelasticity */
+ dvb *= state->e;
+
#if 0
- vela1 += (frand (50) - 25) / ma; /* brownian motion */
- velb1 += (frand (50) - 25) / mb;
+ dva += (frand (50) - 25) / ma; /* q: why are elves so chaotic? */
+ dvb += (frand (50) - 25) / mb; /* a: brownian motion. */
#endif
- state->vx[a] = -cdx * vela1;
- state->vy[a] = -cdy * vela1;
- state->vx[b] = cdx * velb1;
- state->vy[b] = cdy * velb1;
- }
- }
- }
- /* Force all balls to be on screen.
- */
+ vxa += dva * cdx;
+ vya += dva * cdy;
+ vxb += dvb * cdx;
+ vyb += dvb * cdy;
+
+ state->vx[a] = vxa;
+ state->vy[a] = vya;
+ state->vx[b] = vxb;
+ state->vy[b] = vyb;
+ }
+ }
+
+ /* Force all balls to be on screen.
+ */
for (a=1; a <= state->count; a++)
{
if (state->px[a] <= (state->xmin + state->r[a]))
/* Handle X events, specifically, allow a ball to be picked up with the mouse.
*/
-static void
-handle_events (b_state *state)
+static Bool
+fluidballs_event (Display *dpy, Window window, void *closure, XEvent *event)
{
- XSync (state->dpy, False);
- while (XPending (state->dpy))
+ b_state *state = (b_state *) closure;
+
+ if (event->xany.type == ButtonPress)
{
- XEvent event;
- XNextEvent (state->dpy, &event);
- if (event.xany.type == ButtonPress)
+ int i, rx, ry;
+ XTranslateCoordinates (dpy, window, RootWindow (dpy, DefaultScreen(dpy)),
+ event->xbutton.x, event->xbutton.y, &rx, &ry,
+ &window);
+
+ if (state->mouse_ball != 0) /* second down-click? drop the ball. */
+ {
+ state->mouse_ball = 0;
+ return True;
+ }
+ else
{
- int i;
- float fmx = event.xbutton.x_root;
- float fmy = event.xbutton.y_root;
- if (state->mouse_ball != 0) /* second down-click? drop the ball. */
- {
- state->mouse_ball = 0;
- return;
- }
- else
- for (i=1; i <= state->count; i++)
+ /* When trying to pick up a ball, first look for a click directly
+ inside the ball; but if we don't find it, expand the radius
+ outward until we find something nearby.
+ */
+ float max = state->max_radius * 4;
+ float step = max / 10;
+ float r2;
+ for (r2 = step; r2 < max; r2 += step) {
+ for (i = 1; i <= state->count; i++)
{
- float d = ((state->px[i] - fmx) * (state->px[i] - fmx) +
- (state->py[i] - fmy) * (state->py[i] - fmy));
+ float d = ((state->px[i] - rx) * (state->px[i] - rx) +
+ (state->py[i] - ry) * (state->py[i] - ry));
float r = state->r[i];
+ if (r2 > r) r = r2;
if (d < r*r)
{
state->mouse_ball = i;
- return;
+ return True;
}
}
+ }
}
- else if (event.xany.type == ButtonRelease) /* drop the ball */
- {
- state->mouse_ball = 0;
- return;
- }
- else if (event.xany.type == ConfigureNotify)
- {
- /* This is redundant, since we poll this every iteration. */
- check_window_moved (state);
- }
-
- screenhack_handle_event (state->dpy, &event);
+ return True;
}
+ else if (event->xany.type == ButtonRelease) /* drop the ball */
+ {
+ state->mouse_ball = 0;
+ return True;
+ }
+
+ return False;
+}
+
+static unsigned long
+fluidballs_draw (Display *dpy, Window window, void *closure)
+{
+ b_state *state = (b_state *) closure;
+ repaint_balls(state);
+ update_balls(state);
+ return state->delay;
}
-\f
-char *progclass = "FluidBalls";
+static void
+fluidballs_reshape (Display *dpy, Window window, void *closure,
+ unsigned int w, unsigned int h)
+{
+}
-char *defaults [] = {
+static void
+fluidballs_free (Display *dpy, Window window, void *closure)
+{
+ b_state *state = (b_state *) closure;
+ free (state);
+}
+
+
+static const char *fluidballs_defaults [] = {
".background: black",
- ".textColor: yellow",
- ".font: -*-helvetica-*-r-*-*-*-180-*-*-p-*-*-*",
+ ".foreground: yellow",
+ ".textColor: white",
+ "*mouseForeground: white",
"*delay: 10000",
"*count: 300",
"*size: 25",
"*random: True",
"*gravity: 0.01",
"*wind: 0.00",
- "*friction: 0.8",
+ "*elasticity: 0.97",
"*timeScale: 1.0",
- "*doFPS: False",
"*shake: True",
"*shakeThreshold: 0.015",
"*doubleBuffer: True",
"*useDBE: True",
"*useDBEClear: True",
#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
+#ifdef USE_IPHONE
+ "*ignoreRotation: True",
+#endif
0
};
-XrmOptionDescRec options [] = {
+static XrmOptionDescRec fluidballs_options [] = {
{ "-delay", ".delay", XrmoptionSepArg, 0 },
{ "-count", ".count", XrmoptionSepArg, 0 },
{ "-size", ".size", XrmoptionSepArg, 0 },
{ "-count", ".count", XrmoptionSepArg, 0 },
{ "-gravity", ".gravity", XrmoptionSepArg, 0 },
{ "-wind", ".wind", XrmoptionSepArg, 0 },
- { "-friction", ".friction", XrmoptionSepArg, 0 },
- { "-fps", ".doFPS", XrmoptionNoArg, "True" },
- { "-no-fps", ".doFPS", XrmoptionNoArg, "False" },
+ { "-elasticity", ".elasticity", XrmoptionSepArg, 0 },
{ "-shake", ".shake", XrmoptionNoArg, "True" },
{ "-no-shake", ".shake", XrmoptionNoArg, "False" },
{ "-random", ".random", XrmoptionNoArg, "True" },
+ { "-no-random", ".random", XrmoptionNoArg, "False" },
{ "-nonrandom", ".random", XrmoptionNoArg, "False" },
{ "-db", ".doubleBuffer", XrmoptionNoArg, "True" },
{ "-no-db", ".doubleBuffer", XrmoptionNoArg, "False" },
{ 0, 0, 0, 0 }
};
-void
-screenhack (Display *dpy, Window window)
-{
- b_state *state = init_balls(dpy, window);
- int delay = get_integer_resource ("delay", "Integer");
- while (1)
- {
- repaint_balls(state);
- update_balls(state);
-
- XSync (dpy, False);
- handle_events (state);
- if (delay) usleep (delay);
- }
-}
+XSCREENSAVER_MODULE ("FluidBalls", fluidballs)