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/
19 * Specifying a distribution in the ball sizes (with a gamma curve, possibly).
20 * Brownian motion, for that extra touch of realism.
24 #include "screenhack.h"
27 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
29 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
34 XWindowAttributes xgwa;
36 Pixmap b, ba; /* double-buffer to reduce flicker */
37 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
39 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
41 GC draw_gc; /* most of the balls */
42 GC draw_gc2; /* the ball being dragged with the mouse */
47 int count; /* number of balls */
48 float xmin, ymin; /* rectangle of window, relative to root */
51 int mouse_ball; /* index of ball being dragged, or 0 if none. */
53 float tc; /* time constant (time-warp multiplier) */
54 float accx; /* horizontal acceleration (wind) */
55 float accy; /* vertical acceleration (gravity) */
57 float *vx, *vy; /* current ball velocities */
58 float *px, *py; /* current ball positions */
59 float *opx, *opy; /* previous ball positions */
60 float *r; /* ball radiuses */
62 float *m; /* ball mass, precalculated */
63 float e; /* coefficient of friction, I think? */
64 float max_radius; /* largest radius of any ball */
66 Bool random_sizes_p; /* Whether balls should be various sizes up to max. */
67 Bool shake_p; /* Whether to mess with gravity when things settle. */
68 Bool dbuf; /* Whether we're using double buffering. */
69 Bool dbeclear_p; /* ? */
70 float shake_threshold;
73 Bool fps_p; /* Whether to draw some text at the bottom. */
84 /* Draws the frames per second string */
86 draw_fps_string (b_state *state)
88 XFillRectangle (state->dpy, state->b, state->erase_gc,
89 0, state->xgwa.height - state->font_height,
90 state->xgwa.width, state->font_height);
91 XDrawImageString (state->dpy, state->b, state->font_gc,
92 0, state->xgwa.height - state->font_baseline,
93 state->fps_str, strlen(state->fps_str));
96 /* Finds the origin of the window relative to the root window, by
97 walking up the window tree until it reaches the top.
100 window_origin (Display *dpy, Window window, int *x, int *y)
102 Window root, parent, *kids;
104 XWindowAttributes xgwa;
106 XGetWindowAttributes (dpy, window, &xgwa);
115 if (XQueryTree (dpy, window, &root, &parent, &kids, &nkids))
117 if (parent && parent != root)
120 window_origin (dpy, parent, &px, &py);
125 if (kids) XFree (kids);
131 /* Queries the window position to see if the window has moved or resized.
132 We poll this instead of waiting for ConfigureNotify events, because
133 when the window manager moves the window, only one ConfigureNotify
134 comes in: at the end of the motion. If we poll, we can react to the
135 new position while the window is still being moved. (Assuming the WM
136 does OpaqueMove, of course.)
139 check_window_moved (b_state *state)
141 float oxmin = state->xmin;
142 float oxmax = state->xmax;
143 float oymin = state->ymin;
144 float oymax = state->ymax;
146 XGetWindowAttributes (state->dpy, state->window, &state->xgwa);
147 window_origin (state->dpy, state->window, &wx, &wy);
150 state->xmax = state->xmin + state->xgwa.width;
151 state->ymax = state->ymin + state->xgwa.height - state->font_height;
153 if (state->dbuf && (state->ba))
155 if (oxmax != state->xmax || oymax != state->ymax)
157 XFreePixmap (state->dpy, state->ba);
158 state->ba = XCreatePixmap (state->dpy, state->window,
159 state->xgwa.width, state->xgwa.height,
161 XFillRectangle (state->dpy, state->ba, state->erase_gc, 0, 0,
162 state->xgwa.width, state->xgwa.height);
163 state->b = state->ba;
168 /* Only need to erase the window if the origin moved */
169 if (oxmin != state->xmin || oymin != state->ymin)
170 XClearWindow (state->dpy, state->window);
171 else if (state->fps_p && oymax != state->ymax)
172 XFillRectangle (state->dpy, state->b, state->erase_gc,
173 0, state->xgwa.height - state->font_height,
174 state->xgwa.width, state->font_height);
179 /* Returns the position of the mouse relative to the root window.
182 query_mouse (b_state *state, int *x, int *y)
184 Window root1, child1;
185 int mouse_x, mouse_y, root_x, root_y;
187 if (XQueryPointer (state->dpy, state->window, &root1, &child1,
188 &root_x, &root_y, &mouse_x, &mouse_y, &mask))
200 /* Re-pick the colors of the balls, and the mouse-ball.
203 recolor (b_state *state)
206 XFreeColors (state->dpy, state->xgwa.colormap, &state->fg.pixel, 1, 0);
207 if (state->fg2.flags)
208 XFreeColors (state->dpy, state->xgwa.colormap, &state->fg2.pixel, 1, 0);
210 state->fg.flags = DoRed|DoGreen|DoBlue;
211 state->fg.red = 0x8888 + (random() % 0x8888);
212 state->fg.green = 0x8888 + (random() % 0x8888);
213 state->fg.blue = 0x8888 + (random() % 0x8888);
215 state->fg2.flags = DoRed|DoGreen|DoBlue;
216 state->fg2.red = 0x8888 + (random() % 0x8888);
217 state->fg2.green = 0x8888 + (random() % 0x8888);
218 state->fg2.blue = 0x8888 + (random() % 0x8888);
220 if (XAllocColor (state->dpy, state->xgwa.colormap, &state->fg))
221 XSetForeground (state->dpy, state->draw_gc, state->fg.pixel);
223 if (XAllocColor (state->dpy, state->xgwa.colormap, &state->fg2))
224 XSetForeground (state->dpy, state->draw_gc2, state->fg2.pixel);
227 /* Initialize the state structure and various X data.
230 init_balls (Display *dpy, Window window)
234 b_state *state = (b_state *) calloc (1, sizeof(*state));
239 state->window = window;
241 check_window_moved (state);
243 state->dbuf = get_boolean_resource ("doubleBuffer", "Boolean");
244 state->dbeclear_p = get_boolean_resource ("useDBEClear", "Boolean");
248 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
249 if (state->dbeclear_p)
250 state->b = xdbe_get_backbuffer (dpy, window, XdbeBackground);
252 state->b = xdbe_get_backbuffer (dpy, window, XdbeUndefined);
253 state->backb = state->b;
254 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
258 state->ba = XCreatePixmap (state->dpy, state->window,
259 state->xgwa.width, state->xgwa.height,
261 state->b = state->ba;
266 state->b = state->window;
269 /* Select ButtonRelease events on the external window, if no other app has
270 already selected it (only one app can select it at a time: BadAccess. */
271 if (! (state->xgwa.all_event_masks & ButtonReleaseMask))
272 XSelectInput (state->dpy, state->window,
273 state->xgwa.your_event_mask | ButtonReleaseMask);
275 gcv.foreground = get_pixel_resource("foreground", "Foreground",
276 state->dpy, state->xgwa.colormap);
277 gcv.background = get_pixel_resource("background", "Background",
278 state->dpy, state->xgwa.colormap);
279 state->draw_gc = XCreateGC (state->dpy, state->b,
280 GCForeground|GCBackground, &gcv);
282 gcv.foreground = get_pixel_resource("mouseForeground", "MouseForeground",
283 state->dpy, state->xgwa.colormap);
284 state->draw_gc2 = XCreateGC (state->dpy, state->b,
285 GCForeground|GCBackground, &gcv);
287 gcv.foreground = gcv.background;
288 state->erase_gc = XCreateGC (state->dpy, state->b,
289 GCForeground|GCBackground, &gcv);
293 XFillRectangle (state->dpy, state->ba, state->erase_gc, 0, 0,
294 state->xgwa.width, state->xgwa.height);
298 extx = state->xmax - state->xmin;
299 exty = state->ymax - state->ymin;
301 state->count = get_integer_resource ("count", "Count");
302 if (state->count < 1) state->count = 20;
304 state->max_radius = get_float_resource ("size", "Size") / 2;
305 if (state->max_radius < 1.0) state->max_radius = 1.0;
307 state->random_sizes_p = get_boolean_resource ("random", "Random");
309 state->accx = get_float_resource ("wind", "Wind");
310 if (state->accx < -1.0 || state->accx > 1.0) state->accx = 0;
312 state->accy = get_float_resource ("gravity", "Gravity");
313 if (state->accy < -1.0 || state->accy > 1.0) state->accy = 0.01;
315 state->e = get_float_resource ("friction", "Friction");
316 if (state->e < 0.2 || state->e > 1.0) state->e = 0.97;
318 state->tc = get_float_resource ("timeScale", "TimeScale");
319 if (state->tc <= 0 || state->tc > 10) state->tc = 1.0;
321 state->shake_p = get_boolean_resource ("shake", "Shake");
322 state->shake_threshold = get_float_resource ("shakeThreshold",
325 state->fps_p = get_boolean_resource ("doFPS", "DoFPS");
329 char *fontname = get_string_resource ("font", "Font");
330 const char *def_font = "fixed";
331 if (!fontname || !*fontname) fontname = (char *)def_font;
332 font = XLoadQueryFont (dpy, fontname);
333 if (!font) font = XLoadQueryFont (dpy, def_font);
335 gcv.font = font->fid;
336 gcv.foreground = get_pixel_resource("textColor", "Foreground",
337 state->dpy, state->xgwa.colormap);
338 state->font_gc = XCreateGC(dpy, state->b,
339 GCFont|GCForeground|GCBackground, &gcv);
340 state->font_height = font->ascent + font->descent;
341 state->font_baseline = font->descent;
344 state->m = (float *) malloc (sizeof (*state->m) * (state->count + 1));
345 state->r = (float *) malloc (sizeof (*state->r) * (state->count + 1));
346 state->vx = (float *) malloc (sizeof (*state->vx) * (state->count + 1));
347 state->vy = (float *) malloc (sizeof (*state->vy) * (state->count + 1));
348 state->px = (float *) malloc (sizeof (*state->px) * (state->count + 1));
349 state->py = (float *) malloc (sizeof (*state->py) * (state->count + 1));
350 state->opx = (float *) malloc (sizeof (*state->opx) * (state->count + 1));
351 state->opy = (float *) malloc (sizeof (*state->opy) * (state->count + 1));
353 for (i=1; i<=state->count; i++)
355 state->px[i] = frand(extx) + state->xmin;
356 state->py[i] = frand(exty) + state->ymin;
357 state->vx[i] = frand(0.2) - 0.1;
358 state->vy[i] = frand(0.2) - 0.1;
360 state->r[i] = (state->random_sizes_p
361 ? ((0.2 + frand(0.8)) * state->max_radius)
362 : state->max_radius);
363 /*state->r[i] = pow(frand(1.0), state->sizegamma) * state->max_radius;*/
365 /* state->m[i] = pow(state->r[i],2) * M_PI; */
366 state->m[i] = pow(state->r[i],3) * M_PI * 1.3333;
373 /* Messes with gravity: permute "down" to be in a random direction.
376 shake (b_state *state)
378 float a = state->accx;
379 float b = state->accy;
380 int i = random() % 4;
405 state->time_since_shake = 0;
410 /* Look at the current time, and update state->time_since_shake.
411 Draw the FPS display if desired.
414 check_wall_clock (b_state *state, float max_d)
417 state->frame_count++;
419 if (tick++ > 20) /* don't call gettimeofday() too often -- it's slow. */
422 static struct timeval last = {0, };
423 # ifdef GETTIMEOFDAY_TWO_ARGS
425 gettimeofday(&now, &tzp);
430 if (last.tv_sec == 0)
434 if (now.tv_sec == last.tv_sec)
437 state->time_since_shake += (now.tv_sec - last.tv_sec);
441 float elapsed = ((now.tv_sec + (now.tv_usec / 1000000.0)) -
442 (last.tv_sec + (last.tv_usec / 1000000.0)));
443 float fps = state->frame_count / elapsed;
444 float cps = state->collision_count / elapsed;
446 sprintf (state->fps_str,
447 " FPS: %.2f Collisions: %.3f/frame Max motion: %.3f",
448 fps, cps/fps, max_d);
450 draw_fps_string(state);
453 state->frame_count = 0;
454 state->collision_count = 0;
459 /* Erases the balls at their previous positions, and draws the new ones.
462 repaint_balls (b_state *state)
465 int x1a, x2a, y1a, y2a;
466 int x1b, x2b, y1b, y2b;
469 for (a=1; a <= state->count; a++)
472 x1a = (state->opx[a] - state->r[a] - state->xmin);
473 y1a = (state->opy[a] - state->r[a] - state->ymin);
474 x2a = (state->opx[a] + state->r[a] - state->xmin);
475 y2a = (state->opy[a] + state->r[a] - state->ymin);
477 x1b = (state->px[a] - state->r[a] - state->xmin);
478 y1b = (state->py[a] - state->r[a] - state->ymin);
479 x2b = (state->px[a] + state->r[a] - state->xmin);
480 y2b = (state->py[a] + state->r[a] - state->ymin);
482 if (!state->dbeclear_p ||
483 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
485 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
488 /* if (x1a != x1b || y1a != y1b) -- leaves turds if we optimize this */
490 gc = state->erase_gc;
491 XFillArc (state->dpy, state->b, gc,
492 x1a, y1a, x2a-x1a, y2a-y1a,
496 if (state->mouse_ball == a)
497 gc = state->draw_gc2;
501 XFillArc (state->dpy, state->b, gc,
502 x1b, y1b, x2b-x1b, y2b-y1b,
507 /* distance this ball moved this frame */
508 float d = ((state->px[a] - state->opx[a]) *
509 (state->px[a] - state->opx[a]) +
510 (state->py[a] - state->opy[a]) *
511 (state->py[a] - state->opy[a]));
512 if (d > max_d) max_d = d;
515 state->opx[a] = state->px[a];
516 state->opy[a] = state->py[a];
519 if (state->fps_p && state->dbeclear_p)
520 draw_fps_string(state);
522 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
525 XdbeSwapInfo info[1];
526 info[0].swap_window = state->window;
527 info[0].swap_action = (state->dbeclear_p ? XdbeBackground : XdbeUndefined);
528 XdbeSwapBuffers (state->dpy, info, 1);
531 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
534 XCopyArea (state->dpy, state->b, state->window, state->erase_gc,
535 0, 0, state->xgwa.width, state->xgwa.height, 0, 0);
538 if (state->shake_p && state->time_since_shake > 5)
540 max_d /= state->max_radius;
541 if (max_d < state->shake_threshold || /* when its stable */
542 state->time_since_shake > 30) /* or when 30 secs has passed */
548 check_wall_clock (state, max_d);
552 /* Implements the laws of physics: move balls to their new positions.
555 update_balls (b_state *state)
558 float d, vxa, vya, vxb, vyb, dd, cdx, cdy;
559 float ma, mb, vela, velb, vela1, velb1;
562 check_window_moved (state);
564 /* If we're currently tracking the mouse, update that ball first.
566 if (state->mouse_ball != 0)
568 int mouse_x, mouse_y;
569 query_mouse (state, &mouse_x, &mouse_y);
570 state->px[state->mouse_ball] = mouse_x;
571 state->py[state->mouse_ball] = mouse_y;
572 state->vx[state->mouse_ball] =
574 (state->px[state->mouse_ball] - state->opx[state->mouse_ball]) *
576 state->vy[state->mouse_ball] =
578 (state->py[state->mouse_ball] - state->opy[state->mouse_ball]) *
582 /* For each ball, compute the influence of every other ball. */
583 for (a=1; a <= state->count; a++)
584 if (a != state->mouse_ball)
585 for (b=1; b <= state->count; b++)
588 d = ((state->px[a] - state->px[b]) *
589 (state->px[a] - state->px[b]) +
590 (state->py[a] - state->py[b]) *
591 (state->py[a] - state->py[b]));
592 dee2 = (state->r[a] + state->r[b]) *
593 (state->r[a] + state->r[b]);
596 state->collision_count++;
598 dd = state->r[a] + state->r[b] - d;
599 /* A pair of balls that have already collided in this
600 * current frame (and therefore touching each other)
601 * should not have another collision calculated, hence
602 * the fallthru if "dd ~= 0.0".
604 if ((dd < -0.01) || (dd > 0.01))
606 cdx = (state->px[b] - state->px[a]) / d;
607 cdy = (state->py[b] - state->py[a]) / d;
609 /* Move each ball apart from the other by half the
610 * 'collision' distance.
612 state->px[a] -= 0.5 * dd * cdx;
613 state->py[a] -= 0.5 * dd * cdy;
614 state->px[b] += 0.5 * dd * cdx;
615 state->py[b] += 0.5 * dd * cdy;
624 vela = sqrt((vxa * vxa) + (vya * vya));
625 velb = sqrt((vxb * vxb) + (vyb * vyb));
627 vela1 = vela * ((ma - mb) / (ma + mb)) +
628 velb * ((2 * mb) / (ma + mb));
629 velb1 = vela * ((2 * ma) / (ma + mb)) +
630 velb * ((mb - ma) / (ma + mb));
632 vela1 *= state->e; /* "air resistance" */
635 vela1 += (frand (50) - 25) / ma; /* brownian motion */
636 velb1 += (frand (50) - 25) / mb;
638 state->vx[a] = -cdx * vela1;
639 state->vy[a] = -cdy * vela1;
640 state->vx[b] = cdx * velb1;
641 state->vy[b] = cdy * velb1;
646 /* Force all balls to be on screen.
648 for (a=1; a <= state->count; a++)
650 if (state->px[a] <= (state->xmin + state->r[a]))
652 state->px[a] = state->xmin + state->r[a];
653 state->vx[a] = -state->vx[a] * state->e;
655 if (state->px[a] >= (state->xmax - state->r[a]))
657 state->px[a] = state->xmax - state->r[a];
658 state->vx[a] = -state->vx[a] * state->e;
660 if (state->py[a] <= (state->ymin + state->r[a]))
662 state->py[a] = state->ymin + state->r[a];
663 state->vy[a] = -state->vy[a] * state->e;
665 if (state->py[a] >= (state->ymax - state->r[a]))
667 state->py[a] = state->ymax - state->r[a];
668 state->vy[a] = -state->vy[a] * state->e;
672 /* Apply gravity to all balls.
674 for (a=1; a <= state->count; a++)
675 if (a != state->mouse_ball)
677 state->vx[a] += state->accx * state->tc;
678 state->vy[a] += state->accy * state->tc;
679 state->px[a] += state->vx[a] * state->tc;
680 state->py[a] += state->vy[a] * state->tc;
685 /* Handle X events, specifically, allow a ball to be picked up with the mouse.
688 handle_events (b_state *state)
690 XSync (state->dpy, False);
691 while (XPending (state->dpy))
694 XNextEvent (state->dpy, &event);
695 if (event.xany.type == ButtonPress)
698 float fmx = event.xbutton.x_root;
699 float fmy = event.xbutton.y_root;
700 if (state->mouse_ball != 0) /* second down-click? drop the ball. */
702 state->mouse_ball = 0;
706 for (i=1; i <= state->count; i++)
708 float d = ((state->px[i] - fmx) * (state->px[i] - fmx) +
709 (state->py[i] - fmy) * (state->py[i] - fmy));
710 float r = state->r[i];
713 state->mouse_ball = i;
718 else if (event.xany.type == ButtonRelease) /* drop the ball */
720 state->mouse_ball = 0;
723 else if (event.xany.type == ConfigureNotify)
725 /* This is redundant, since we poll this every iteration. */
726 check_window_moved (state);
729 screenhack_handle_event (state->dpy, &event);
734 char *progclass = "FluidBalls";
736 char *defaults [] = {
737 ".background: black",
738 ".textColor: yellow",
739 ".font: -*-helvetica-*-r-*-*-*-180-*-*-p-*-*-*",
750 "*shakeThreshold: 0.015",
751 "*doubleBuffer: True",
752 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
754 "*useDBEClear: True",
755 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
759 XrmOptionDescRec options [] = {
760 { "-delay", ".delay", XrmoptionSepArg, 0 },
761 { "-count", ".count", XrmoptionSepArg, 0 },
762 { "-size", ".size", XrmoptionSepArg, 0 },
763 { "-count", ".count", XrmoptionSepArg, 0 },
764 { "-gravity", ".gravity", XrmoptionSepArg, 0 },
765 { "-wind", ".wind", XrmoptionSepArg, 0 },
766 { "-friction", ".friction", XrmoptionSepArg, 0 },
767 { "-fps", ".doFPS", XrmoptionNoArg, "True" },
768 { "-no-fps", ".doFPS", XrmoptionNoArg, "False" },
769 { "-shake", ".shake", XrmoptionNoArg, "True" },
770 { "-no-shake", ".shake", XrmoptionNoArg, "False" },
771 { "-random", ".random", XrmoptionNoArg, "True" },
772 { "-nonrandom", ".random", XrmoptionNoArg, "False" },
773 { "-db", ".doubleBuffer", XrmoptionNoArg, "True" },
774 { "-no-db", ".doubleBuffer", XrmoptionNoArg, "False" },
779 screenhack (Display *dpy, Window window)
781 b_state *state = init_balls(dpy, window);
782 int delay = get_integer_resource ("delay", "Integer");
786 repaint_balls(state);
790 handle_events (state);
791 if (delay) usleep (delay);