From http://www.jwz.org/xscreensaver/xscreensaver-5.38.tar.gz
[xscreensaver] / hacks / fluidballs.c
1 /* fluidballs, Copyright (c) 2000 by Peter Birtles <peter@bqdesign.com.au>
2  *
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
9  * implied warranty.
10  *
11  * Ported to X11 and xscreensaver by jwz, 27-Feb-2002.
12  *
13  * http://astronomy.swin.edu.au/~pbourke/modelling/fluid/
14  *
15  * Some physics improvements by Steven Barker <steve@blckknght.org>
16  */
17
18 /* Future ideas:
19  * Specifying a distribution in the ball sizes (with a gamma curve, possibly).
20  * Brownian motion, for that extra touch of realism.
21  *
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.
25  */
26
27 #include <math.h>
28 #include "screenhack.h"
29 #include <stdio.h>
30
31 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
32 #include "xdbe.h"
33 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
34
35 typedef struct {
36   Display *dpy;
37   Window window;
38   XWindowAttributes xgwa;
39   int delay;
40
41   Pixmap b, ba; /* double-buffer to reduce flicker */
42 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
43   XdbeBackBuffer backb;
44   Bool dbeclear_p;
45 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
46
47   GC draw_gc;           /* most of the balls */
48   GC draw_gc2;          /* the ball being dragged with the mouse */
49   GC erase_gc;
50   XColor fg;
51   XColor fg2;
52
53   int count;            /* number of balls */
54   float xmin, ymin;     /* rectangle of window, relative to root */
55   float xmax, ymax;
56
57   int mouse_ball;       /* index of ball being dragged, or 0 if none. */
58
59   float tc;             /* time constant (time-warp multiplier) */
60   float accx;           /* horizontal acceleration (wind) */
61   float accy;           /* vertical acceleration (gravity) */
62
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 */
67
68   float *m;             /* ball mass, precalculated */
69   float e;              /* coeficient of elasticity */
70   float max_radius;     /* largest radius of any ball */
71
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;
76   int time_since_shake;
77
78   Bool fps_p;           /* Whether to draw some text at the bottom. */
79   GC font_gc;
80   int font_height;
81   int font_baseline;
82   int frame_count;
83   int collision_count;
84   char fps_str[1024];
85   
86   int time_tick;
87   struct timeval last_time;
88
89 } b_state;
90
91
92 /* Draws the frames per second string */
93 static void 
94 draw_fps_string (b_state *state)
95 {  
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));
103 }
104
105 /* Finds the origin of the window relative to the root window, by
106    walking up the window tree until it reaches the top.
107  */
108 static void
109 window_origin (Display *dpy, Window window, int *x, int *y)
110 {
111   XTranslateCoordinates (dpy, window, RootWindow (dpy, DefaultScreen (dpy)),
112                          0, 0, x, y, &window);
113 }
114
115
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.)
122  */
123 static void
124 check_window_moved (b_state *state)
125 {
126   float oxmin = state->xmin;
127   float oxmax = state->xmax;
128   float oymin = state->ymin;
129   float oymax = state->ymax;
130   int wx, wy;
131   XGetWindowAttributes (state->dpy, state->window, &state->xgwa);
132   window_origin (state->dpy, state->window, &wx, &wy);
133   state->xmin = wx;
134   state->ymin = 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);
138
139   if (state->dbuf && (state->ba))
140     {
141       if (oxmax != state->xmax || oymax != state->ymax)
142         {
143           XFreePixmap (state->dpy, state->ba);
144           state->ba = XCreatePixmap (state->dpy, state->window, 
145                                      state->xgwa.width, state->xgwa.height,
146                                      state->xgwa.depth);
147           XFillRectangle (state->dpy, state->ba, state->erase_gc, 0, 0, 
148                           state->xgwa.width, state->xgwa.height);
149           state->b = state->ba;
150         }
151     }
152   else 
153     {
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);
161     }
162 }
163
164
165 /* Returns the position of the mouse relative to the root window.
166  */
167 static void
168 query_mouse (b_state *state, int *x, int *y)
169 {
170   Window root1, child1;
171   int mouse_x, mouse_y, root_x, root_y;
172   unsigned int mask;
173   if (XQueryPointer (state->dpy, state->window, &root1, &child1,
174                      &root_x, &root_y, &mouse_x, &mouse_y, &mask))
175     {
176       *x = root_x;
177       *y = root_y;
178     }
179   else
180     {
181       *x = -9999;
182       *y = -9999;
183     }
184 }
185
186 /* Re-pick the colors of the balls, and the mouse-ball.
187  */
188 static void
189 recolor (b_state *state)
190 {
191   if (state->fg.flags)
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);
195
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);
200
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);
205
206   if (XAllocColor (state->dpy, state->xgwa.colormap, &state->fg))
207     XSetForeground (state->dpy, state->draw_gc,  state->fg.pixel);
208
209   if (XAllocColor (state->dpy, state->xgwa.colormap, &state->fg2))
210     XSetForeground (state->dpy, state->draw_gc2, state->fg2.pixel);
211 }
212
213 /* Initialize the state structure and various X data.
214  */
215 static void *
216 fluidballs_init (Display *dpy, Window window)
217 {
218   int i;
219   float extx, exty;
220   b_state *state = (b_state *) calloc (1, sizeof(*state));
221   XGCValues gcv;
222
223   state->dpy = dpy;
224   state->window = window;
225   state->delay = get_integer_resource (dpy, "delay", "Integer");
226
227   check_window_moved (state);
228
229   state->dbuf = get_boolean_resource (dpy, "doubleBuffer", "Boolean");
230
231 # ifdef HAVE_JWXYZ      /* Don't second-guess Quartz's double-buffering */
232   state->dbuf = False;
233 # endif
234
235   if (state->dbuf)
236     {
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);
241       else
242         state->b = xdbe_get_backbuffer (dpy, window, XdbeUndefined);
243       state->backb = state->b;
244 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
245
246       if (!state->b)
247         {
248           state->ba = XCreatePixmap (state->dpy, state->window, 
249                                      state->xgwa.width, state->xgwa.height,
250                                      state->xgwa.depth);
251           state->b = state->ba;
252         }
253     }
254   else
255     {
256       state->b = state->window;
257     }
258
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. */
261 #if 0
262   if (! (state->xgwa.all_event_masks & ButtonReleaseMask))
263     XSelectInput (state->dpy, state->window,
264                   state->xgwa.your_event_mask | ButtonReleaseMask);
265 #endif
266
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);
273
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);
278
279   gcv.foreground = gcv.background;
280   state->erase_gc = XCreateGC (state->dpy, state->b,
281                                GCForeground|GCBackground, &gcv);
282
283
284   if (state->ba) 
285     XFillRectangle (state->dpy, state->ba, state->erase_gc, 0, 0, 
286                     state->xgwa.width, state->xgwa.height);
287
288   recolor (state);
289
290   extx = state->xmax - state->xmin;
291   exty = state->ymax - state->ymin;
292
293   state->count = get_integer_resource (dpy, "count", "Count");
294   if (state->count < 1) state->count = 20;
295
296   state->max_radius = get_float_resource (dpy, "size", "Size") / 2;
297   if (state->max_radius < 1.0) state->max_radius = 1.0;
298
299   if (state->xgwa.width < 100 || state->xgwa.height < 100) /* tiny window */
300     {
301       if (state->max_radius > 5)
302         state->max_radius = 5;
303     }
304
305   state->random_sizes_p = get_boolean_resource (dpy, "random", "Random");
306
307   /* If the initial window size is too small to hold all these balls,
308      make fewer of them...
309    */
310   {
311     float r = (state->random_sizes_p
312                ? state->max_radius * 0.7
313                : state->max_radius);
314     float ball_area = M_PI * r * r;
315     float balls_area = state->count * ball_area;
316     float window_area = state->xgwa.width * state->xgwa.height;
317     window_area *= 0.75;  /* don't pack it completely full */
318     if (balls_area > window_area)
319       state->count = window_area / ball_area;
320   }
321
322   state->accx = get_float_resource (dpy, "wind", "Wind");
323   if (state->accx < -1.0 || state->accx > 1.0) state->accx = 0;
324
325   state->accy = get_float_resource (dpy, "gravity", "Gravity");
326   if (state->accy < -1.0 || state->accy > 1.0) state->accy = 0.01;
327
328   state->e = get_float_resource (dpy, "elasticity", "Elacitcity");
329   if (state->e < 0.2 || state->e > 1.0) state->e = 0.97;
330
331   state->tc = get_float_resource (dpy, "timeScale", "TimeScale");
332   if (state->tc <= 0 || state->tc > 10) state->tc = 1.0;
333
334   state->shake_p = get_boolean_resource (dpy, "shake", "Shake");
335   state->shake_threshold = get_float_resource (dpy, "shakeThreshold",
336                                                "ShakeThreshold");
337   state->time_tick = 999999;
338
339 # ifdef HAVE_MOBILE     /* Always obey real-world gravity */
340   state->shake_p = False;
341 # endif
342
343
344   state->fps_p = get_boolean_resource (dpy, "doFPS", "DoFPS");
345   if (state->fps_p)
346     {
347       XFontStruct *font;
348       char *fontname = get_string_resource (dpy, "fpsFont", "Font");
349       if (!fontname) fontname = "-*-courier-bold-r-normal-*-180-*";
350       font = XLoadQueryFont (dpy, fontname);
351       if (!font) font = XLoadQueryFont (dpy, "fixed");
352       if (!font) exit(-1);
353       gcv.font = font->fid;
354       gcv.foreground = get_pixel_resource(state->dpy, state->xgwa.colormap,
355                                           "textColor", "Foreground");
356       state->font_gc = XCreateGC(dpy, state->b,
357                                  GCFont|GCForeground|GCBackground, &gcv);
358       state->font_height = font->ascent + font->descent;
359       state->font_baseline = font->descent;
360     }
361
362   state->m   = (float *) malloc (sizeof (*state->m)   * (state->count + 1));
363   state->r   = (float *) malloc (sizeof (*state->r)   * (state->count + 1));
364   state->vx  = (float *) malloc (sizeof (*state->vx)  * (state->count + 1));
365   state->vy  = (float *) malloc (sizeof (*state->vy)  * (state->count + 1));
366   state->px  = (float *) malloc (sizeof (*state->px)  * (state->count + 1));
367   state->py  = (float *) malloc (sizeof (*state->py)  * (state->count + 1));
368   state->opx = (float *) malloc (sizeof (*state->opx) * (state->count + 1));
369   state->opy = (float *) malloc (sizeof (*state->opy) * (state->count + 1));
370
371   for (i=1; i<=state->count; i++)
372     {
373       state->px[i] = frand(extx) + state->xmin;
374       state->py[i] = frand(exty) + state->ymin;
375       state->vx[i] = frand(0.2) - 0.1;
376       state->vy[i] = frand(0.2) - 0.1;
377
378       state->r[i] = (state->random_sizes_p
379                      ? ((0.2 + frand(0.8)) * state->max_radius)
380                      : state->max_radius);
381       /*state->r[i] = pow(frand(1.0), state->sizegamma) * state->max_radius;*/
382
383       /* state->m[i] = pow(state->r[i],2) * M_PI; */
384       state->m[i] = pow(state->r[i],3) * M_PI * 1.3333;
385     }
386
387   memcpy (state->opx, state->px, sizeof (*state->opx) * (state->count + 1));
388   memcpy (state->opy, state->py, sizeof (*state->opx) * (state->count + 1));
389
390   return state;
391 }
392
393
394 /* Messes with gravity: permute "down" to be in a random direction.
395  */
396 static void
397 shake (b_state *state)
398 {
399   float a = state->accx;
400   float b = state->accy;
401   int i = random() % 4;
402
403   switch (i)
404     {
405     case 0:
406       state->accx = a;
407       state->accy = b;
408       break;
409     case 1:
410       state->accx = -a;
411       state->accy = -b;
412       break;
413     case 2:
414       state->accx = b;
415       state->accy = a;
416       break;
417     case 3:
418       state->accx = -b;
419       state->accy = -a;
420       break;
421     default:
422       abort();
423       break;
424     }
425
426   state->time_since_shake = 0;
427   recolor (state);
428 }
429
430
431 /* Look at the current time, and update state->time_since_shake.
432    Draw the FPS display if desired.
433  */
434 static void
435 check_wall_clock (b_state *state, float max_d)
436 {
437   state->frame_count++;
438   
439   if (state->time_tick++ > 20)  /* don't call gettimeofday() too often -- it's slow. */
440     {
441       struct timeval now;
442 # ifdef GETTIMEOFDAY_TWO_ARGS
443       struct timezone tzp;
444       gettimeofday(&now, &tzp);
445 # else
446       gettimeofday(&now);
447 # endif
448
449       if (state->last_time.tv_sec == 0)
450         state->last_time = now;
451
452       state->time_tick = 0;
453       if (now.tv_sec == state->last_time.tv_sec)
454         return;
455
456       state->time_since_shake += (now.tv_sec - state->last_time.tv_sec);
457
458 # ifdef HAVE_MOBILE     /* Always obey real-world gravity */
459       {
460         float a = fabs (fabs(state->accx) > fabs(state->accy)
461                         ? state->accx : state->accy);
462         int rot = current_device_rotation();
463         switch (rot) {
464         case    0: case  360: state->accx =  0; state->accy =  a; break;
465         case  -90:            state->accx = -a; state->accy =  0; break;
466         case   90:            state->accx =  a; state->accy =  0; break;
467         case  180: case -180: state->accx =  0; state->accy = -a; break;
468         default: break;
469         }
470       }
471 # endif /* HAVE_MOBILE */
472
473       if (state->fps_p) 
474         {
475           float elapsed = ((now.tv_sec  + (now.tv_usec  / 1000000.0)) -
476                            (state->last_time.tv_sec + (state->last_time.tv_usec / 1000000.0)));
477           float fps = state->frame_count / elapsed;
478           float cps = state->collision_count / elapsed;
479           
480           sprintf (state->fps_str, "Collisions: %.3f/frame  Max motion: %.3f",
481                    cps/fps, max_d);
482           
483           draw_fps_string(state);
484         }
485
486       state->frame_count = 0;
487       state->collision_count = 0;
488       state->last_time = now;
489     }
490 }
491
492 /* Erases the balls at their previous positions, and draws the new ones.
493  */
494 static void
495 repaint_balls (b_state *state)
496 {
497   int a;
498 # ifndef HAVE_JWXYZ
499   int x1a, x2a, y1a, y2a;
500 # endif
501   int x1b, x2b, y1b, y2b;
502   float max_d = 0;
503
504 #ifdef HAVE_JWXYZ       /* Don't second-guess Quartz's double-buffering */
505   XClearWindow (state->dpy, state->b);
506 #endif
507
508   for (a=1; a <= state->count; a++)
509     {
510       GC gc;
511 # ifndef HAVE_JWXYZ
512       x1a = (state->opx[a] - state->r[a] - state->xmin);
513       y1a = (state->opy[a] - state->r[a] - state->ymin);
514       x2a = (state->opx[a] + state->r[a] - state->xmin);
515       y2a = (state->opy[a] + state->r[a] - state->ymin);
516 # endif
517
518       x1b = (state->px[a] - state->r[a] - state->xmin);
519       y1b = (state->py[a] - state->r[a] - state->ymin);
520       x2b = (state->px[a] + state->r[a] - state->xmin);
521       y2b = (state->py[a] + state->r[a] - state->ymin);
522
523 #ifndef HAVE_JWXYZ      /* Don't second-guess Quartz's double-buffering */
524 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
525       if (!state->dbeclear_p || !state->backb)
526 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
527         {
528 /*        if (x1a != x1b || y1a != y1b)   -- leaves turds if we optimize this */
529             {
530               gc = state->erase_gc;
531               XFillArc (state->dpy, state->b, gc,
532                         x1a, y1a, x2a-x1a, y2a-y1a,
533                         0, 360*64);
534             }
535         }
536 #endif /* !HAVE_JWXYZ */
537
538       if (state->mouse_ball == a)
539         gc = state->draw_gc2;
540       else
541         gc = state->draw_gc;
542
543       XFillArc (state->dpy, state->b, gc,
544                 x1b, y1b, x2b-x1b, y2b-y1b,
545                 0, 360*64);
546
547       if (state->shake_p)
548         {
549           /* distance this ball moved this frame */
550           float d = ((state->px[a] - state->opx[a]) *
551                      (state->px[a] - state->opx[a]) +
552                      (state->py[a] - state->opy[a]) *
553                      (state->py[a] - state->opy[a]));
554           if (d > max_d) max_d = d;
555         }
556
557       state->opx[a] = state->px[a];
558       state->opy[a] = state->py[a];
559     }
560
561   if (state->fps_p
562 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
563       && (state->backb ? state->dbeclear_p : 1)
564 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
565       )
566     draw_fps_string(state);
567
568 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
569   if (state->backb)
570     {
571       XdbeSwapInfo info[1];
572       info[0].swap_window = state->window;
573       info[0].swap_action = (state->dbeclear_p ? XdbeBackground : XdbeUndefined);
574       XdbeSwapBuffers (state->dpy, info, 1);
575     }
576   else
577 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
578   if (state->dbuf)
579     {
580       XCopyArea (state->dpy, state->b, state->window, state->erase_gc,
581                  0, 0, state->xgwa.width, state->xgwa.height, 0, 0);
582     }
583
584   if (state->shake_p && state->time_since_shake > 5)
585     {
586       max_d /= state->max_radius;
587       if (max_d < state->shake_threshold ||  /* when its stable */
588           state->time_since_shake > 30)      /* or when 30 secs has passed */
589         {
590           shake (state);
591         }
592     }
593
594   check_wall_clock (state, max_d);
595 }
596
597
598 /* Implements the laws of physics: move balls to their new positions.
599  */
600 static void
601 update_balls (b_state *state)
602 {
603   int a, b;
604   float d, vxa, vya, vxb, vyb, dd, cdx, cdy;
605   float ma, mb, vca, vcb, dva, dvb;
606   float dee2;
607
608   check_window_moved (state);
609
610   /* If we're currently tracking the mouse, update that ball first.
611    */
612   if (state->mouse_ball != 0)
613     {
614       int mouse_x, mouse_y;
615       query_mouse (state, &mouse_x, &mouse_y);
616       state->px[state->mouse_ball] = mouse_x;
617       state->py[state->mouse_ball] = mouse_y;
618       state->vx[state->mouse_ball] =
619         (0.1 *
620          (state->px[state->mouse_ball] - state->opx[state->mouse_ball]) *
621          state->tc);
622       state->vy[state->mouse_ball] =
623         (0.1 *
624          (state->py[state->mouse_ball] - state->opy[state->mouse_ball]) *
625          state->tc);
626     }
627
628   /* For each ball, compute the influence of every other ball. */
629   for (a=1; a <= state->count -  1; a++)
630     for (b=a + 1; b <= state->count; b++)
631       {
632          d = ((state->px[a] - state->px[b]) *
633               (state->px[a] - state->px[b]) +
634               (state->py[a] - state->py[b]) *
635               (state->py[a] - state->py[b]));
636          dee2 = (state->r[a] + state->r[b]) *
637                 (state->r[a] + state->r[b]);
638          if (d < dee2)
639          {
640             state->collision_count++;
641             d = sqrt(d);
642             dd = state->r[a] + state->r[b] - d;
643
644             cdx = (state->px[b] - state->px[a]) / d;
645             cdy = (state->py[b] - state->py[a]) / d;
646
647             /* Move each ball apart from the other by half the
648              * 'collision' distance.
649              */
650             state->px[a] -= 0.5 * dd * cdx;
651             state->py[a] -= 0.5 * dd * cdy;
652             state->px[b] += 0.5 * dd * cdx;
653             state->py[b] += 0.5 * dd * cdy;
654
655             ma = state->m[a];
656             mb = state->m[b];
657
658             vxa = state->vx[a];
659             vya = state->vy[a];
660             vxb = state->vx[b];
661             vyb = state->vy[b];
662
663             vca = vxa * cdx + vya * cdy; /* the component of each velocity */
664             vcb = vxb * cdx + vyb * cdy; /* along the axis of the collision */
665
666             /* elastic collison */
667             dva = (vca * (ma - mb) + vcb * 2 * mb) / (ma + mb) - vca;
668             dvb = (vcb * (mb - ma) + vca * 2 * ma) / (ma + mb) - vcb;
669
670             dva *= state->e; /* some energy lost to inelasticity */
671             dvb *= state->e;
672
673 #if 0
674             dva += (frand (50) - 25) / ma;   /* q: why are elves so chaotic? */
675             dvb += (frand (50) - 25) / mb;   /* a: brownian motion. */
676 #endif
677
678             vxa += dva * cdx;
679             vya += dva * cdy;
680             vxb += dvb * cdx;
681             vyb += dvb * cdy;
682
683             state->vx[a] = vxa;
684             state->vy[a] = vya;
685             state->vx[b] = vxb;
686             state->vy[b] = vyb;
687          }
688       }
689
690    /* Force all balls to be on screen.
691     */
692   for (a=1; a <= state->count; a++)
693     {
694       if (state->px[a] <= (state->xmin + state->r[a]))
695         {
696           state->px[a] = state->xmin + state->r[a];
697           state->vx[a] = -state->vx[a] * state->e;
698         }
699       if (state->px[a] >= (state->xmax - state->r[a]))
700         {
701           state->px[a] = state->xmax - state->r[a];
702           state->vx[a] = -state->vx[a] * state->e;
703         }
704       if (state->py[a] <= (state->ymin + state->r[a]))
705         {
706           state->py[a] = state->ymin + state->r[a];
707           state->vy[a] = -state->vy[a] * state->e;
708         }
709       if (state->py[a] >= (state->ymax - state->r[a]))
710         {
711           state->py[a] = state->ymax - state->r[a];
712           state->vy[a] = -state->vy[a] * state->e;
713         }
714     }
715
716   /* Apply gravity to all balls.
717    */
718   for (a=1; a <= state->count; a++)
719     if (a != state->mouse_ball)
720       {
721         state->vx[a] += state->accx * state->tc;
722         state->vy[a] += state->accy * state->tc;
723         state->px[a] += state->vx[a] * state->tc;
724         state->py[a] += state->vy[a] * state->tc;
725       }
726 }
727
728
729 /* Handle X events, specifically, allow a ball to be picked up with the mouse.
730  */
731 static Bool
732 fluidballs_event (Display *dpy, Window window, void *closure, XEvent *event)
733 {
734   b_state *state = (b_state *) closure;
735
736   if (event->xany.type == ButtonPress)
737     {
738       int i, rx, ry;
739       XTranslateCoordinates (dpy, window, RootWindow (dpy, DefaultScreen(dpy)),
740                              event->xbutton.x, event->xbutton.y, &rx, &ry,
741                              &window);
742
743       if (state->mouse_ball != 0)  /* second down-click?  drop the ball. */
744         {
745           state->mouse_ball = 0;
746           return True;
747         }
748       else
749         {
750           /* When trying to pick up a ball, first look for a click directly
751              inside the ball; but if we don't find it, expand the radius
752              outward until we find something nearby.
753            */
754           float max = state->max_radius * 4;
755           float step = max / 10;
756           float r2;
757           for (r2 = step; r2 < max; r2 += step) {
758             for (i = 1; i <= state->count; i++)
759               {
760                 float d = ((state->px[i] - rx) * (state->px[i] - rx) +
761                            (state->py[i] - ry) * (state->py[i] - ry));
762                 float r = state->r[i];
763                 if (r2 > r) r = r2;
764                 if (d < r*r)
765                   {
766                     state->mouse_ball = i;
767                     return True;
768                   }
769               }
770           }
771         }
772       return True;
773     }
774   else if (event->xany.type == ButtonRelease)   /* drop the ball */
775     {
776       state->mouse_ball = 0;
777       return True;
778     }
779
780   return False;
781 }
782
783 static unsigned long
784 fluidballs_draw (Display *dpy, Window window, void *closure)
785 {
786   b_state *state = (b_state *) closure;
787   repaint_balls(state);
788   update_balls(state);
789   return state->delay;
790 }
791
792 static void
793 fluidballs_reshape (Display *dpy, Window window, void *closure, 
794                  unsigned int w, unsigned int h)
795 {
796 }
797
798 static void
799 fluidballs_free (Display *dpy, Window window, void *closure)
800 {
801   b_state *state = (b_state *) closure;
802   free (state);
803 }
804
805
806 static const char *fluidballs_defaults [] = {
807   ".background:         black",
808   ".foreground:         yellow",
809   ".textColor:          yellow",
810   "*mouseForeground:    white",
811   "*delay:              10000",
812   "*count:              300",
813   "*size:               25",
814   "*random:             True",
815   "*gravity:            0.01",
816   "*wind:               0.00",
817   "*elasticity:         0.97",
818   "*timeScale:          1.0",
819   "*shake:              True",
820   "*shakeThreshold:     0.015",
821   "*doubleBuffer:       True",
822 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
823   "*useDBE:             True",
824   "*useDBEClear:        True",
825 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
826 #ifdef HAVE_MOBILE
827   "*ignoreRotation:     True",
828 #endif
829   0
830 };
831
832 static XrmOptionDescRec fluidballs_options [] = {
833   { "-delay",           ".delay",       XrmoptionSepArg, 0 },
834   { "-count",           ".count",       XrmoptionSepArg, 0 },
835   { "-size",            ".size",        XrmoptionSepArg, 0 },
836   { "-count",           ".count",       XrmoptionSepArg, 0 },
837   { "-gravity",         ".gravity",     XrmoptionSepArg, 0 },
838   { "-wind",            ".wind",        XrmoptionSepArg, 0 },
839   { "-elasticity",      ".elasticity",  XrmoptionSepArg, 0 },
840   { "-shake",           ".shake",       XrmoptionNoArg, "True" },
841   { "-no-shake",        ".shake",       XrmoptionNoArg, "False" },
842   { "-random",          ".random",      XrmoptionNoArg, "True" },
843   { "-no-random",       ".random",      XrmoptionNoArg, "False" },
844   { "-nonrandom",       ".random",      XrmoptionNoArg, "False" },
845   { "-db",              ".doubleBuffer", XrmoptionNoArg,  "True" },
846   { "-no-db",           ".doubleBuffer", XrmoptionNoArg,  "False" },
847   { 0, 0, 0, 0 }
848 };
849
850
851 XSCREENSAVER_MODULE ("FluidBalls", fluidballs)