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*3 - 20,
98 state->xgwa.width, state->font_height*3 + 20);
99 XDrawImageString (state->dpy, state->b, state->font_gc,
100 10, state->xgwa.height - state->font_height*2 -
101 state->font_baseline - 10,
102 state->fps_str, strlen(state->fps_str));
105 /* Finds the origin of the window relative to the root window, by
106 walking up the window tree until it reaches the top.
109 window_origin (Display *dpy, Window window, int *x, int *y)
111 XTranslateCoordinates (dpy, window, RootWindow (dpy, DefaultScreen (dpy)),
112 0, 0, x, y, &window);
116 /* Queries the window position to see if the window has moved or resized.
117 We poll this instead of waiting for ConfigureNotify events, because
118 when the window manager moves the window, only one ConfigureNotify
119 comes in: at the end of the motion. If we poll, we can react to the
120 new position while the window is still being moved. (Assuming the WM
121 does OpaqueMove, of course.)
124 check_window_moved (b_state *state)
126 float oxmin = state->xmin;
127 float oxmax = state->xmax;
128 float oymin = state->ymin;
129 float oymax = state->ymax;
131 XGetWindowAttributes (state->dpy, state->window, &state->xgwa);
132 window_origin (state->dpy, state->window, &wx, &wy);
135 state->xmax = state->xmin + state->xgwa.width;
136 state->ymax = state->ymin + state->xgwa.height - (state->font_height*3) -
137 (state->font_height ? 22 : 0);
139 if (state->dbuf && (state->ba))
141 if (oxmax != state->xmax || oymax != state->ymax)
143 XFreePixmap (state->dpy, state->ba);
144 state->ba = XCreatePixmap (state->dpy, state->window,
145 state->xgwa.width, state->xgwa.height,
147 XFillRectangle (state->dpy, state->ba, state->erase_gc, 0, 0,
148 state->xgwa.width, state->xgwa.height);
149 state->b = state->ba;
154 /* Only need to erase the window if the origin moved */
155 if (oxmin != state->xmin || oymin != state->ymin)
156 XClearWindow (state->dpy, state->window);
157 else if (state->fps_p && oymax != state->ymax)
158 XFillRectangle (state->dpy, state->b, state->erase_gc,
159 0, state->xgwa.height - state->font_height*3,
160 state->xgwa.width, state->font_height*3);
165 /* Returns the position of the mouse relative to the root window.
168 query_mouse (b_state *state, int *x, int *y)
170 Window root1, child1;
171 int mouse_x, mouse_y, root_x, root_y;
173 if (XQueryPointer (state->dpy, state->window, &root1, &child1,
174 &root_x, &root_y, &mouse_x, &mouse_y, &mask))
186 /* Re-pick the colors of the balls, and the mouse-ball.
189 recolor (b_state *state)
192 XFreeColors (state->dpy, state->xgwa.colormap, &state->fg.pixel, 1, 0);
193 if (state->fg2.flags)
194 XFreeColors (state->dpy, state->xgwa.colormap, &state->fg2.pixel, 1, 0);
196 state->fg.flags = DoRed|DoGreen|DoBlue;
197 state->fg.red = 0x8888 + (random() % 0x8888);
198 state->fg.green = 0x8888 + (random() % 0x8888);
199 state->fg.blue = 0x8888 + (random() % 0x8888);
201 state->fg2.flags = DoRed|DoGreen|DoBlue;
202 state->fg2.red = 0x8888 + (random() % 0x8888);
203 state->fg2.green = 0x8888 + (random() % 0x8888);
204 state->fg2.blue = 0x8888 + (random() % 0x8888);
206 if (XAllocColor (state->dpy, state->xgwa.colormap, &state->fg))
207 XSetForeground (state->dpy, state->draw_gc, state->fg.pixel);
209 if (XAllocColor (state->dpy, state->xgwa.colormap, &state->fg2))
210 XSetForeground (state->dpy, state->draw_gc2, state->fg2.pixel);
213 /* Initialize the state structure and various X data.
216 fluidballs_init (Display *dpy, Window window)
220 b_state *state = (b_state *) calloc (1, sizeof(*state));
224 state->window = window;
225 state->delay = get_integer_resource (dpy, "delay", "Integer");
227 check_window_moved (state);
229 state->dbuf = get_boolean_resource (dpy, "doubleBuffer", "Boolean");
231 # ifdef HAVE_JWXYZ /* Don't second-guess Quartz's double-buffering */
237 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
238 state->dbeclear_p = get_boolean_resource (dpy, "useDBEClear", "Boolean");
239 if (state->dbeclear_p)
240 state->b = xdbe_get_backbuffer (dpy, window, XdbeBackground);
242 state->b = xdbe_get_backbuffer (dpy, window, XdbeUndefined);
243 state->backb = state->b;
244 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
248 state->ba = XCreatePixmap (state->dpy, state->window,
249 state->xgwa.width, state->xgwa.height,
251 state->b = state->ba;
256 state->b = state->window;
259 /* Select ButtonRelease events on the external window, if no other app has
260 already selected it (only one app can select it at a time: BadAccess. */
262 if (! (state->xgwa.all_event_masks & ButtonReleaseMask))
263 XSelectInput (state->dpy, state->window,
264 state->xgwa.your_event_mask | ButtonReleaseMask);
267 gcv.foreground = get_pixel_resource(state->dpy, state->xgwa.colormap,
268 "foreground", "Foreground");
269 gcv.background = get_pixel_resource(state->dpy, state->xgwa.colormap,
270 "background", "Background");
271 state->draw_gc = XCreateGC (state->dpy, state->b,
272 GCForeground|GCBackground, &gcv);
274 gcv.foreground = get_pixel_resource(state->dpy, state->xgwa.colormap,
275 "mouseForeground", "MouseForeground");
276 state->draw_gc2 = XCreateGC (state->dpy, state->b,
277 GCForeground|GCBackground, &gcv);
279 gcv.foreground = gcv.background;
280 state->erase_gc = XCreateGC (state->dpy, state->b,
281 GCForeground|GCBackground, &gcv);
285 XFillRectangle (state->dpy, state->ba, state->erase_gc, 0, 0,
286 state->xgwa.width, state->xgwa.height);
290 extx = state->xmax - state->xmin;
291 exty = state->ymax - state->ymin;
293 state->count = get_integer_resource (dpy, "count", "Count");
294 if (state->count < 1) state->count = 20;
296 state->max_radius = get_float_resource (dpy, "size", "Size") / 2;
297 if (state->max_radius < 1.0) state->max_radius = 1.0;
299 if (state->xgwa.width > 2560) state->max_radius *= 2; /* Retina displays */
301 if (state->xgwa.width < 100 || state->xgwa.height < 100) /* tiny window */
303 if (state->max_radius > 5)
304 state->max_radius = 5;
307 state->random_sizes_p = get_boolean_resource (dpy, "random", "Random");
309 /* If the initial window size is too small to hold all these balls,
310 make fewer of them...
313 float r = (state->random_sizes_p
314 ? state->max_radius * 0.7
315 : state->max_radius);
316 float ball_area = M_PI * r * r;
317 float balls_area = state->count * ball_area;
318 float window_area = state->xgwa.width * state->xgwa.height;
319 window_area *= 0.75; /* don't pack it completely full */
320 if (balls_area > window_area)
321 state->count = window_area / ball_area;
324 state->accx = get_float_resource (dpy, "wind", "Wind");
325 if (state->accx < -1.0 || state->accx > 1.0) state->accx = 0;
327 state->accy = get_float_resource (dpy, "gravity", "Gravity");
328 if (state->accy < -1.0 || state->accy > 1.0) state->accy = 0.01;
330 state->e = get_float_resource (dpy, "elasticity", "Elacitcity");
331 if (state->e < 0.2 || state->e > 1.0) state->e = 0.97;
333 state->tc = get_float_resource (dpy, "timeScale", "TimeScale");
334 if (state->tc <= 0 || state->tc > 10) state->tc = 1.0;
336 state->shake_p = get_boolean_resource (dpy, "shake", "Shake");
337 state->shake_threshold = get_float_resource (dpy, "shakeThreshold",
339 state->time_tick = 999999;
341 # ifdef HAVE_MOBILE /* Always obey real-world gravity */
342 state->shake_p = False;
346 state->fps_p = get_boolean_resource (dpy, "doFPS", "DoFPS");
350 char *fontname = get_string_resource (dpy, "fpsFont", "Font");
351 if (!fontname) fontname = "-*-courier-bold-r-normal-*-180-*";
352 font = load_font_retry (dpy, fontname);
354 gcv.font = font->fid;
355 gcv.foreground = get_pixel_resource(state->dpy, state->xgwa.colormap,
356 "textColor", "Foreground");
357 state->font_gc = XCreateGC(dpy, state->b,
358 GCFont|GCForeground|GCBackground, &gcv);
359 state->font_height = font->ascent + font->descent;
360 state->font_baseline = font->descent;
363 state->m = (float *) malloc (sizeof (*state->m) * (state->count + 1));
364 state->r = (float *) malloc (sizeof (*state->r) * (state->count + 1));
365 state->vx = (float *) malloc (sizeof (*state->vx) * (state->count + 1));
366 state->vy = (float *) malloc (sizeof (*state->vy) * (state->count + 1));
367 state->px = (float *) malloc (sizeof (*state->px) * (state->count + 1));
368 state->py = (float *) malloc (sizeof (*state->py) * (state->count + 1));
369 state->opx = (float *) malloc (sizeof (*state->opx) * (state->count + 1));
370 state->opy = (float *) malloc (sizeof (*state->opy) * (state->count + 1));
372 for (i=1; i<=state->count; i++)
374 state->px[i] = frand(extx) + state->xmin;
375 state->py[i] = frand(exty) + state->ymin;
376 state->vx[i] = frand(0.2) - 0.1;
377 state->vy[i] = frand(0.2) - 0.1;
379 state->r[i] = (state->random_sizes_p
380 ? ((0.2 + frand(0.8)) * state->max_radius)
381 : state->max_radius);
382 /*state->r[i] = pow(frand(1.0), state->sizegamma) * state->max_radius;*/
384 /* state->m[i] = pow(state->r[i],2) * M_PI; */
385 state->m[i] = pow(state->r[i],3) * M_PI * 1.3333;
388 memcpy (state->opx, state->px, sizeof (*state->opx) * (state->count + 1));
389 memcpy (state->opy, state->py, sizeof (*state->opx) * (state->count + 1));
395 /* Messes with gravity: permute "down" to be in a random direction.
398 shake (b_state *state)
400 float a = state->accx;
401 float b = state->accy;
402 int i = random() % 4;
427 state->time_since_shake = 0;
432 /* Look at the current time, and update state->time_since_shake.
433 Draw the FPS display if desired.
436 check_wall_clock (b_state *state, float max_d)
438 state->frame_count++;
440 if (state->time_tick++ > 20) /* don't call gettimeofday() too often -- it's slow. */
443 # ifdef GETTIMEOFDAY_TWO_ARGS
445 gettimeofday(&now, &tzp);
450 if (state->last_time.tv_sec == 0)
451 state->last_time = now;
453 state->time_tick = 0;
454 if (now.tv_sec == state->last_time.tv_sec)
457 state->time_since_shake += (now.tv_sec - state->last_time.tv_sec);
459 # ifdef HAVE_MOBILE /* Always obey real-world gravity */
461 float a = fabs (fabs(state->accx) > fabs(state->accy)
462 ? state->accx : state->accy);
463 int rot = current_device_rotation();
465 case 0: case 360: state->accx = 0; state->accy = a; break;
466 case -90: state->accx = -a; state->accy = 0; break;
467 case 90: state->accx = a; state->accy = 0; break;
468 case 180: case -180: state->accx = 0; state->accy = -a; break;
472 # endif /* HAVE_MOBILE */
476 float elapsed = ((now.tv_sec + (now.tv_usec / 1000000.0)) -
477 (state->last_time.tv_sec + (state->last_time.tv_usec / 1000000.0)));
478 float fps = state->frame_count / elapsed;
479 float cps = state->collision_count / elapsed;
481 sprintf (state->fps_str, "Collisions: %.3f/frame Max motion: %.3f",
484 draw_fps_string(state);
487 state->frame_count = 0;
488 state->collision_count = 0;
489 state->last_time = now;
493 /* Erases the balls at their previous positions, and draws the new ones.
496 repaint_balls (b_state *state)
500 int x1a, x2a, y1a, y2a;
502 int x1b, x2b, y1b, y2b;
505 #ifdef HAVE_JWXYZ /* Don't second-guess Quartz's double-buffering */
506 XClearWindow (state->dpy, state->b);
509 for (a=1; a <= state->count; a++)
513 x1a = (state->opx[a] - state->r[a] - state->xmin);
514 y1a = (state->opy[a] - state->r[a] - state->ymin);
515 x2a = (state->opx[a] + state->r[a] - state->xmin);
516 y2a = (state->opy[a] + state->r[a] - state->ymin);
519 x1b = (state->px[a] - state->r[a] - state->xmin);
520 y1b = (state->py[a] - state->r[a] - state->ymin);
521 x2b = (state->px[a] + state->r[a] - state->xmin);
522 y2b = (state->py[a] + state->r[a] - state->ymin);
524 #ifndef HAVE_JWXYZ /* Don't second-guess Quartz's double-buffering */
525 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
526 if (!state->dbeclear_p || !state->backb)
527 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
529 /* if (x1a != x1b || y1a != y1b) -- leaves turds if we optimize this */
531 gc = state->erase_gc;
532 XFillArc (state->dpy, state->b, gc,
533 x1a, y1a, x2a-x1a, y2a-y1a,
537 #endif /* !HAVE_JWXYZ */
539 if (state->mouse_ball == a)
540 gc = state->draw_gc2;
544 XFillArc (state->dpy, state->b, gc,
545 x1b, y1b, x2b-x1b, y2b-y1b,
550 /* distance this ball moved this frame */
551 float d = ((state->px[a] - state->opx[a]) *
552 (state->px[a] - state->opx[a]) +
553 (state->py[a] - state->opy[a]) *
554 (state->py[a] - state->opy[a]));
555 if (d > max_d) max_d = d;
558 state->opx[a] = state->px[a];
559 state->opy[a] = state->py[a];
563 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
564 && (state->backb ? state->dbeclear_p : 1)
565 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
567 draw_fps_string(state);
569 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
572 XdbeSwapInfo info[1];
573 info[0].swap_window = state->window;
574 info[0].swap_action = (state->dbeclear_p ? XdbeBackground : XdbeUndefined);
575 XdbeSwapBuffers (state->dpy, info, 1);
578 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
581 XCopyArea (state->dpy, state->b, state->window, state->erase_gc,
582 0, 0, state->xgwa.width, state->xgwa.height, 0, 0);
585 if (state->shake_p && state->time_since_shake > 5)
587 max_d /= state->max_radius;
588 if (max_d < state->shake_threshold || /* when its stable */
589 state->time_since_shake > 30) /* or when 30 secs has passed */
595 check_wall_clock (state, max_d);
599 /* Implements the laws of physics: move balls to their new positions.
602 update_balls (b_state *state)
605 float d, vxa, vya, vxb, vyb, dd, cdx, cdy;
606 float ma, mb, vca, vcb, dva, dvb;
609 check_window_moved (state);
611 /* If we're currently tracking the mouse, update that ball first.
613 if (state->mouse_ball != 0)
615 int mouse_x, mouse_y;
616 query_mouse (state, &mouse_x, &mouse_y);
617 state->px[state->mouse_ball] = mouse_x;
618 state->py[state->mouse_ball] = mouse_y;
619 state->vx[state->mouse_ball] =
621 (state->px[state->mouse_ball] - state->opx[state->mouse_ball]) *
623 state->vy[state->mouse_ball] =
625 (state->py[state->mouse_ball] - state->opy[state->mouse_ball]) *
629 /* For each ball, compute the influence of every other ball. */
630 for (a=1; a <= state->count - 1; a++)
631 for (b=a + 1; b <= state->count; b++)
633 d = ((state->px[a] - state->px[b]) *
634 (state->px[a] - state->px[b]) +
635 (state->py[a] - state->py[b]) *
636 (state->py[a] - state->py[b]));
637 dee2 = (state->r[a] + state->r[b]) *
638 (state->r[a] + state->r[b]);
641 state->collision_count++;
643 dd = state->r[a] + state->r[b] - d;
645 cdx = (state->px[b] - state->px[a]) / d;
646 cdy = (state->py[b] - state->py[a]) / d;
648 /* Move each ball apart from the other by half the
649 * 'collision' distance.
651 state->px[a] -= 0.5 * dd * cdx;
652 state->py[a] -= 0.5 * dd * cdy;
653 state->px[b] += 0.5 * dd * cdx;
654 state->py[b] += 0.5 * dd * cdy;
664 vca = vxa * cdx + vya * cdy; /* the component of each velocity */
665 vcb = vxb * cdx + vyb * cdy; /* along the axis of the collision */
667 /* elastic collison */
668 dva = (vca * (ma - mb) + vcb * 2 * mb) / (ma + mb) - vca;
669 dvb = (vcb * (mb - ma) + vca * 2 * ma) / (ma + mb) - vcb;
671 dva *= state->e; /* some energy lost to inelasticity */
675 dva += (frand (50) - 25) / ma; /* q: why are elves so chaotic? */
676 dvb += (frand (50) - 25) / mb; /* a: brownian motion. */
691 /* Force all balls to be on screen.
693 for (a=1; a <= state->count; a++)
695 if (state->px[a] <= (state->xmin + state->r[a]))
697 state->px[a] = state->xmin + state->r[a];
698 state->vx[a] = -state->vx[a] * state->e;
700 if (state->px[a] >= (state->xmax - state->r[a]))
702 state->px[a] = state->xmax - state->r[a];
703 state->vx[a] = -state->vx[a] * state->e;
705 if (state->py[a] <= (state->ymin + state->r[a]))
707 state->py[a] = state->ymin + state->r[a];
708 state->vy[a] = -state->vy[a] * state->e;
710 if (state->py[a] >= (state->ymax - state->r[a]))
712 state->py[a] = state->ymax - state->r[a];
713 state->vy[a] = -state->vy[a] * state->e;
717 /* Apply gravity to all balls.
719 for (a=1; a <= state->count; a++)
720 if (a != state->mouse_ball)
722 state->vx[a] += state->accx * state->tc;
723 state->vy[a] += state->accy * state->tc;
724 state->px[a] += state->vx[a] * state->tc;
725 state->py[a] += state->vy[a] * state->tc;
730 /* Handle X events, specifically, allow a ball to be picked up with the mouse.
733 fluidballs_event (Display *dpy, Window window, void *closure, XEvent *event)
735 b_state *state = (b_state *) closure;
737 if (event->xany.type == ButtonPress)
740 XTranslateCoordinates (dpy, window, RootWindow (dpy, DefaultScreen(dpy)),
741 event->xbutton.x, event->xbutton.y, &rx, &ry,
744 if (state->mouse_ball != 0) /* second down-click? drop the ball. */
746 state->mouse_ball = 0;
751 /* When trying to pick up a ball, first look for a click directly
752 inside the ball; but if we don't find it, expand the radius
753 outward until we find something nearby.
755 float max = state->max_radius * 4;
756 float step = max / 10;
758 for (r2 = step; r2 < max; r2 += step) {
759 for (i = 1; i <= state->count; i++)
761 float d = ((state->px[i] - rx) * (state->px[i] - rx) +
762 (state->py[i] - ry) * (state->py[i] - ry));
763 float r = state->r[i];
767 state->mouse_ball = i;
775 else if (event->xany.type == ButtonRelease) /* drop the ball */
777 state->mouse_ball = 0;
785 fluidballs_draw (Display *dpy, Window window, void *closure)
787 b_state *state = (b_state *) closure;
788 repaint_balls(state);
794 fluidballs_reshape (Display *dpy, Window window, void *closure,
795 unsigned int w, unsigned int h)
800 fluidballs_free (Display *dpy, Window window, void *closure)
802 b_state *state = (b_state *) closure;
807 static const char *fluidballs_defaults [] = {
808 ".background: black",
809 ".foreground: yellow",
810 ".textColor: yellow",
811 "*mouseForeground: white",
821 "*shakeThreshold: 0.015",
822 "*doubleBuffer: True",
823 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
825 "*useDBEClear: True",
826 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
828 "*ignoreRotation: True",
833 static XrmOptionDescRec fluidballs_options [] = {
834 { "-delay", ".delay", XrmoptionSepArg, 0 },
835 { "-count", ".count", XrmoptionSepArg, 0 },
836 { "-size", ".size", XrmoptionSepArg, 0 },
837 { "-count", ".count", XrmoptionSepArg, 0 },
838 { "-gravity", ".gravity", XrmoptionSepArg, 0 },
839 { "-wind", ".wind", XrmoptionSepArg, 0 },
840 { "-elasticity", ".elasticity", XrmoptionSepArg, 0 },
841 { "-shake", ".shake", XrmoptionNoArg, "True" },
842 { "-no-shake", ".shake", XrmoptionNoArg, "False" },
843 { "-random", ".random", XrmoptionNoArg, "True" },
844 { "-no-random", ".random", XrmoptionNoArg, "False" },
845 { "-nonrandom", ".random", XrmoptionNoArg, "False" },
846 { "-db", ".doubleBuffer", XrmoptionNoArg, "True" },
847 { "-no-db", ".doubleBuffer", XrmoptionNoArg, "False" },
852 XSCREENSAVER_MODULE ("FluidBalls", fluidballs)