From http://www.jwz.org/xscreensaver/xscreensaver-5.35.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   state->random_sizes_p = get_boolean_resource (dpy, "random", "Random");
300
301   /* If the initial window size is too small to hold all these balls,
302      make fewer of them...
303    */
304   {
305     float r = (state->random_sizes_p
306                ? state->max_radius * 0.7
307                : state->max_radius);
308     float ball_area = M_PI * r * r;
309     float balls_area = state->count * ball_area;
310     float window_area = state->xgwa.width * state->xgwa.height;
311     window_area *= 0.75;  /* don't pack it completely full */
312     if (balls_area > window_area)
313       state->count = window_area / ball_area;
314   }
315
316   state->accx = get_float_resource (dpy, "wind", "Wind");
317   if (state->accx < -1.0 || state->accx > 1.0) state->accx = 0;
318
319   state->accy = get_float_resource (dpy, "gravity", "Gravity");
320   if (state->accy < -1.0 || state->accy > 1.0) state->accy = 0.01;
321
322   state->e = get_float_resource (dpy, "elasticity", "Elacitcity");
323   if (state->e < 0.2 || state->e > 1.0) state->e = 0.97;
324
325   state->tc = get_float_resource (dpy, "timeScale", "TimeScale");
326   if (state->tc <= 0 || state->tc > 10) state->tc = 1.0;
327
328   state->shake_p = get_boolean_resource (dpy, "shake", "Shake");
329   state->shake_threshold = get_float_resource (dpy, "shakeThreshold",
330                                                "ShakeThreshold");
331   state->time_tick = 999999;
332
333 # ifdef HAVE_MOBILE     /* Always obey real-world gravity */
334   state->shake_p = False;
335 # endif
336
337
338   state->fps_p = get_boolean_resource (dpy, "doFPS", "DoFPS");
339   if (state->fps_p)
340     {
341       XFontStruct *font;
342       char *fontname = get_string_resource (dpy, "fpsFont", "Font");
343       if (!fontname) fontname = "-*-courier-bold-r-normal-*-180-*";
344       font = XLoadQueryFont (dpy, fontname);
345       if (!font) font = XLoadQueryFont (dpy, "fixed");
346       if (!font) exit(-1);
347       gcv.font = font->fid;
348       gcv.foreground = get_pixel_resource(state->dpy, state->xgwa.colormap,
349                                           "textColor", "Foreground");
350       state->font_gc = XCreateGC(dpy, state->b,
351                                  GCFont|GCForeground|GCBackground, &gcv);
352       state->font_height = font->ascent + font->descent;
353       state->font_baseline = font->descent;
354     }
355
356   state->m   = (float *) malloc (sizeof (*state->m)   * (state->count + 1));
357   state->r   = (float *) malloc (sizeof (*state->r)   * (state->count + 1));
358   state->vx  = (float *) malloc (sizeof (*state->vx)  * (state->count + 1));
359   state->vy  = (float *) malloc (sizeof (*state->vy)  * (state->count + 1));
360   state->px  = (float *) malloc (sizeof (*state->px)  * (state->count + 1));
361   state->py  = (float *) malloc (sizeof (*state->py)  * (state->count + 1));
362   state->opx = (float *) malloc (sizeof (*state->opx) * (state->count + 1));
363   state->opy = (float *) malloc (sizeof (*state->opy) * (state->count + 1));
364
365   for (i=1; i<=state->count; i++)
366     {
367       state->px[i] = frand(extx) + state->xmin;
368       state->py[i] = frand(exty) + state->ymin;
369       state->vx[i] = frand(0.2) - 0.1;
370       state->vy[i] = frand(0.2) - 0.1;
371
372       state->r[i] = (state->random_sizes_p
373                      ? ((0.2 + frand(0.8)) * state->max_radius)
374                      : state->max_radius);
375       /*state->r[i] = pow(frand(1.0), state->sizegamma) * state->max_radius;*/
376
377       /* state->m[i] = pow(state->r[i],2) * M_PI; */
378       state->m[i] = pow(state->r[i],3) * M_PI * 1.3333;
379     }
380
381   memcpy (state->opx, state->px, sizeof (*state->opx) * (state->count + 1));
382   memcpy (state->opy, state->py, sizeof (*state->opx) * (state->count + 1));
383
384   return state;
385 }
386
387
388 /* Messes with gravity: permute "down" to be in a random direction.
389  */
390 static void
391 shake (b_state *state)
392 {
393   float a = state->accx;
394   float b = state->accy;
395   int i = random() % 4;
396
397   switch (i)
398     {
399     case 0:
400       state->accx = a;
401       state->accy = b;
402       break;
403     case 1:
404       state->accx = -a;
405       state->accy = -b;
406       break;
407     case 2:
408       state->accx = b;
409       state->accy = a;
410       break;
411     case 3:
412       state->accx = -b;
413       state->accy = -a;
414       break;
415     default:
416       abort();
417       break;
418     }
419
420   state->time_since_shake = 0;
421   recolor (state);
422 }
423
424
425 /* Look at the current time, and update state->time_since_shake.
426    Draw the FPS display if desired.
427  */
428 static void
429 check_wall_clock (b_state *state, float max_d)
430 {
431   state->frame_count++;
432   
433   if (state->time_tick++ > 20)  /* don't call gettimeofday() too often -- it's slow. */
434     {
435       struct timeval now;
436 # ifdef GETTIMEOFDAY_TWO_ARGS
437       struct timezone tzp;
438       gettimeofday(&now, &tzp);
439 # else
440       gettimeofday(&now);
441 # endif
442
443       if (state->last_time.tv_sec == 0)
444         state->last_time = now;
445
446       state->time_tick = 0;
447       if (now.tv_sec == state->last_time.tv_sec)
448         return;
449
450       state->time_since_shake += (now.tv_sec - state->last_time.tv_sec);
451
452 # ifdef HAVE_MOBILE     /* Always obey real-world gravity */
453       {
454         float a = fabs (fabs(state->accx) > fabs(state->accy)
455                         ? state->accx : state->accy);
456         int rot = current_device_rotation();
457         switch (rot) {
458         case    0: case  360: state->accx =  0; state->accy =  a; break;
459         case  -90:            state->accx = -a; state->accy =  0; break;
460         case   90:            state->accx =  a; state->accy =  0; break;
461         case  180: case -180: state->accx =  0; state->accy = -a; break;
462         default: break;
463         }
464       }
465 # endif /* HAVE_MOBILE */
466
467       if (state->fps_p) 
468         {
469           float elapsed = ((now.tv_sec  + (now.tv_usec  / 1000000.0)) -
470                            (state->last_time.tv_sec + (state->last_time.tv_usec / 1000000.0)));
471           float fps = state->frame_count / elapsed;
472           float cps = state->collision_count / elapsed;
473           
474           sprintf (state->fps_str, "Collisions: %.3f/frame  Max motion: %.3f",
475                    cps/fps, max_d);
476           
477           draw_fps_string(state);
478         }
479
480       state->frame_count = 0;
481       state->collision_count = 0;
482       state->last_time = now;
483     }
484 }
485
486 /* Erases the balls at their previous positions, and draws the new ones.
487  */
488 static void
489 repaint_balls (b_state *state)
490 {
491   int a;
492 # ifndef HAVE_JWXYZ
493   int x1a, x2a, y1a, y2a;
494 # endif
495   int x1b, x2b, y1b, y2b;
496   float max_d = 0;
497
498 #ifdef HAVE_JWXYZ       /* Don't second-guess Quartz's double-buffering */
499   XClearWindow (state->dpy, state->b);
500 #endif
501
502   for (a=1; a <= state->count; a++)
503     {
504       GC gc;
505 # ifndef HAVE_JWXYZ
506       x1a = (state->opx[a] - state->r[a] - state->xmin);
507       y1a = (state->opy[a] - state->r[a] - state->ymin);
508       x2a = (state->opx[a] + state->r[a] - state->xmin);
509       y2a = (state->opy[a] + state->r[a] - state->ymin);
510 # endif
511
512       x1b = (state->px[a] - state->r[a] - state->xmin);
513       y1b = (state->py[a] - state->r[a] - state->ymin);
514       x2b = (state->px[a] + state->r[a] - state->xmin);
515       y2b = (state->py[a] + state->r[a] - state->ymin);
516
517 #ifndef HAVE_JWXYZ      /* Don't second-guess Quartz's double-buffering */
518 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
519       if (!state->dbeclear_p || !state->backb)
520 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
521         {
522 /*        if (x1a != x1b || y1a != y1b)   -- leaves turds if we optimize this */
523             {
524               gc = state->erase_gc;
525               XFillArc (state->dpy, state->b, gc,
526                         x1a, y1a, x2a-x1a, y2a-y1a,
527                         0, 360*64);
528             }
529         }
530 #endif /* !HAVE_JWXYZ */
531
532       if (state->mouse_ball == a)
533         gc = state->draw_gc2;
534       else
535         gc = state->draw_gc;
536
537       XFillArc (state->dpy, state->b, gc,
538                 x1b, y1b, x2b-x1b, y2b-y1b,
539                 0, 360*64);
540
541       if (state->shake_p)
542         {
543           /* distance this ball moved this frame */
544           float d = ((state->px[a] - state->opx[a]) *
545                      (state->px[a] - state->opx[a]) +
546                      (state->py[a] - state->opy[a]) *
547                      (state->py[a] - state->opy[a]));
548           if (d > max_d) max_d = d;
549         }
550
551       state->opx[a] = state->px[a];
552       state->opy[a] = state->py[a];
553     }
554
555   if (state->fps_p
556 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
557       && (state->backb ? state->dbeclear_p : 1)
558 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
559       )
560     draw_fps_string(state);
561
562 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
563   if (state->backb)
564     {
565       XdbeSwapInfo info[1];
566       info[0].swap_window = state->window;
567       info[0].swap_action = (state->dbeclear_p ? XdbeBackground : XdbeUndefined);
568       XdbeSwapBuffers (state->dpy, info, 1);
569     }
570   else
571 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
572   if (state->dbuf)
573     {
574       XCopyArea (state->dpy, state->b, state->window, state->erase_gc,
575                  0, 0, state->xgwa.width, state->xgwa.height, 0, 0);
576     }
577
578   if (state->shake_p && state->time_since_shake > 5)
579     {
580       max_d /= state->max_radius;
581       if (max_d < state->shake_threshold ||  /* when its stable */
582           state->time_since_shake > 30)      /* or when 30 secs has passed */
583         {
584           shake (state);
585         }
586     }
587
588   check_wall_clock (state, max_d);
589 }
590
591
592 /* Implements the laws of physics: move balls to their new positions.
593  */
594 static void
595 update_balls (b_state *state)
596 {
597   int a, b;
598   float d, vxa, vya, vxb, vyb, dd, cdx, cdy;
599   float ma, mb, vca, vcb, dva, dvb;
600   float dee2;
601
602   check_window_moved (state);
603
604   /* If we're currently tracking the mouse, update that ball first.
605    */
606   if (state->mouse_ball != 0)
607     {
608       int mouse_x, mouse_y;
609       query_mouse (state, &mouse_x, &mouse_y);
610       state->px[state->mouse_ball] = mouse_x;
611       state->py[state->mouse_ball] = mouse_y;
612       state->vx[state->mouse_ball] =
613         (0.1 *
614          (state->px[state->mouse_ball] - state->opx[state->mouse_ball]) *
615          state->tc);
616       state->vy[state->mouse_ball] =
617         (0.1 *
618          (state->py[state->mouse_ball] - state->opy[state->mouse_ball]) *
619          state->tc);
620     }
621
622   /* For each ball, compute the influence of every other ball. */
623   for (a=1; a <= state->count -  1; a++)
624     for (b=a + 1; b <= state->count; b++)
625       {
626          d = ((state->px[a] - state->px[b]) *
627               (state->px[a] - state->px[b]) +
628               (state->py[a] - state->py[b]) *
629               (state->py[a] - state->py[b]));
630          dee2 = (state->r[a] + state->r[b]) *
631                 (state->r[a] + state->r[b]);
632          if (d < dee2)
633          {
634             state->collision_count++;
635             d = sqrt(d);
636             dd = state->r[a] + state->r[b] - d;
637
638             cdx = (state->px[b] - state->px[a]) / d;
639             cdy = (state->py[b] - state->py[a]) / d;
640
641             /* Move each ball apart from the other by half the
642              * 'collision' distance.
643              */
644             state->px[a] -= 0.5 * dd * cdx;
645             state->py[a] -= 0.5 * dd * cdy;
646             state->px[b] += 0.5 * dd * cdx;
647             state->py[b] += 0.5 * dd * cdy;
648
649             ma = state->m[a];
650             mb = state->m[b];
651
652             vxa = state->vx[a];
653             vya = state->vy[a];
654             vxb = state->vx[b];
655             vyb = state->vy[b];
656
657             vca = vxa * cdx + vya * cdy; /* the component of each velocity */
658             vcb = vxb * cdx + vyb * cdy; /* along the axis of the collision */
659
660             /* elastic collison */
661             dva = (vca * (ma - mb) + vcb * 2 * mb) / (ma + mb) - vca;
662             dvb = (vcb * (mb - ma) + vca * 2 * ma) / (ma + mb) - vcb;
663
664             dva *= state->e; /* some energy lost to inelasticity */
665             dvb *= state->e;
666
667 #if 0
668             dva += (frand (50) - 25) / ma;   /* q: why are elves so chaotic? */
669             dvb += (frand (50) - 25) / mb;   /* a: brownian motion. */
670 #endif
671
672             vxa += dva * cdx;
673             vya += dva * cdy;
674             vxb += dvb * cdx;
675             vyb += dvb * cdy;
676
677             state->vx[a] = vxa;
678             state->vy[a] = vya;
679             state->vx[b] = vxb;
680             state->vy[b] = vyb;
681          }
682       }
683
684    /* Force all balls to be on screen.
685     */
686   for (a=1; a <= state->count; a++)
687     {
688       if (state->px[a] <= (state->xmin + state->r[a]))
689         {
690           state->px[a] = state->xmin + state->r[a];
691           state->vx[a] = -state->vx[a] * state->e;
692         }
693       if (state->px[a] >= (state->xmax - state->r[a]))
694         {
695           state->px[a] = state->xmax - state->r[a];
696           state->vx[a] = -state->vx[a] * state->e;
697         }
698       if (state->py[a] <= (state->ymin + state->r[a]))
699         {
700           state->py[a] = state->ymin + state->r[a];
701           state->vy[a] = -state->vy[a] * state->e;
702         }
703       if (state->py[a] >= (state->ymax - state->r[a]))
704         {
705           state->py[a] = state->ymax - state->r[a];
706           state->vy[a] = -state->vy[a] * state->e;
707         }
708     }
709
710   /* Apply gravity to all balls.
711    */
712   for (a=1; a <= state->count; a++)
713     if (a != state->mouse_ball)
714       {
715         state->vx[a] += state->accx * state->tc;
716         state->vy[a] += state->accy * state->tc;
717         state->px[a] += state->vx[a] * state->tc;
718         state->py[a] += state->vy[a] * state->tc;
719       }
720 }
721
722
723 /* Handle X events, specifically, allow a ball to be picked up with the mouse.
724  */
725 static Bool
726 fluidballs_event (Display *dpy, Window window, void *closure, XEvent *event)
727 {
728   b_state *state = (b_state *) closure;
729
730   if (event->xany.type == ButtonPress)
731     {
732       int i, rx, ry;
733       XTranslateCoordinates (dpy, window, RootWindow (dpy, DefaultScreen(dpy)),
734                              event->xbutton.x, event->xbutton.y, &rx, &ry,
735                              &window);
736
737       if (state->mouse_ball != 0)  /* second down-click?  drop the ball. */
738         {
739           state->mouse_ball = 0;
740           return True;
741         }
742       else
743         {
744           /* When trying to pick up a ball, first look for a click directly
745              inside the ball; but if we don't find it, expand the radius
746              outward until we find something nearby.
747            */
748           float max = state->max_radius * 4;
749           float step = max / 10;
750           float r2;
751           for (r2 = step; r2 < max; r2 += step) {
752             for (i = 1; i <= state->count; i++)
753               {
754                 float d = ((state->px[i] - rx) * (state->px[i] - rx) +
755                            (state->py[i] - ry) * (state->py[i] - ry));
756                 float r = state->r[i];
757                 if (r2 > r) r = r2;
758                 if (d < r*r)
759                   {
760                     state->mouse_ball = i;
761                     return True;
762                   }
763               }
764           }
765         }
766       return True;
767     }
768   else if (event->xany.type == ButtonRelease)   /* drop the ball */
769     {
770       state->mouse_ball = 0;
771       return True;
772     }
773
774   return False;
775 }
776
777 static unsigned long
778 fluidballs_draw (Display *dpy, Window window, void *closure)
779 {
780   b_state *state = (b_state *) closure;
781   repaint_balls(state);
782   update_balls(state);
783   return state->delay;
784 }
785
786 static void
787 fluidballs_reshape (Display *dpy, Window window, void *closure, 
788                  unsigned int w, unsigned int h)
789 {
790 }
791
792 static void
793 fluidballs_free (Display *dpy, Window window, void *closure)
794 {
795   b_state *state = (b_state *) closure;
796   free (state);
797 }
798
799
800 static const char *fluidballs_defaults [] = {
801   ".background:         black",
802   ".foreground:         yellow",
803   ".textColor:          yellow",
804   "*mouseForeground:    white",
805   "*delay:              10000",
806   "*count:              300",
807   "*size:               25",
808   "*random:             True",
809   "*gravity:            0.01",
810   "*wind:               0.00",
811   "*elasticity:         0.97",
812   "*timeScale:          1.0",
813   "*shake:              True",
814   "*shakeThreshold:     0.015",
815   "*doubleBuffer:       True",
816 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
817   "*useDBE:             True",
818   "*useDBEClear:        True",
819 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
820 #ifdef HAVE_MOBILE
821   "*ignoreRotation:     True",
822 #endif
823   0
824 };
825
826 static XrmOptionDescRec fluidballs_options [] = {
827   { "-delay",           ".delay",       XrmoptionSepArg, 0 },
828   { "-count",           ".count",       XrmoptionSepArg, 0 },
829   { "-size",            ".size",        XrmoptionSepArg, 0 },
830   { "-count",           ".count",       XrmoptionSepArg, 0 },
831   { "-gravity",         ".gravity",     XrmoptionSepArg, 0 },
832   { "-wind",            ".wind",        XrmoptionSepArg, 0 },
833   { "-elasticity",      ".elasticity",  XrmoptionSepArg, 0 },
834   { "-shake",           ".shake",       XrmoptionNoArg, "True" },
835   { "-no-shake",        ".shake",       XrmoptionNoArg, "False" },
836   { "-random",          ".random",      XrmoptionNoArg, "True" },
837   { "-no-random",       ".random",      XrmoptionNoArg, "False" },
838   { "-nonrandom",       ".random",      XrmoptionNoArg, "False" },
839   { "-db",              ".doubleBuffer", XrmoptionNoArg,  "True" },
840   { "-no-db",           ".doubleBuffer", XrmoptionNoArg,  "False" },
841   { 0, 0, 0, 0 }
842 };
843
844
845 XSCREENSAVER_MODULE ("FluidBalls", fluidballs)