1 /* fluidballs, Copyright (c) 2000 by Peter Birtles <peter@bqdesign.com.au>
3 * Permission to use, copy, modify, distribute, and sell this software and its
4 * documentation for any purpose is hereby granted without fee, provided that
5 * the above copyright notice appear in all copies and that both that
6 * copyright notice and this permission notice appear in supporting
7 * documentation. No representations are made about the suitability of this
8 * software for any purpose. It is provided "as is" without express or
11 * Ported to X11 and xscreensaver by jwz, 27-Feb-2002.
13 * http://astronomy.swin.edu.au/~pbourke/modelling/fluid/
15 * Some physics improvements by Steven Barker <steve@blckknght.org>
19 * Specifying a distribution in the ball sizes (with a gamma curve, possibly).
20 * Brownian motion, for that extra touch of realism.
22 * It would be nice to detect when there are more balls than fit in
23 * the window, and scale the number of balls back. Useful for the
24 * xscreensaver-demo preview, which is often too tight by default.
28 #include "screenhack.h"
31 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
33 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
38 XWindowAttributes xgwa;
40 Pixmap b, ba; /* double-buffer to reduce flicker */
41 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
43 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
45 GC draw_gc; /* most of the balls */
46 GC draw_gc2; /* the ball being dragged with the mouse */
51 int count; /* number of balls */
52 float xmin, ymin; /* rectangle of window, relative to root */
55 int mouse_ball; /* index of ball being dragged, or 0 if none. */
57 float tc; /* time constant (time-warp multiplier) */
58 float accx; /* horizontal acceleration (wind) */
59 float accy; /* vertical acceleration (gravity) */
61 float *vx, *vy; /* current ball velocities */
62 float *px, *py; /* current ball positions */
63 float *opx, *opy; /* previous ball positions */
64 float *r; /* ball radiuses */
66 float *m; /* ball mass, precalculated */
67 float e; /* coeficient of elasticity */
68 float max_radius; /* largest radius of any ball */
70 Bool random_sizes_p; /* Whether balls should be various sizes up to max. */
71 Bool shake_p; /* Whether to mess with gravity when things settle. */
72 Bool dbuf; /* Whether we're using double buffering. */
73 Bool dbeclear_p; /* ? */
74 float shake_threshold;
77 Bool fps_p; /* Whether to draw some text at the bottom. */
88 /* Draws the frames per second string */
90 draw_fps_string (b_state *state)
92 XFillRectangle (state->dpy, state->b, state->erase_gc,
93 0, state->xgwa.height - state->font_height,
94 state->xgwa.width, state->font_height);
95 XDrawImageString (state->dpy, state->b, state->font_gc,
96 0, state->xgwa.height - state->font_baseline,
97 state->fps_str, strlen(state->fps_str));
100 /* Finds the origin of the window relative to the root window, by
101 walking up the window tree until it reaches the top.
104 window_origin (Display *dpy, Window window, int *x, int *y)
106 Window root, parent, *kids;
108 XWindowAttributes xgwa;
110 XGetWindowAttributes (dpy, window, &xgwa);
119 if (XQueryTree (dpy, window, &root, &parent, &kids, &nkids))
121 if (parent && parent != root)
124 window_origin (dpy, parent, &px, &py);
129 if (kids) XFree (kids);
135 /* Queries the window position to see if the window has moved or resized.
136 We poll this instead of waiting for ConfigureNotify events, because
137 when the window manager moves the window, only one ConfigureNotify
138 comes in: at the end of the motion. If we poll, we can react to the
139 new position while the window is still being moved. (Assuming the WM
140 does OpaqueMove, of course.)
143 check_window_moved (b_state *state)
145 float oxmin = state->xmin;
146 float oxmax = state->xmax;
147 float oymin = state->ymin;
148 float oymax = state->ymax;
150 XGetWindowAttributes (state->dpy, state->window, &state->xgwa);
151 window_origin (state->dpy, state->window, &wx, &wy);
154 state->xmax = state->xmin + state->xgwa.width;
155 state->ymax = state->ymin + state->xgwa.height - state->font_height;
157 if (state->dbuf && (state->ba))
159 if (oxmax != state->xmax || oymax != state->ymax)
161 XFreePixmap (state->dpy, state->ba);
162 state->ba = XCreatePixmap (state->dpy, state->window,
163 state->xgwa.width, state->xgwa.height,
165 XFillRectangle (state->dpy, state->ba, state->erase_gc, 0, 0,
166 state->xgwa.width, state->xgwa.height);
167 state->b = state->ba;
172 /* Only need to erase the window if the origin moved */
173 if (oxmin != state->xmin || oymin != state->ymin)
174 XClearWindow (state->dpy, state->window);
175 else if (state->fps_p && oymax != state->ymax)
176 XFillRectangle (state->dpy, state->b, state->erase_gc,
177 0, state->xgwa.height - state->font_height,
178 state->xgwa.width, state->font_height);
183 /* Returns the position of the mouse relative to the root window.
186 query_mouse (b_state *state, int *x, int *y)
188 Window root1, child1;
189 int mouse_x, mouse_y, root_x, root_y;
191 if (XQueryPointer (state->dpy, state->window, &root1, &child1,
192 &root_x, &root_y, &mouse_x, &mouse_y, &mask))
204 /* Re-pick the colors of the balls, and the mouse-ball.
207 recolor (b_state *state)
210 XFreeColors (state->dpy, state->xgwa.colormap, &state->fg.pixel, 1, 0);
211 if (state->fg2.flags)
212 XFreeColors (state->dpy, state->xgwa.colormap, &state->fg2.pixel, 1, 0);
214 state->fg.flags = DoRed|DoGreen|DoBlue;
215 state->fg.red = 0x8888 + (random() % 0x8888);
216 state->fg.green = 0x8888 + (random() % 0x8888);
217 state->fg.blue = 0x8888 + (random() % 0x8888);
219 state->fg2.flags = DoRed|DoGreen|DoBlue;
220 state->fg2.red = 0x8888 + (random() % 0x8888);
221 state->fg2.green = 0x8888 + (random() % 0x8888);
222 state->fg2.blue = 0x8888 + (random() % 0x8888);
224 if (XAllocColor (state->dpy, state->xgwa.colormap, &state->fg))
225 XSetForeground (state->dpy, state->draw_gc, state->fg.pixel);
227 if (XAllocColor (state->dpy, state->xgwa.colormap, &state->fg2))
228 XSetForeground (state->dpy, state->draw_gc2, state->fg2.pixel);
231 /* Initialize the state structure and various X data.
234 init_balls (Display *dpy, Window window)
238 b_state *state = (b_state *) calloc (1, sizeof(*state));
243 state->window = window;
245 check_window_moved (state);
247 state->dbuf = get_boolean_resource ("doubleBuffer", "Boolean");
248 state->dbeclear_p = get_boolean_resource ("useDBEClear", "Boolean");
252 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
253 if (state->dbeclear_p)
254 state->b = xdbe_get_backbuffer (dpy, window, XdbeBackground);
256 state->b = xdbe_get_backbuffer (dpy, window, XdbeUndefined);
257 state->backb = state->b;
258 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
262 state->ba = XCreatePixmap (state->dpy, state->window,
263 state->xgwa.width, state->xgwa.height,
265 state->b = state->ba;
270 state->b = state->window;
273 /* Select ButtonRelease events on the external window, if no other app has
274 already selected it (only one app can select it at a time: BadAccess. */
275 if (! (state->xgwa.all_event_masks & ButtonReleaseMask))
276 XSelectInput (state->dpy, state->window,
277 state->xgwa.your_event_mask | ButtonReleaseMask);
279 gcv.foreground = get_pixel_resource("foreground", "Foreground",
280 state->dpy, state->xgwa.colormap);
281 gcv.background = get_pixel_resource("background", "Background",
282 state->dpy, state->xgwa.colormap);
283 state->draw_gc = XCreateGC (state->dpy, state->b,
284 GCForeground|GCBackground, &gcv);
286 gcv.foreground = get_pixel_resource("mouseForeground", "MouseForeground",
287 state->dpy, state->xgwa.colormap);
288 state->draw_gc2 = XCreateGC (state->dpy, state->b,
289 GCForeground|GCBackground, &gcv);
291 gcv.foreground = gcv.background;
292 state->erase_gc = XCreateGC (state->dpy, state->b,
293 GCForeground|GCBackground, &gcv);
297 XFillRectangle (state->dpy, state->ba, state->erase_gc, 0, 0,
298 state->xgwa.width, state->xgwa.height);
302 extx = state->xmax - state->xmin;
303 exty = state->ymax - state->ymin;
305 state->count = get_integer_resource ("count", "Count");
306 if (state->count < 1) state->count = 20;
308 state->max_radius = get_float_resource ("size", "Size") / 2;
309 if (state->max_radius < 1.0) state->max_radius = 1.0;
311 state->random_sizes_p = get_boolean_resource ("random", "Random");
313 state->accx = get_float_resource ("wind", "Wind");
314 if (state->accx < -1.0 || state->accx > 1.0) state->accx = 0;
316 state->accy = get_float_resource ("gravity", "Gravity");
317 if (state->accy < -1.0 || state->accy > 1.0) state->accy = 0.01;
319 state->e = get_float_resource ("elasticity", "Elacitcity");
320 if (state->e < 0.2 || state->e > 1.0) state->e = 0.97;
322 state->tc = get_float_resource ("timeScale", "TimeScale");
323 if (state->tc <= 0 || state->tc > 10) state->tc = 1.0;
325 state->shake_p = get_boolean_resource ("shake", "Shake");
326 state->shake_threshold = get_float_resource ("shakeThreshold",
329 state->fps_p = get_boolean_resource ("doFPS", "DoFPS");
333 char *fontname = get_string_resource ("font", "Font");
334 const char *def_font = "fixed";
335 if (!fontname || !*fontname) fontname = (char *)def_font;
336 font = XLoadQueryFont (dpy, fontname);
337 if (!font) font = XLoadQueryFont (dpy, def_font);
339 gcv.font = font->fid;
340 gcv.foreground = get_pixel_resource("textColor", "Foreground",
341 state->dpy, state->xgwa.colormap);
342 state->font_gc = XCreateGC(dpy, state->b,
343 GCFont|GCForeground|GCBackground, &gcv);
344 state->font_height = font->ascent + font->descent;
345 state->font_baseline = font->descent;
348 state->m = (float *) malloc (sizeof (*state->m) * (state->count + 1));
349 state->r = (float *) malloc (sizeof (*state->r) * (state->count + 1));
350 state->vx = (float *) malloc (sizeof (*state->vx) * (state->count + 1));
351 state->vy = (float *) malloc (sizeof (*state->vy) * (state->count + 1));
352 state->px = (float *) malloc (sizeof (*state->px) * (state->count + 1));
353 state->py = (float *) malloc (sizeof (*state->py) * (state->count + 1));
354 state->opx = (float *) malloc (sizeof (*state->opx) * (state->count + 1));
355 state->opy = (float *) malloc (sizeof (*state->opy) * (state->count + 1));
357 for (i=1; i<=state->count; i++)
359 state->px[i] = frand(extx) + state->xmin;
360 state->py[i] = frand(exty) + state->ymin;
361 state->vx[i] = frand(0.2) - 0.1;
362 state->vy[i] = frand(0.2) - 0.1;
364 state->r[i] = (state->random_sizes_p
365 ? ((0.2 + frand(0.8)) * state->max_radius)
366 : state->max_radius);
367 /*state->r[i] = pow(frand(1.0), state->sizegamma) * state->max_radius;*/
369 /* state->m[i] = pow(state->r[i],2) * M_PI; */
370 state->m[i] = pow(state->r[i],3) * M_PI * 1.3333;
373 memcpy (state->opx, state->px, sizeof (*state->opx) * (state->count + 1));
374 memcpy (state->opy, state->py, sizeof (*state->opx) * (state->count + 1));
380 /* Messes with gravity: permute "down" to be in a random direction.
383 shake (b_state *state)
385 float a = state->accx;
386 float b = state->accy;
387 int i = random() % 4;
412 state->time_since_shake = 0;
417 /* Look at the current time, and update state->time_since_shake.
418 Draw the FPS display if desired.
421 check_wall_clock (b_state *state, float max_d)
424 state->frame_count++;
426 if (tick++ > 20) /* don't call gettimeofday() too often -- it's slow. */
429 static struct timeval last = { 0, 0 };
430 # ifdef GETTIMEOFDAY_TWO_ARGS
432 gettimeofday(&now, &tzp);
437 if (last.tv_sec == 0)
441 if (now.tv_sec == last.tv_sec)
444 state->time_since_shake += (now.tv_sec - last.tv_sec);
448 float elapsed = ((now.tv_sec + (now.tv_usec / 1000000.0)) -
449 (last.tv_sec + (last.tv_usec / 1000000.0)));
450 float fps = state->frame_count / elapsed;
451 float cps = state->collision_count / elapsed;
453 sprintf (state->fps_str,
454 " FPS: %.2f Collisions: %.3f/frame Max motion: %.3f",
455 fps, cps/fps, max_d);
457 draw_fps_string(state);
460 state->frame_count = 0;
461 state->collision_count = 0;
466 /* Erases the balls at their previous positions, and draws the new ones.
469 repaint_balls (b_state *state)
472 int x1a, x2a, y1a, y2a;
473 int x1b, x2b, y1b, y2b;
476 for (a=1; a <= state->count; a++)
479 x1a = (state->opx[a] - state->r[a] - state->xmin);
480 y1a = (state->opy[a] - state->r[a] - state->ymin);
481 x2a = (state->opx[a] + state->r[a] - state->xmin);
482 y2a = (state->opy[a] + state->r[a] - state->ymin);
484 x1b = (state->px[a] - state->r[a] - state->xmin);
485 y1b = (state->py[a] - state->r[a] - state->ymin);
486 x2b = (state->px[a] + state->r[a] - state->xmin);
487 y2b = (state->py[a] + state->r[a] - state->ymin);
489 if (!state->dbeclear_p
490 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
492 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
495 /* if (x1a != x1b || y1a != y1b) -- leaves turds if we optimize this */
497 gc = state->erase_gc;
498 XFillArc (state->dpy, state->b, gc,
499 x1a, y1a, x2a-x1a, y2a-y1a,
503 if (state->mouse_ball == a)
504 gc = state->draw_gc2;
508 XFillArc (state->dpy, state->b, gc,
509 x1b, y1b, x2b-x1b, y2b-y1b,
514 /* distance this ball moved this frame */
515 float d = ((state->px[a] - state->opx[a]) *
516 (state->px[a] - state->opx[a]) +
517 (state->py[a] - state->opy[a]) *
518 (state->py[a] - state->opy[a]));
519 if (d > max_d) max_d = d;
522 state->opx[a] = state->px[a];
523 state->opy[a] = state->py[a];
526 if (state->fps_p && state->dbeclear_p)
527 draw_fps_string(state);
529 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
532 XdbeSwapInfo info[1];
533 info[0].swap_window = state->window;
534 info[0].swap_action = (state->dbeclear_p ? XdbeBackground : XdbeUndefined);
535 XdbeSwapBuffers (state->dpy, info, 1);
538 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
541 XCopyArea (state->dpy, state->b, state->window, state->erase_gc,
542 0, 0, state->xgwa.width, state->xgwa.height, 0, 0);
545 if (state->shake_p && state->time_since_shake > 5)
547 max_d /= state->max_radius;
548 if (max_d < state->shake_threshold || /* when its stable */
549 state->time_since_shake > 30) /* or when 30 secs has passed */
555 check_wall_clock (state, max_d);
559 /* Implements the laws of physics: move balls to their new positions.
562 update_balls (b_state *state)
565 float d, vxa, vya, vxb, vyb, dd, cdx, cdy;
566 float ma, mb, vca, vcb, dva, dvb;
569 check_window_moved (state);
571 /* If we're currently tracking the mouse, update that ball first.
573 if (state->mouse_ball != 0)
575 int mouse_x, mouse_y;
576 query_mouse (state, &mouse_x, &mouse_y);
577 state->px[state->mouse_ball] = mouse_x;
578 state->py[state->mouse_ball] = mouse_y;
579 state->vx[state->mouse_ball] =
581 (state->px[state->mouse_ball] - state->opx[state->mouse_ball]) *
583 state->vy[state->mouse_ball] =
585 (state->py[state->mouse_ball] - state->opy[state->mouse_ball]) *
589 /* For each ball, compute the influence of every other ball. */
590 for (a=1; a <= state->count - 1; a++)
591 for (b=a + 1; b <= state->count; b++)
593 d = ((state->px[a] - state->px[b]) *
594 (state->px[a] - state->px[b]) +
595 (state->py[a] - state->py[b]) *
596 (state->py[a] - state->py[b]));
597 dee2 = (state->r[a] + state->r[b]) *
598 (state->r[a] + state->r[b]);
601 state->collision_count++;
603 dd = state->r[a] + state->r[b] - d;
605 cdx = (state->px[b] - state->px[a]) / d;
606 cdy = (state->py[b] - state->py[a]) / d;
608 /* Move each ball apart from the other by half the
609 * 'collision' distance.
611 state->px[a] -= 0.5 * dd * cdx;
612 state->py[a] -= 0.5 * dd * cdy;
613 state->px[b] += 0.5 * dd * cdx;
614 state->py[b] += 0.5 * dd * cdy;
624 vca = vxa * cdx + vya * cdy; /* the component of each velocity */
625 vcb = vxb * cdx + vyb * cdy; /* along the axis of the collision */
627 /* elastic collison */
628 dva = (vca * (ma - mb) + vcb * 2 * mb) / (ma + mb) - vca;
629 dvb = (vcb * (mb - ma) + vca * 2 * ma) / (ma + mb) - vcb;
631 dva *= state->e; /* some energy lost to inelasticity */
635 dva += (frand (50) - 25) / ma; /* q: why are elves so chaotic? */
636 dvb += (frand (50) - 25) / mb; /* a: brownian motion. */
651 /* Force all balls to be on screen.
653 for (a=1; a <= state->count; a++)
655 if (state->px[a] <= (state->xmin + state->r[a]))
657 state->px[a] = state->xmin + state->r[a];
658 state->vx[a] = -state->vx[a] * state->e;
660 if (state->px[a] >= (state->xmax - state->r[a]))
662 state->px[a] = state->xmax - state->r[a];
663 state->vx[a] = -state->vx[a] * state->e;
665 if (state->py[a] <= (state->ymin + state->r[a]))
667 state->py[a] = state->ymin + state->r[a];
668 state->vy[a] = -state->vy[a] * state->e;
670 if (state->py[a] >= (state->ymax - state->r[a]))
672 state->py[a] = state->ymax - state->r[a];
673 state->vy[a] = -state->vy[a] * state->e;
677 /* Apply gravity to all balls.
679 for (a=1; a <= state->count; a++)
680 if (a != state->mouse_ball)
682 state->vx[a] += state->accx * state->tc;
683 state->vy[a] += state->accy * state->tc;
684 state->px[a] += state->vx[a] * state->tc;
685 state->py[a] += state->vy[a] * state->tc;
690 /* Handle X events, specifically, allow a ball to be picked up with the mouse.
693 handle_events (b_state *state)
695 XSync (state->dpy, False);
696 while (XPending (state->dpy))
699 XNextEvent (state->dpy, &event);
700 if (event.xany.type == ButtonPress)
703 float fmx = event.xbutton.x_root;
704 float fmy = event.xbutton.y_root;
705 if (state->mouse_ball != 0) /* second down-click? drop the ball. */
707 state->mouse_ball = 0;
711 for (i=1; i <= state->count; i++)
713 float d = ((state->px[i] - fmx) * (state->px[i] - fmx) +
714 (state->py[i] - fmy) * (state->py[i] - fmy));
715 float r = state->r[i];
718 state->mouse_ball = i;
723 else if (event.xany.type == ButtonRelease) /* drop the ball */
725 state->mouse_ball = 0;
728 else if (event.xany.type == ConfigureNotify)
730 /* This is redundant, since we poll this every iteration. */
731 check_window_moved (state);
734 screenhack_handle_event (state->dpy, &event);
739 char *progclass = "FluidBalls";
741 char *defaults [] = {
742 ".background: black",
743 ".textColor: yellow",
744 ".font: -*-helvetica-*-r-*-*-*-180-*-*-p-*-*-*",
755 "*shakeThreshold: 0.015",
756 "*doubleBuffer: True",
757 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
759 "*useDBEClear: True",
760 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
764 XrmOptionDescRec options [] = {
765 { "-delay", ".delay", XrmoptionSepArg, 0 },
766 { "-count", ".count", XrmoptionSepArg, 0 },
767 { "-size", ".size", XrmoptionSepArg, 0 },
768 { "-count", ".count", XrmoptionSepArg, 0 },
769 { "-gravity", ".gravity", XrmoptionSepArg, 0 },
770 { "-wind", ".wind", XrmoptionSepArg, 0 },
771 { "-elasticity", ".elasticity", XrmoptionSepArg, 0 },
772 { "-fps", ".doFPS", XrmoptionNoArg, "True" },
773 { "-no-fps", ".doFPS", XrmoptionNoArg, "False" },
774 { "-shake", ".shake", XrmoptionNoArg, "True" },
775 { "-no-shake", ".shake", XrmoptionNoArg, "False" },
776 { "-random", ".random", XrmoptionNoArg, "True" },
777 { "-nonrandom", ".random", XrmoptionNoArg, "False" },
778 { "-db", ".doubleBuffer", XrmoptionNoArg, "True" },
779 { "-no-db", ".doubleBuffer", XrmoptionNoArg, "False" },
784 screenhack (Display *dpy, Window window)
786 b_state *state = init_balls(dpy, window);
787 int delay = get_integer_resource ("delay", "Integer");
791 repaint_balls(state);
795 handle_events (state);
796 if (delay) usleep (delay);