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/
17 #include "screenhack.h"
24 XWindowAttributes xgwa;
26 GC draw_gc; /* most of the balls */
27 GC draw_gc2; /* the ball being dragged with the mouse */
32 int count; /* number of balls */
33 float xmin, ymin; /* rectangle of window, relative to root */
36 int mouse_ball; /* index of ball being dragged, or 0 if none. */
38 float tc; /* time constant (time-warp multiplier) */
39 float accx; /* horizontal acceleration (wind) */
40 float accy; /* vertical acceleration (gravity) */
42 float *vx, *vy; /* current ball velocities */
43 float *px, *py; /* current ball positions */
44 float *opx, *opy; /* previous ball positions */
45 float *r; /* ball radiuses */
47 float e; /* coefficient of friction, I think? */
48 float max_radius; /* largest radius of any ball */
50 Bool shake_p; /* Whether to mess with gravity when things settle. */
51 float shake_threshold;
54 Bool fps_p; /* Whether to draw some text at the bottom. */
65 /* Finds the origin of the window relative to the root window, by
66 walking up the window tree until it reaches the top.
69 window_origin (Display *dpy, Window window, int *x, int *y)
71 Window root, parent, *kids;
73 XWindowAttributes xgwa;
75 XGetWindowAttributes (dpy, window, &xgwa);
84 if (XQueryTree (dpy, window, &root, &parent, &kids, &nkids))
86 if (parent && parent != root)
89 window_origin (dpy, parent, &px, &py);
94 if (kids) XFree (kids);
100 /* Queries the window position to see if the window has moved or resized.
101 We poll this instead of waiting for ConfigureNotify events, because
102 when the window manager moves the window, only one ConfigureNotify
103 comes in: at the end of the motion. If we poll, we can react to the
104 new position while the window is still being moved. (Assuming the WM
105 does OpaqueMove, of course.)
108 check_window_moved (b_state *state)
110 float oxmin = state->xmin;
111 float oymin = state->ymin;
112 float oymax = state->ymax;
114 XGetWindowAttributes (state->dpy, state->window, &state->xgwa);
115 window_origin (state->dpy, state->window, &wx, &wy);
118 state->xmax = state->xmin + state->xgwa.width;
119 state->ymax = state->ymin + state->xgwa.height - state->font_height;
121 /* Only need to erase the window if the origin moved */
122 if (oxmin != state->xmin || oymin != state->ymin)
123 XClearWindow (state->dpy, state->window);
124 else if (state->fps_p && oymax != state->ymax)
125 XFillRectangle (state->dpy, state->window, state->erase_gc,
126 0, state->xgwa.height - state->font_height,
127 state->xgwa.width, state->font_height);
132 /* Returns the position of the mouse relative to the root window.
135 query_mouse (b_state *state, int *x, int *y)
137 Window root1, child1;
138 int mouse_x, mouse_y, root_x, root_y;
140 if (XQueryPointer (state->dpy, state->window, &root1, &child1,
141 &root_x, &root_y, &mouse_x, &mouse_y, &mask))
153 /* Re-pick the colors of the balls, and the mouse-ball.
156 recolor (b_state *state)
159 XFreeColors (state->dpy, state->xgwa.colormap, &state->fg.pixel, 1, 0);
160 if (state->fg2.flags)
161 XFreeColors (state->dpy, state->xgwa.colormap, &state->fg2.pixel, 1, 0);
163 state->fg.flags = DoRed|DoGreen|DoBlue;
164 state->fg.red = 0x8888 + (random() % 0x8888);
165 state->fg.green = 0x8888 + (random() % 0x8888);
166 state->fg.blue = 0x8888 + (random() % 0x8888);
168 state->fg2.flags = DoRed|DoGreen|DoBlue;
169 state->fg2.red = 0x8888 + (random() % 0x8888);
170 state->fg2.green = 0x8888 + (random() % 0x8888);
171 state->fg2.blue = 0x8888 + (random() % 0x8888);
173 if (XAllocColor (state->dpy, state->xgwa.colormap, &state->fg))
174 XSetForeground (state->dpy, state->draw_gc, state->fg.pixel);
176 if (XAllocColor (state->dpy, state->xgwa.colormap, &state->fg2))
177 XSetForeground (state->dpy, state->draw_gc2, state->fg2.pixel);
181 /* Initialize the state structure and various X data.
184 init_balls (Display *dpy, Window window)
188 b_state *state = (b_state *) calloc (1, sizeof(*state));
192 state->window = window;
194 check_window_moved (state);
196 /* Select ButtonRelease events on the external window, if no other app has
197 already selected it (only one app can select it at a time: BadAccess. */
198 if (! (state->xgwa.all_event_masks & ButtonReleaseMask))
199 XSelectInput (state->dpy, state->window,
200 state->xgwa.your_event_mask | ButtonReleaseMask);
202 gcv.foreground = get_pixel_resource("foreground", "Foreground",
203 state->dpy, state->xgwa.colormap);
204 gcv.background = get_pixel_resource("background", "Background",
205 state->dpy, state->xgwa.colormap);
206 state->draw_gc = XCreateGC (state->dpy, state->window,
207 GCForeground|GCBackground, &gcv);
209 gcv.foreground = get_pixel_resource("mouseForeground", "MouseForeground",
210 state->dpy, state->xgwa.colormap);
211 state->draw_gc2 = XCreateGC (state->dpy, state->window,
212 GCForeground|GCBackground, &gcv);
214 gcv.foreground = gcv.background;
215 state->erase_gc = XCreateGC (state->dpy, state->window,
216 GCForeground|GCBackground, &gcv);
220 extx = state->xmax - state->xmin;
221 exty = state->ymax - state->ymin;
223 state->count = get_integer_resource ("count", "Count");
224 if (state->count < 1) state->count = 20;
226 state->max_radius = get_float_resource ("size", "Size") / 2;
227 if (state->max_radius < 1.0) state->max_radius = 1.0;
229 state->accx = get_float_resource ("wind", "Wind");
230 if (state->accx < -1.0 || state->accx > 1.0) state->accx = 0;
232 state->accy = get_float_resource ("gravity", "Gravity");
233 if (state->accy < -1.0 || state->accy > 1.0) state->accy = 0.01;
235 state->e = get_float_resource ("friction", "Friction");
236 if (state->e < 0.2 || state->e > 1.0) state->e = 0.97;
238 state->tc = get_float_resource ("timeScale", "TimeScale");
239 if (state->tc <= 0 || state->tc > 10) state->tc = 1.0;
241 state->shake_p = get_boolean_resource ("shake", "Shake");
242 state->shake_threshold = get_float_resource ("shakeThreshold",
245 state->fps_p = get_boolean_resource ("doFPS", "DoFPS");
249 char *fontname = get_string_resource ("font", "Font");
250 const char *def_font = "fixed";
251 if (!fontname || !*fontname) fontname = (char *)def_font;
252 font = XLoadQueryFont (dpy, fontname);
253 if (!font) font = XLoadQueryFont (dpy, def_font);
255 gcv.font = font->fid;
256 gcv.foreground = get_pixel_resource("textColor", "Foreground",
257 state->dpy, state->xgwa.colormap);
258 state->font_gc = XCreateGC(dpy, window,
259 GCFont|GCForeground|GCBackground, &gcv);
260 state->font_height = font->ascent + font->descent;
261 state->font_baseline = font->descent;
264 state->r = (float *) malloc (sizeof (*state->r) * (state->count + 1));
265 state->vx = (float *) malloc (sizeof (*state->vx) * (state->count + 1));
266 state->vy = (float *) malloc (sizeof (*state->vy) * (state->count + 1));
267 state->px = (float *) malloc (sizeof (*state->px) * (state->count + 1));
268 state->py = (float *) malloc (sizeof (*state->py) * (state->count + 1));
269 state->opx = (float *) malloc (sizeof (*state->opx) * (state->count + 1));
270 state->opy = (float *) malloc (sizeof (*state->opy) * (state->count + 1));
272 for (i=1; i<=state->count; i++)
274 state->px[i] = frand(extx) + state->xmin;
275 state->py[i] = frand(exty) + state->ymin;
276 state->vx[i] = frand(0.2);
277 state->vy[i] = frand(0.2);
279 state->r[i] = state->max_radius;
282 /* state->r[i] = 4 + (random() % (int) (state->max_radius - 4)); */
289 /* Messes with gravity: permute "down" to be in a random direction.
292 shake (b_state *state)
294 float a = state->accx;
295 float b = state->accy;
296 int i = random() % 4;
321 state->time_since_shake = 0;
326 /* Look at the current time, and update state->time_since_shake.
327 Draw the FPS display if desired.
330 check_wall_clock (b_state *state, float max_d)
333 state->frame_count++;
335 if (tick++ > 20) /* don't call gettimeofday() too often -- it's slow. */
337 static struct timeval last = { 0, };
339 # ifdef GETTIMEOFDAY_TWO_ARGS
341 gettimeofday(&now, &tzp);
346 if (last.tv_sec == 0)
350 if (now.tv_sec == last.tv_sec)
353 state->time_since_shake += (now.tv_sec - last.tv_sec);
357 static char buf[1024];
358 float elapsed = ((now.tv_sec + (now.tv_usec / 1000000.0)) -
359 (last.tv_sec + (last.tv_usec / 1000000.0)));
360 float fps = state->frame_count / elapsed;
361 float cps = state->collision_count / elapsed;
363 sprintf (buf, " FPS: %.2f Collisions: %.f/frame Max motion: %.3f",
364 fps, cps/fps, max_d);
366 XFillRectangle (state->dpy, state->window, state->erase_gc,
367 0, state->xgwa.height - state->font_height,
368 state->xgwa.width, state->font_height);
369 XDrawImageString (state->dpy, state->window, state->font_gc,
370 0, state->xgwa.height - state->font_baseline,
374 state->frame_count = 0;
375 state->collision_count = 0;
381 /* Erases the balls at their previous positions, and draws the new ones.
384 repaint_balls (b_state *state)
387 int x1a, x2a, y1a, y2a;
388 int x1b, x2b, y1b, y2b;
391 for (a=1; a <= state->count; a++)
394 x1a = (state->opx[a] - state->r[a] - state->xmin);
395 y1a = (state->opy[a] - state->r[a] - state->ymin);
396 x2a = (state->opx[a] + state->r[a] - state->xmin);
397 y2a = (state->opy[a] + state->r[a] - state->ymin);
399 x1b = (state->px[a] - state->r[a] - state->xmin);
400 y1b = (state->py[a] - state->r[a] - state->ymin);
401 x2b = (state->px[a] + state->r[a] - state->xmin);
402 y2b = (state->py[a] + state->r[a] - state->ymin);
404 /* if (x1a != x1b || y1a != y1b) -- leaves turds if we optimize this */
406 gc = state->erase_gc;
407 XFillArc (state->dpy, state->window, gc,
408 x1a, y1a, x2a-x1a, y2a-y1a,
412 if (state->mouse_ball == a)
413 gc = state->draw_gc2;
417 XFillArc (state->dpy, state->window, gc,
418 x1b, y1b, x2b-x1b, y2b-y1b,
423 /* distance this ball moved this frame */
424 float d = ((state->px[a] - state->opx[a]) *
425 (state->px[a] - state->opx[a]) +
426 (state->py[a] - state->opy[a]) *
427 (state->py[a] - state->opy[a]));
428 if (d > max_d) max_d = d;
431 state->opx[a] = state->px[a];
432 state->opy[a] = state->py[a];
435 if (state->shake_p && state->time_since_shake > 5)
437 max_d /= state->max_radius;
438 if (max_d < state->shake_threshold || /* when its stable */
439 state->time_since_shake > 30) /* or when 30 secs has passed */
445 check_wall_clock (state, max_d);
449 /* Implements the laws of physics: move balls to their new positions.
452 update_balls (b_state *state)
455 float d, nx, ny, m, vxa, vya, vxb, vyb, dd, cdx, cdy, cosam;
456 float dee2 = state->max_radius * state->max_radius * 4;
458 check_window_moved (state);
460 /* If we're currently tracking the mouse, update that ball first.
462 if (state->mouse_ball != 0)
464 int mouse_x, mouse_y;
465 query_mouse (state, &mouse_x, &mouse_y);
466 state->px[state->mouse_ball] = mouse_x;
467 state->py[state->mouse_ball] = mouse_y;
468 state->vx[state->mouse_ball] =
470 (state->px[state->mouse_ball] - state->opx[state->mouse_ball]) *
472 state->vy[state->mouse_ball] =
474 (state->py[state->mouse_ball] - state->opy[state->mouse_ball]) *
478 /* For each ball, compute the influence of every other ball. */
479 for (a=1; a <= state->count; a++)
480 if (a != state->mouse_ball)
481 for (b=1; b <= state->count; b++)
484 d = ((state->px[a] - state->px[b]) *
485 (state->px[a] - state->px[b]) +
486 (state->py[a] - state->py[b]) *
487 (state->py[a] - state->py[b]));
490 state->collision_count++;
496 nx = state->px[b] - state->px[a];
497 ny = state->py[b] - state->py[a];
498 if (d < -0.0001 || d > 0.0001)
502 dd = state->r[a] + state->r[b] - d;
503 state->px[a] -= dd*cdx; /* just move them apart */
504 state->py[a] -= dd*cdy; /* no physical rationale, sorry */
505 state->px[b] += dd*cdx;
506 state->py[b] += dd*cdy;
507 m = sqrt (state->vx[a] * state->vx[a] +
508 state->vy[a] * state->vy[a]);
509 if (m < -0.0001 || m > 0.0001) /* A's velocity > 0 ? */
511 cosam = ((cdx * state->vx[a] + cdy * state->vy[a]) *
514 vya -= cdy * cosam; /* conserve momentum */
518 m = sqrt (state->vx[b] *
522 if (m < -0.0001 || m > 0.0001)
524 cosam = ((cdx * state->vx[b] + cdy * state->vy[b]) *
539 /* Force all balls to be on screen.
541 for (a=1; a <= state->count; a++)
543 if (state->px[a] <= (state->xmin + state->r[a]))
545 state->px[a] = state->xmin + state->r[a];
546 state->vx[a] = -state->vx[a] * state->e;
548 if (state->px[a] >= (state->xmax - state->r[a]))
550 state->px[a] = state->xmax - state->r[a];
551 state->vx[a] = -state->vx[a] * state->e;
553 if (state->py[a] <= (state->ymin + state->r[a]))
555 state->py[a] = state->ymin + state->r[a];
556 state->vy[a] = -state->vy[a] * state->e;
558 if (state->py[a] >= (state->ymax - state->r[a]))
560 state->py[a] = state->ymax - state->r[a];
561 state->vy[a] = -state->vy[a] * state->e;
565 /* Apply gravity to all balls.
567 for (a=1; a <= state->count; a++)
568 if (a != state->mouse_ball)
570 state->vx[a] += state->accx * state->tc;
571 state->vy[a] += state->accy * state->tc;
572 state->px[a] += state->vx[a] * state->tc;
573 state->py[a] += state->vy[a] * state->tc;
578 /* Handle X events, specifically, allow a ball to be picked up with the mouse.
581 handle_events (b_state *state)
583 XSync (state->dpy, False);
584 while (XPending (state->dpy))
587 XNextEvent (state->dpy, &event);
588 if (event.xany.type == ButtonPress)
591 float fmx = event.xbutton.x_root;
592 float fmy = event.xbutton.y_root;
593 if (state->mouse_ball != 0) /* second down-click? drop the ball. */
595 state->mouse_ball = 0;
599 for (i=1; i <= state->count; i++)
601 float d = ((state->px[i] - fmx) * (state->px[i] - fmx) +
602 (state->py[i] - fmy) * (state->py[i] - fmy));
603 float r = state->r[i];
606 state->mouse_ball = i;
611 else if (event.xany.type == ButtonRelease) /* drop the ball */
613 state->mouse_ball = 0;
616 else if (event.xany.type == ConfigureNotify)
618 /* This is redundant, since we poll this every iteration. */
619 check_window_moved (state);
622 screenhack_handle_event (state->dpy, &event);
627 char *progclass = "FluidBalls";
629 char *defaults [] = {
630 ".background: black",
631 ".textColor: yellow",
632 ".font: -*-helvetica-*-r-*-*-*-180-*-*-p-*-*-*",
642 "*shakeThreshold: 0.015",
646 XrmOptionDescRec options [] = {
647 { "-delay", ".delay", XrmoptionSepArg, 0 },
648 { "-count", ".count", XrmoptionSepArg, 0 },
649 { "-size", ".size", XrmoptionSepArg, 0 },
650 { "-count", ".count", XrmoptionSepArg, 0 },
651 { "-gravity", ".gravity", XrmoptionSepArg, 0 },
652 { "-wind", ".wind", XrmoptionSepArg, 0 },
653 { "-friction", ".friction", XrmoptionSepArg, 0 },
654 { "-fps", ".doFPS", XrmoptionNoArg, "True" },
655 { "-no-fps", ".doFPS", XrmoptionNoArg, "False" },
656 { "-shake", ".shake", XrmoptionNoArg, "True" },
657 { "-no-shake", ".shake", XrmoptionNoArg, "False" },
662 screenhack (Display *dpy, Window window)
664 b_state *state = init_balls(dpy, window);
665 int delay = get_integer_resource ("delay", "Integer");
669 repaint_balls(state);
673 handle_events (state);
674 if (delay) usleep (delay);