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;
41 Pixmap b, ba; /* double-buffer to reduce flicker */
42 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
45 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
47 GC draw_gc; /* most of the balls */
48 GC draw_gc2; /* the ball being dragged with the mouse */
53 int count; /* number of balls */
54 float xmin, ymin; /* rectangle of window, relative to root */
57 int mouse_ball; /* index of ball being dragged, or 0 if none. */
59 float tc; /* time constant (time-warp multiplier) */
60 float accx; /* horizontal acceleration (wind) */
61 float accy; /* vertical acceleration (gravity) */
63 float *vx, *vy; /* current ball velocities */
64 float *px, *py; /* current ball positions */
65 float *opx, *opy; /* previous ball positions */
66 float *r; /* ball radiuses */
68 float *m; /* ball mass, precalculated */
69 float e; /* coeficient of elasticity */
70 float max_radius; /* largest radius of any ball */
72 Bool random_sizes_p; /* Whether balls should be various sizes up to max. */
73 Bool shake_p; /* Whether to mess with gravity when things settle. */
74 Bool dbuf; /* Whether we're using double buffering. */
75 float shake_threshold;
78 Bool fps_p; /* Whether to draw some text at the bottom. */
87 struct timeval last_time;
92 /* Draws the frames per second string */
94 draw_fps_string (b_state *state)
96 XFillRectangle (state->dpy, state->b, state->erase_gc,
97 0, state->xgwa.height - state->font_height,
98 state->xgwa.width, state->font_height);
99 XDrawImageString (state->dpy, state->b, state->font_gc,
100 0, state->xgwa.height - state->font_baseline,
101 state->fps_str, strlen(state->fps_str));
104 /* Finds the origin of the window relative to the root window, by
105 walking up the window tree until it reaches the top.
108 window_origin (Display *dpy, Window window, int *x, int *y)
110 XTranslateCoordinates (dpy, window, RootWindow (dpy, DefaultScreen (dpy)),
111 0, 0, x, y, &window);
115 /* Queries the window position to see if the window has moved or resized.
116 We poll this instead of waiting for ConfigureNotify events, because
117 when the window manager moves the window, only one ConfigureNotify
118 comes in: at the end of the motion. If we poll, we can react to the
119 new position while the window is still being moved. (Assuming the WM
120 does OpaqueMove, of course.)
123 check_window_moved (b_state *state)
125 float oxmin = state->xmin;
126 float oxmax = state->xmax;
127 float oymin = state->ymin;
128 float oymax = state->ymax;
130 XGetWindowAttributes (state->dpy, state->window, &state->xgwa);
131 window_origin (state->dpy, state->window, &wx, &wy);
134 state->xmax = state->xmin + state->xgwa.width;
135 state->ymax = state->ymin + state->xgwa.height - state->font_height;
137 if (state->dbuf && (state->ba))
139 if (oxmax != state->xmax || oymax != state->ymax)
141 XFreePixmap (state->dpy, state->ba);
142 state->ba = XCreatePixmap (state->dpy, state->window,
143 state->xgwa.width, state->xgwa.height,
145 XFillRectangle (state->dpy, state->ba, state->erase_gc, 0, 0,
146 state->xgwa.width, state->xgwa.height);
147 state->b = state->ba;
152 /* Only need to erase the window if the origin moved */
153 if (oxmin != state->xmin || oymin != state->ymin)
154 XClearWindow (state->dpy, state->window);
155 else if (state->fps_p && oymax != state->ymax)
156 XFillRectangle (state->dpy, state->b, state->erase_gc,
157 0, state->xgwa.height - state->font_height,
158 state->xgwa.width, state->font_height);
163 /* Returns the position of the mouse relative to the root window.
166 query_mouse (b_state *state, int *x, int *y)
168 Window root1, child1;
169 int mouse_x, mouse_y, root_x, root_y;
171 if (XQueryPointer (state->dpy, state->window, &root1, &child1,
172 &root_x, &root_y, &mouse_x, &mouse_y, &mask))
184 /* Re-pick the colors of the balls, and the mouse-ball.
187 recolor (b_state *state)
190 XFreeColors (state->dpy, state->xgwa.colormap, &state->fg.pixel, 1, 0);
191 if (state->fg2.flags)
192 XFreeColors (state->dpy, state->xgwa.colormap, &state->fg2.pixel, 1, 0);
194 state->fg.flags = DoRed|DoGreen|DoBlue;
195 state->fg.red = 0x8888 + (random() % 0x8888);
196 state->fg.green = 0x8888 + (random() % 0x8888);
197 state->fg.blue = 0x8888 + (random() % 0x8888);
199 state->fg2.flags = DoRed|DoGreen|DoBlue;
200 state->fg2.red = 0x8888 + (random() % 0x8888);
201 state->fg2.green = 0x8888 + (random() % 0x8888);
202 state->fg2.blue = 0x8888 + (random() % 0x8888);
204 if (XAllocColor (state->dpy, state->xgwa.colormap, &state->fg))
205 XSetForeground (state->dpy, state->draw_gc, state->fg.pixel);
207 if (XAllocColor (state->dpy, state->xgwa.colormap, &state->fg2))
208 XSetForeground (state->dpy, state->draw_gc2, state->fg2.pixel);
211 /* Initialize the state structure and various X data.
214 fluidballs_init (Display *dpy, Window window)
218 b_state *state = (b_state *) calloc (1, sizeof(*state));
222 state->window = window;
223 state->delay = get_integer_resource (dpy, "delay", "Integer");
225 check_window_moved (state);
227 state->dbuf = get_boolean_resource (dpy, "doubleBuffer", "Boolean");
229 # ifdef HAVE_COCOA /* Don't second-guess Quartz's double-buffering */
235 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
236 state->dbeclear_p = get_boolean_resource (dpy, "useDBEClear", "Boolean");
237 if (state->dbeclear_p)
238 state->b = xdbe_get_backbuffer (dpy, window, XdbeBackground);
240 state->b = xdbe_get_backbuffer (dpy, window, XdbeUndefined);
241 state->backb = state->b;
242 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
246 state->ba = XCreatePixmap (state->dpy, state->window,
247 state->xgwa.width, state->xgwa.height,
249 state->b = state->ba;
254 state->b = state->window;
257 /* Select ButtonRelease events on the external window, if no other app has
258 already selected it (only one app can select it at a time: BadAccess. */
260 if (! (state->xgwa.all_event_masks & ButtonReleaseMask))
261 XSelectInput (state->dpy, state->window,
262 state->xgwa.your_event_mask | ButtonReleaseMask);
265 gcv.foreground = get_pixel_resource(state->dpy, state->xgwa.colormap,
266 "foreground", "Foreground");
267 gcv.background = get_pixel_resource(state->dpy, state->xgwa.colormap,
268 "background", "Background");
269 state->draw_gc = XCreateGC (state->dpy, state->b,
270 GCForeground|GCBackground, &gcv);
272 gcv.foreground = get_pixel_resource(state->dpy, state->xgwa.colormap,
273 "mouseForeground", "MouseForeground");
274 state->draw_gc2 = XCreateGC (state->dpy, state->b,
275 GCForeground|GCBackground, &gcv);
277 gcv.foreground = gcv.background;
278 state->erase_gc = XCreateGC (state->dpy, state->b,
279 GCForeground|GCBackground, &gcv);
283 XFillRectangle (state->dpy, state->ba, state->erase_gc, 0, 0,
284 state->xgwa.width, state->xgwa.height);
288 extx = state->xmax - state->xmin;
289 exty = state->ymax - state->ymin;
291 state->count = get_integer_resource (dpy, "count", "Count");
292 if (state->count < 1) state->count = 20;
294 state->max_radius = get_float_resource (dpy, "size", "Size") / 2;
295 if (state->max_radius < 1.0) state->max_radius = 1.0;
297 state->random_sizes_p = get_boolean_resource (dpy, "random", "Random");
299 /* If the initial window size is too small to hold all these balls,
300 make fewer of them...
303 float r = (state->random_sizes_p
304 ? state->max_radius * 0.7
305 : state->max_radius);
306 float ball_area = M_PI * r * r;
307 float balls_area = state->count * ball_area;
308 float window_area = state->xgwa.width * state->xgwa.height;
309 window_area *= 0.75; /* don't pack it completely full */
310 if (balls_area > window_area)
311 state->count = window_area / ball_area;
314 state->accx = get_float_resource (dpy, "wind", "Wind");
315 if (state->accx < -1.0 || state->accx > 1.0) state->accx = 0;
317 state->accy = get_float_resource (dpy, "gravity", "Gravity");
318 if (state->accy < -1.0 || state->accy > 1.0) state->accy = 0.01;
320 state->e = get_float_resource (dpy, "elasticity", "Elacitcity");
321 if (state->e < 0.2 || state->e > 1.0) state->e = 0.97;
323 state->tc = get_float_resource (dpy, "timeScale", "TimeScale");
324 if (state->tc <= 0 || state->tc > 10) state->tc = 1.0;
326 state->shake_p = get_boolean_resource (dpy, "shake", "Shake");
327 state->shake_threshold = get_float_resource (dpy, "shakeThreshold",
330 state->fps_p = get_boolean_resource (dpy, "doFPS", "DoFPS");
334 char *fontname = get_string_resource (dpy, "font", "Font");
335 const char *def_font = "fixed";
336 if (!fontname || !*fontname) fontname = (char *)def_font;
337 font = XLoadQueryFont (dpy, fontname);
338 if (!font) font = XLoadQueryFont (dpy, def_font);
340 gcv.font = font->fid;
341 gcv.foreground = get_pixel_resource(state->dpy, state->xgwa.colormap,
342 "textColor", "Foreground");
343 state->font_gc = XCreateGC(dpy, state->b,
344 GCFont|GCForeground|GCBackground, &gcv);
345 state->font_height = font->ascent + font->descent;
346 state->font_baseline = font->descent;
349 state->m = (float *) malloc (sizeof (*state->m) * (state->count + 1));
350 state->r = (float *) malloc (sizeof (*state->r) * (state->count + 1));
351 state->vx = (float *) malloc (sizeof (*state->vx) * (state->count + 1));
352 state->vy = (float *) malloc (sizeof (*state->vy) * (state->count + 1));
353 state->px = (float *) malloc (sizeof (*state->px) * (state->count + 1));
354 state->py = (float *) malloc (sizeof (*state->py) * (state->count + 1));
355 state->opx = (float *) malloc (sizeof (*state->opx) * (state->count + 1));
356 state->opy = (float *) malloc (sizeof (*state->opy) * (state->count + 1));
358 for (i=1; i<=state->count; i++)
360 state->px[i] = frand(extx) + state->xmin;
361 state->py[i] = frand(exty) + state->ymin;
362 state->vx[i] = frand(0.2) - 0.1;
363 state->vy[i] = frand(0.2) - 0.1;
365 state->r[i] = (state->random_sizes_p
366 ? ((0.2 + frand(0.8)) * state->max_radius)
367 : state->max_radius);
368 /*state->r[i] = pow(frand(1.0), state->sizegamma) * state->max_radius;*/
370 /* state->m[i] = pow(state->r[i],2) * M_PI; */
371 state->m[i] = pow(state->r[i],3) * M_PI * 1.3333;
374 memcpy (state->opx, state->px, sizeof (*state->opx) * (state->count + 1));
375 memcpy (state->opy, state->py, sizeof (*state->opx) * (state->count + 1));
381 /* Messes with gravity: permute "down" to be in a random direction.
384 shake (b_state *state)
386 float a = state->accx;
387 float b = state->accy;
388 int i = random() % 4;
413 state->time_since_shake = 0;
418 /* Look at the current time, and update state->time_since_shake.
419 Draw the FPS display if desired.
422 check_wall_clock (b_state *state, float max_d)
424 state->frame_count++;
426 if (state->time_tick++ > 20) /* don't call gettimeofday() too often -- it's slow. */
429 # ifdef GETTIMEOFDAY_TWO_ARGS
431 gettimeofday(&now, &tzp);
436 if (state->last_time.tv_sec == 0)
437 state->last_time = now;
439 state->time_tick = 0;
440 if (now.tv_sec == state->last_time.tv_sec)
443 state->time_since_shake += (now.tv_sec - state->last_time.tv_sec);
447 float elapsed = ((now.tv_sec + (now.tv_usec / 1000000.0)) -
448 (state->last_time.tv_sec + (state->last_time.tv_usec / 1000000.0)));
449 float fps = state->frame_count / elapsed;
450 float cps = state->collision_count / elapsed;
452 sprintf (state->fps_str,
453 " FPS: %.2f Collisions: %.3f/frame Max motion: %.3f",
454 fps, cps/fps, max_d);
456 draw_fps_string(state);
459 state->frame_count = 0;
460 state->collision_count = 0;
461 state->last_time = now;
465 /* Erases the balls at their previous positions, and draws the new ones.
468 repaint_balls (b_state *state)
471 int x1a, x2a, y1a, y2a;
472 int x1b, x2b, y1b, y2b;
475 #ifdef HAVE_COCOA /* Don't second-guess Quartz's double-buffering */
476 XClearWindow (state->dpy, state->b);
479 for (a=1; a <= state->count; a++)
482 x1a = (state->opx[a] - state->r[a] - state->xmin);
483 y1a = (state->opy[a] - state->r[a] - state->ymin);
484 x2a = (state->opx[a] + state->r[a] - state->xmin);
485 y2a = (state->opy[a] + state->r[a] - state->ymin);
487 x1b = (state->px[a] - state->r[a] - state->xmin);
488 y1b = (state->py[a] - state->r[a] - state->ymin);
489 x2b = (state->px[a] + state->r[a] - state->xmin);
490 y2b = (state->py[a] + state->r[a] - state->ymin);
492 #ifndef HAVE_COCOA /* Don't second-guess Quartz's double-buffering */
493 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
494 if (!state->dbeclear_p || !state->backb)
495 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
497 /* if (x1a != x1b || y1a != y1b) -- leaves turds if we optimize this */
499 gc = state->erase_gc;
500 XFillArc (state->dpy, state->b, gc,
501 x1a, y1a, x2a-x1a, y2a-y1a,
505 #endif /* !HAVE_COCOA */
507 if (state->mouse_ball == a)
508 gc = state->draw_gc2;
512 XFillArc (state->dpy, state->b, gc,
513 x1b, y1b, x2b-x1b, y2b-y1b,
518 /* distance this ball moved this frame */
519 float d = ((state->px[a] - state->opx[a]) *
520 (state->px[a] - state->opx[a]) +
521 (state->py[a] - state->opy[a]) *
522 (state->py[a] - state->opy[a]));
523 if (d > max_d) max_d = d;
526 state->opx[a] = state->px[a];
527 state->opy[a] = state->py[a];
531 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
532 && (state->backb ? state->dbeclear_p : 1)
533 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
535 draw_fps_string(state);
537 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
540 XdbeSwapInfo info[1];
541 info[0].swap_window = state->window;
542 info[0].swap_action = (state->dbeclear_p ? XdbeBackground : XdbeUndefined);
543 XdbeSwapBuffers (state->dpy, info, 1);
546 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
549 XCopyArea (state->dpy, state->b, state->window, state->erase_gc,
550 0, 0, state->xgwa.width, state->xgwa.height, 0, 0);
553 if (state->shake_p && state->time_since_shake > 5)
555 max_d /= state->max_radius;
556 if (max_d < state->shake_threshold || /* when its stable */
557 state->time_since_shake > 30) /* or when 30 secs has passed */
563 check_wall_clock (state, max_d);
567 /* Implements the laws of physics: move balls to their new positions.
570 update_balls (b_state *state)
573 float d, vxa, vya, vxb, vyb, dd, cdx, cdy;
574 float ma, mb, vca, vcb, dva, dvb;
577 check_window_moved (state);
579 /* If we're currently tracking the mouse, update that ball first.
581 if (state->mouse_ball != 0)
583 int mouse_x, mouse_y;
584 query_mouse (state, &mouse_x, &mouse_y);
585 state->px[state->mouse_ball] = mouse_x;
586 state->py[state->mouse_ball] = mouse_y;
587 state->vx[state->mouse_ball] =
589 (state->px[state->mouse_ball] - state->opx[state->mouse_ball]) *
591 state->vy[state->mouse_ball] =
593 (state->py[state->mouse_ball] - state->opy[state->mouse_ball]) *
597 /* For each ball, compute the influence of every other ball. */
598 for (a=1; a <= state->count - 1; a++)
599 for (b=a + 1; b <= state->count; b++)
601 d = ((state->px[a] - state->px[b]) *
602 (state->px[a] - state->px[b]) +
603 (state->py[a] - state->py[b]) *
604 (state->py[a] - state->py[b]));
605 dee2 = (state->r[a] + state->r[b]) *
606 (state->r[a] + state->r[b]);
609 state->collision_count++;
611 dd = state->r[a] + state->r[b] - d;
613 cdx = (state->px[b] - state->px[a]) / d;
614 cdy = (state->py[b] - state->py[a]) / d;
616 /* Move each ball apart from the other by half the
617 * 'collision' distance.
619 state->px[a] -= 0.5 * dd * cdx;
620 state->py[a] -= 0.5 * dd * cdy;
621 state->px[b] += 0.5 * dd * cdx;
622 state->py[b] += 0.5 * dd * cdy;
632 vca = vxa * cdx + vya * cdy; /* the component of each velocity */
633 vcb = vxb * cdx + vyb * cdy; /* along the axis of the collision */
635 /* elastic collison */
636 dva = (vca * (ma - mb) + vcb * 2 * mb) / (ma + mb) - vca;
637 dvb = (vcb * (mb - ma) + vca * 2 * ma) / (ma + mb) - vcb;
639 dva *= state->e; /* some energy lost to inelasticity */
643 dva += (frand (50) - 25) / ma; /* q: why are elves so chaotic? */
644 dvb += (frand (50) - 25) / mb; /* a: brownian motion. */
659 /* Force all balls to be on screen.
661 for (a=1; a <= state->count; a++)
663 if (state->px[a] <= (state->xmin + state->r[a]))
665 state->px[a] = state->xmin + state->r[a];
666 state->vx[a] = -state->vx[a] * state->e;
668 if (state->px[a] >= (state->xmax - state->r[a]))
670 state->px[a] = state->xmax - state->r[a];
671 state->vx[a] = -state->vx[a] * state->e;
673 if (state->py[a] <= (state->ymin + state->r[a]))
675 state->py[a] = state->ymin + state->r[a];
676 state->vy[a] = -state->vy[a] * state->e;
678 if (state->py[a] >= (state->ymax - state->r[a]))
680 state->py[a] = state->ymax - state->r[a];
681 state->vy[a] = -state->vy[a] * state->e;
685 /* Apply gravity to all balls.
687 for (a=1; a <= state->count; a++)
688 if (a != state->mouse_ball)
690 state->vx[a] += state->accx * state->tc;
691 state->vy[a] += state->accy * state->tc;
692 state->px[a] += state->vx[a] * state->tc;
693 state->py[a] += state->vy[a] * state->tc;
698 /* Handle X events, specifically, allow a ball to be picked up with the mouse.
701 fluidballs_event (Display *dpy, Window window, void *closure, XEvent *event)
703 b_state *state = (b_state *) closure;
705 if (event->xany.type == ButtonPress)
708 XTranslateCoordinates (dpy, window, RootWindow (dpy, DefaultScreen(dpy)),
709 event->xbutton.x, event->xbutton.y, &rx, &ry,
712 if (state->mouse_ball != 0) /* second down-click? drop the ball. */
714 state->mouse_ball = 0;
718 for (i=1; i <= state->count; i++)
720 float d = ((state->px[i] - rx) * (state->px[i] - rx) +
721 (state->py[i] - ry) * (state->py[i] - ry));
722 float r = state->r[i];
725 state->mouse_ball = i;
731 else if (event->xany.type == ButtonRelease) /* drop the ball */
733 state->mouse_ball = 0;
741 fluidballs_draw (Display *dpy, Window window, void *closure)
743 b_state *state = (b_state *) closure;
744 repaint_balls(state);
750 fluidballs_reshape (Display *dpy, Window window, void *closure,
751 unsigned int w, unsigned int h)
756 fluidballs_free (Display *dpy, Window window, void *closure)
758 b_state *state = (b_state *) closure;
763 static const char *fluidballs_defaults [] = {
764 ".background: black",
765 ".foreground: yellow",
766 ".textColor: yellow",
767 "*mouseForeground: white",
768 ".font: -*-helvetica-*-r-*-*-*-180-*-*-p-*-*-*",
779 "*shakeThreshold: 0.015",
780 "*doubleBuffer: True",
781 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
783 "*useDBEClear: True",
784 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
788 static XrmOptionDescRec fluidballs_options [] = {
789 { "-delay", ".delay", XrmoptionSepArg, 0 },
790 { "-count", ".count", XrmoptionSepArg, 0 },
791 { "-size", ".size", XrmoptionSepArg, 0 },
792 { "-count", ".count", XrmoptionSepArg, 0 },
793 { "-gravity", ".gravity", XrmoptionSepArg, 0 },
794 { "-wind", ".wind", XrmoptionSepArg, 0 },
795 { "-elasticity", ".elasticity", XrmoptionSepArg, 0 },
796 { "-fps", ".doFPS", XrmoptionNoArg, "True" },
797 { "-no-fps", ".doFPS", XrmoptionNoArg, "False" },
798 { "-shake", ".shake", XrmoptionNoArg, "True" },
799 { "-no-shake", ".shake", XrmoptionNoArg, "False" },
800 { "-random", ".random", XrmoptionNoArg, "True" },
801 { "-no-random", ".random", XrmoptionNoArg, "False" },
802 { "-nonrandom", ".random", XrmoptionNoArg, "False" },
803 { "-db", ".doubleBuffer", XrmoptionNoArg, "True" },
804 { "-no-db", ".doubleBuffer", XrmoptionNoArg, "False" },
809 XSCREENSAVER_MODULE ("Fluidballs", fluidballs)