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"
31 XWindowAttributes xgwa;
33 GC draw_gc; /* most of the balls */
34 GC draw_gc2; /* the ball being dragged with the mouse */
39 int count; /* number of balls */
40 float xmin, ymin; /* rectangle of window, relative to root */
43 int mouse_ball; /* index of ball being dragged, or 0 if none. */
45 float tc; /* time constant (time-warp multiplier) */
46 float accx; /* horizontal acceleration (wind) */
47 float accy; /* vertical acceleration (gravity) */
49 float *vx, *vy; /* current ball velocities */
50 float *px, *py; /* current ball positions */
51 float *opx, *opy; /* previous ball positions */
52 float *r; /* ball radiuses */
54 float *m; /* ball mass, precalculated */
55 float e; /* coefficient of friction, I think? */
56 float max_radius; /* largest radius of any ball */
58 Bool random_sizes_p; /* Whether balls should be various sizes up to max. */
59 Bool shake_p; /* Whether to mess with gravity when things settle. */
60 float shake_threshold;
63 Bool fps_p; /* Whether to draw some text at the bottom. */
74 /* Finds the origin of the window relative to the root window, by
75 walking up the window tree until it reaches the top.
78 window_origin (Display *dpy, Window window, int *x, int *y)
80 Window root, parent, *kids;
82 XWindowAttributes xgwa;
84 XGetWindowAttributes (dpy, window, &xgwa);
93 if (XQueryTree (dpy, window, &root, &parent, &kids, &nkids))
95 if (parent && parent != root)
98 window_origin (dpy, parent, &px, &py);
103 if (kids) XFree (kids);
109 /* Queries the window position to see if the window has moved or resized.
110 We poll this instead of waiting for ConfigureNotify events, because
111 when the window manager moves the window, only one ConfigureNotify
112 comes in: at the end of the motion. If we poll, we can react to the
113 new position while the window is still being moved. (Assuming the WM
114 does OpaqueMove, of course.)
117 check_window_moved (b_state *state)
119 float oxmin = state->xmin;
120 float oymin = state->ymin;
121 float oymax = state->ymax;
123 XGetWindowAttributes (state->dpy, state->window, &state->xgwa);
124 window_origin (state->dpy, state->window, &wx, &wy);
127 state->xmax = state->xmin + state->xgwa.width;
128 state->ymax = state->ymin + state->xgwa.height - state->font_height;
130 /* Only need to erase the window if the origin moved */
131 if (oxmin != state->xmin || oymin != state->ymin)
132 XClearWindow (state->dpy, state->window);
133 else if (state->fps_p && oymax != state->ymax)
134 XFillRectangle (state->dpy, state->window, state->erase_gc,
135 0, state->xgwa.height - state->font_height,
136 state->xgwa.width, state->font_height);
141 /* Returns the position of the mouse relative to the root window.
144 query_mouse (b_state *state, int *x, int *y)
146 Window root1, child1;
147 int mouse_x, mouse_y, root_x, root_y;
149 if (XQueryPointer (state->dpy, state->window, &root1, &child1,
150 &root_x, &root_y, &mouse_x, &mouse_y, &mask))
162 /* Re-pick the colors of the balls, and the mouse-ball.
165 recolor (b_state *state)
168 XFreeColors (state->dpy, state->xgwa.colormap, &state->fg.pixel, 1, 0);
169 if (state->fg2.flags)
170 XFreeColors (state->dpy, state->xgwa.colormap, &state->fg2.pixel, 1, 0);
172 state->fg.flags = DoRed|DoGreen|DoBlue;
173 state->fg.red = 0x8888 + (random() % 0x8888);
174 state->fg.green = 0x8888 + (random() % 0x8888);
175 state->fg.blue = 0x8888 + (random() % 0x8888);
177 state->fg2.flags = DoRed|DoGreen|DoBlue;
178 state->fg2.red = 0x8888 + (random() % 0x8888);
179 state->fg2.green = 0x8888 + (random() % 0x8888);
180 state->fg2.blue = 0x8888 + (random() % 0x8888);
182 if (XAllocColor (state->dpy, state->xgwa.colormap, &state->fg))
183 XSetForeground (state->dpy, state->draw_gc, state->fg.pixel);
185 if (XAllocColor (state->dpy, state->xgwa.colormap, &state->fg2))
186 XSetForeground (state->dpy, state->draw_gc2, state->fg2.pixel);
190 /* Initialize the state structure and various X data.
193 init_balls (Display *dpy, Window window)
197 b_state *state = (b_state *) calloc (1, sizeof(*state));
201 state->window = window;
203 check_window_moved (state);
205 /* Select ButtonRelease events on the external window, if no other app has
206 already selected it (only one app can select it at a time: BadAccess. */
207 if (! (state->xgwa.all_event_masks & ButtonReleaseMask))
208 XSelectInput (state->dpy, state->window,
209 state->xgwa.your_event_mask | ButtonReleaseMask);
211 gcv.foreground = get_pixel_resource("foreground", "Foreground",
212 state->dpy, state->xgwa.colormap);
213 gcv.background = get_pixel_resource("background", "Background",
214 state->dpy, state->xgwa.colormap);
215 state->draw_gc = XCreateGC (state->dpy, state->window,
216 GCForeground|GCBackground, &gcv);
218 gcv.foreground = get_pixel_resource("mouseForeground", "MouseForeground",
219 state->dpy, state->xgwa.colormap);
220 state->draw_gc2 = XCreateGC (state->dpy, state->window,
221 GCForeground|GCBackground, &gcv);
223 gcv.foreground = gcv.background;
224 state->erase_gc = XCreateGC (state->dpy, state->window,
225 GCForeground|GCBackground, &gcv);
229 extx = state->xmax - state->xmin;
230 exty = state->ymax - state->ymin;
232 state->count = get_integer_resource ("count", "Count");
233 if (state->count < 1) state->count = 20;
235 state->max_radius = get_float_resource ("size", "Size") / 2;
236 if (state->max_radius < 1.0) state->max_radius = 1.0;
238 state->random_sizes_p = get_boolean_resource ("random", "Random");
240 state->accx = get_float_resource ("wind", "Wind");
241 if (state->accx < -1.0 || state->accx > 1.0) state->accx = 0;
243 state->accy = get_float_resource ("gravity", "Gravity");
244 if (state->accy < -1.0 || state->accy > 1.0) state->accy = 0.01;
246 state->e = get_float_resource ("friction", "Friction");
247 if (state->e < 0.2 || state->e > 1.0) state->e = 0.97;
249 state->tc = get_float_resource ("timeScale", "TimeScale");
250 if (state->tc <= 0 || state->tc > 10) state->tc = 1.0;
252 state->shake_p = get_boolean_resource ("shake", "Shake");
253 state->shake_threshold = get_float_resource ("shakeThreshold",
256 state->fps_p = get_boolean_resource ("doFPS", "DoFPS");
260 char *fontname = get_string_resource ("font", "Font");
261 const char *def_font = "fixed";
262 if (!fontname || !*fontname) fontname = (char *)def_font;
263 font = XLoadQueryFont (dpy, fontname);
264 if (!font) font = XLoadQueryFont (dpy, def_font);
266 gcv.font = font->fid;
267 gcv.foreground = get_pixel_resource("textColor", "Foreground",
268 state->dpy, state->xgwa.colormap);
269 state->font_gc = XCreateGC(dpy, window,
270 GCFont|GCForeground|GCBackground, &gcv);
271 state->font_height = font->ascent + font->descent;
272 state->font_baseline = font->descent;
275 state->m = (float *) malloc (sizeof (*state->m) * (state->count + 1));
276 state->r = (float *) malloc (sizeof (*state->r) * (state->count + 1));
277 state->vx = (float *) malloc (sizeof (*state->vx) * (state->count + 1));
278 state->vy = (float *) malloc (sizeof (*state->vy) * (state->count + 1));
279 state->px = (float *) malloc (sizeof (*state->px) * (state->count + 1));
280 state->py = (float *) malloc (sizeof (*state->py) * (state->count + 1));
281 state->opx = (float *) malloc (sizeof (*state->opx) * (state->count + 1));
282 state->opy = (float *) malloc (sizeof (*state->opy) * (state->count + 1));
284 for (i=1; i<=state->count; i++)
286 state->px[i] = frand(extx) + state->xmin;
287 state->py[i] = frand(exty) + state->ymin;
288 state->vx[i] = frand(0.2) - 0.1;
289 state->vy[i] = frand(0.2) - 0.1;
291 state->r[i] = (state->random_sizes_p
292 ? ((0.2 + frand(0.8)) * state->max_radius)
293 : state->max_radius);
294 /*state->r[i] = pow(frand(1.0), state->sizegamma) * state->max_radius;*/
296 /* state->m[i] = pow(state->r[i],2) * M_PI; */
297 state->m[i] = pow(state->r[i],3) * M_PI * 1.3333;
304 /* Messes with gravity: permute "down" to be in a random direction.
307 shake (b_state *state)
309 float a = state->accx;
310 float b = state->accy;
311 int i = random() % 4;
336 state->time_since_shake = 0;
341 /* Look at the current time, and update state->time_since_shake.
342 Draw the FPS display if desired.
345 check_wall_clock (b_state *state, float max_d)
348 state->frame_count++;
350 if (tick++ > 20) /* don't call gettimeofday() too often -- it's slow. */
352 static struct timeval last = { 0, };
354 # ifdef GETTIMEOFDAY_TWO_ARGS
356 gettimeofday(&now, &tzp);
361 if (last.tv_sec == 0)
365 if (now.tv_sec == last.tv_sec)
368 state->time_since_shake += (now.tv_sec - last.tv_sec);
372 static char buf[1024];
373 float elapsed = ((now.tv_sec + (now.tv_usec / 1000000.0)) -
374 (last.tv_sec + (last.tv_usec / 1000000.0)));
375 float fps = state->frame_count / elapsed;
376 float cps = state->collision_count / elapsed;
378 sprintf (buf, " FPS: %.2f Collisions: %.f/frame Max motion: %.3f",
379 fps, cps/fps, max_d);
381 XFillRectangle (state->dpy, state->window, state->erase_gc,
382 0, state->xgwa.height - state->font_height,
383 state->xgwa.width, state->font_height);
384 XDrawImageString (state->dpy, state->window, state->font_gc,
385 0, state->xgwa.height - state->font_baseline,
389 state->frame_count = 0;
390 state->collision_count = 0;
396 /* Erases the balls at their previous positions, and draws the new ones.
399 repaint_balls (b_state *state)
402 int x1a, x2a, y1a, y2a;
403 int x1b, x2b, y1b, y2b;
406 for (a=1; a <= state->count; a++)
409 x1a = (state->opx[a] - state->r[a] - state->xmin);
410 y1a = (state->opy[a] - state->r[a] - state->ymin);
411 x2a = (state->opx[a] + state->r[a] - state->xmin);
412 y2a = (state->opy[a] + state->r[a] - state->ymin);
414 x1b = (state->px[a] - state->r[a] - state->xmin);
415 y1b = (state->py[a] - state->r[a] - state->ymin);
416 x2b = (state->px[a] + state->r[a] - state->xmin);
417 y2b = (state->py[a] + state->r[a] - state->ymin);
419 /* if (x1a != x1b || y1a != y1b) -- leaves turds if we optimize this */
421 gc = state->erase_gc;
422 XFillArc (state->dpy, state->window, gc,
423 x1a, y1a, x2a-x1a, y2a-y1a,
427 if (state->mouse_ball == a)
428 gc = state->draw_gc2;
432 XFillArc (state->dpy, state->window, gc,
433 x1b, y1b, x2b-x1b, y2b-y1b,
438 /* distance this ball moved this frame */
439 float d = ((state->px[a] - state->opx[a]) *
440 (state->px[a] - state->opx[a]) +
441 (state->py[a] - state->opy[a]) *
442 (state->py[a] - state->opy[a]));
443 if (d > max_d) max_d = d;
446 state->opx[a] = state->px[a];
447 state->opy[a] = state->py[a];
450 if (state->shake_p && state->time_since_shake > 5)
452 max_d /= state->max_radius;
453 if (max_d < state->shake_threshold || /* when its stable */
454 state->time_since_shake > 30) /* or when 30 secs has passed */
460 check_wall_clock (state, max_d);
464 /* Implements the laws of physics: move balls to their new positions.
467 update_balls (b_state *state)
470 float d, vxa, vya, vxb, vyb, dd, cdx, cdy;
471 float ma, mb, vela, velb, vela1, velb1;
474 check_window_moved (state);
476 /* If we're currently tracking the mouse, update that ball first.
478 if (state->mouse_ball != 0)
480 int mouse_x, mouse_y;
481 query_mouse (state, &mouse_x, &mouse_y);
482 state->px[state->mouse_ball] = mouse_x;
483 state->py[state->mouse_ball] = mouse_y;
484 state->vx[state->mouse_ball] =
486 (state->px[state->mouse_ball] - state->opx[state->mouse_ball]) *
488 state->vy[state->mouse_ball] =
490 (state->py[state->mouse_ball] - state->opy[state->mouse_ball]) *
494 /* For each ball, compute the influence of every other ball. */
495 for (a=1; a <= state->count; a++)
496 if (a != state->mouse_ball)
497 for (b=1; b <= state->count; b++)
500 d = ((state->px[a] - state->px[b]) *
501 (state->px[a] - state->px[b]) +
502 (state->py[a] - state->py[b]) *
503 (state->py[a] - state->py[b]));
504 dee2 = (state->r[a] + state->r[b]) *
505 (state->r[a] + state->r[b]);
508 state->collision_count++;
510 dd = state->r[a] + state->r[b] - d;
511 /* A pair of balls that have already collided in this
512 * current frame (and therefore touching each other)
513 * should not have another collision calculated, hence
514 * the fallthru if "dd ~= 0.0".
516 if ((dd < -0.01) || (dd > 0.01))
518 cdx = (state->px[b] - state->px[a]) / d;
519 cdy = (state->py[b] - state->py[a]) / d;
521 /* Move each ball apart from the other by half the
522 * 'collision' distance.
524 state->px[a] -= 0.5 * dd * cdx;
525 state->py[a] -= 0.5 * dd * cdy;
526 state->px[b] += 0.5 * dd * cdx;
527 state->py[b] += 0.5 * dd * cdy;
536 vela = sqrt((vxa * vxa) + (vya * vya));
537 velb = sqrt((vxb * vxb) + (vyb * vyb));
539 vela1 = vela * ((ma - mb) / (ma + mb)) +
540 velb * ((2 * mb) / (ma + mb));
541 velb1 = vela * ((2 * ma) / (ma + mb)) +
542 velb * ((mb - ma) / (ma + mb));
544 vela1 *= state->e; /* "air resistance" */
547 vela1 += (frand (50) - 25) / ma; /* brownian motion */
548 velb1 += (frand (50) - 25) / mb;
550 state->vx[a] = -cdx * vela1;
551 state->vy[a] = -cdy * vela1;
552 state->vx[b] = cdx * velb1;
553 state->vy[b] = cdy * velb1;
558 /* Force all balls to be on screen.
560 for (a=1; a <= state->count; a++)
562 if (state->px[a] <= (state->xmin + state->r[a]))
564 state->px[a] = state->xmin + state->r[a];
565 state->vx[a] = -state->vx[a] * state->e;
567 if (state->px[a] >= (state->xmax - state->r[a]))
569 state->px[a] = state->xmax - state->r[a];
570 state->vx[a] = -state->vx[a] * state->e;
572 if (state->py[a] <= (state->ymin + state->r[a]))
574 state->py[a] = state->ymin + state->r[a];
575 state->vy[a] = -state->vy[a] * state->e;
577 if (state->py[a] >= (state->ymax - state->r[a]))
579 state->py[a] = state->ymax - state->r[a];
580 state->vy[a] = -state->vy[a] * state->e;
584 /* Apply gravity to all balls.
586 for (a=1; a <= state->count; a++)
587 if (a != state->mouse_ball)
589 state->vx[a] += state->accx * state->tc;
590 state->vy[a] += state->accy * state->tc;
591 state->px[a] += state->vx[a] * state->tc;
592 state->py[a] += state->vy[a] * state->tc;
597 /* Handle X events, specifically, allow a ball to be picked up with the mouse.
600 handle_events (b_state *state)
602 XSync (state->dpy, False);
603 while (XPending (state->dpy))
606 XNextEvent (state->dpy, &event);
607 if (event.xany.type == ButtonPress)
610 float fmx = event.xbutton.x_root;
611 float fmy = event.xbutton.y_root;
612 if (state->mouse_ball != 0) /* second down-click? drop the ball. */
614 state->mouse_ball = 0;
618 for (i=1; i <= state->count; i++)
620 float d = ((state->px[i] - fmx) * (state->px[i] - fmx) +
621 (state->py[i] - fmy) * (state->py[i] - fmy));
622 float r = state->r[i];
625 state->mouse_ball = i;
630 else if (event.xany.type == ButtonRelease) /* drop the ball */
632 state->mouse_ball = 0;
635 else if (event.xany.type == ConfigureNotify)
637 /* This is redundant, since we poll this every iteration. */
638 check_window_moved (state);
641 screenhack_handle_event (state->dpy, &event);
646 char *progclass = "FluidBalls";
648 char *defaults [] = {
649 ".background: black",
650 ".textColor: yellow",
651 ".font: -*-helvetica-*-r-*-*-*-180-*-*-p-*-*-*",
662 "*shakeThreshold: 0.015",
666 XrmOptionDescRec options [] = {
667 { "-delay", ".delay", XrmoptionSepArg, 0 },
668 { "-count", ".count", XrmoptionSepArg, 0 },
669 { "-size", ".size", XrmoptionSepArg, 0 },
670 { "-count", ".count", XrmoptionSepArg, 0 },
671 { "-gravity", ".gravity", XrmoptionSepArg, 0 },
672 { "-wind", ".wind", XrmoptionSepArg, 0 },
673 { "-friction", ".friction", XrmoptionSepArg, 0 },
674 { "-fps", ".doFPS", XrmoptionNoArg, "True" },
675 { "-no-fps", ".doFPS", XrmoptionNoArg, "False" },
676 { "-shake", ".shake", XrmoptionNoArg, "True" },
677 { "-no-shake", ".shake", XrmoptionNoArg, "False" },
678 { "-random", ".random", XrmoptionNoArg, "True" },
679 { "-nonrandom", ".random", XrmoptionNoArg, "False" },
684 screenhack (Display *dpy, Window window)
686 b_state *state = init_balls(dpy, window);
687 int delay = get_integer_resource ("delay", "Integer");
691 repaint_balls(state);
695 handle_events (state);
696 if (delay) usleep (delay);