From http://www.jwz.org/xscreensaver/xscreensaver-5.39.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 > 2560) state->max_radius *= 2;  /* Retina displays */
300
301   if (state->xgwa.width < 100 || state->xgwa.height < 100) /* tiny window */
302     {
303       if (state->max_radius > 5)
304         state->max_radius = 5;
305     }
306
307   state->random_sizes_p = get_boolean_resource (dpy, "random", "Random");
308
309   /* If the initial window size is too small to hold all these balls,
310      make fewer of them...
311    */
312   {
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;
322   }
323
324   state->accx = get_float_resource (dpy, "wind", "Wind");
325   if (state->accx < -1.0 || state->accx > 1.0) state->accx = 0;
326
327   state->accy = get_float_resource (dpy, "gravity", "Gravity");
328   if (state->accy < -1.0 || state->accy > 1.0) state->accy = 0.01;
329
330   state->e = get_float_resource (dpy, "elasticity", "Elacitcity");
331   if (state->e < 0.2 || state->e > 1.0) state->e = 0.97;
332
333   state->tc = get_float_resource (dpy, "timeScale", "TimeScale");
334   if (state->tc <= 0 || state->tc > 10) state->tc = 1.0;
335
336   state->shake_p = get_boolean_resource (dpy, "shake", "Shake");
337   state->shake_threshold = get_float_resource (dpy, "shakeThreshold",
338                                                "ShakeThreshold");
339   state->time_tick = 999999;
340
341 # ifdef HAVE_MOBILE     /* Always obey real-world gravity */
342   state->shake_p = False;
343 # endif
344
345
346   state->fps_p = get_boolean_resource (dpy, "doFPS", "DoFPS");
347   if (state->fps_p)
348     {
349       XFontStruct *font;
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);
353       if (!font) abort();
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;
361     }
362
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));
371
372   for (i=1; i<=state->count; i++)
373     {
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;
378
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;*/
383
384       /* state->m[i] = pow(state->r[i],2) * M_PI; */
385       state->m[i] = pow(state->r[i],3) * M_PI * 1.3333;
386     }
387
388   memcpy (state->opx, state->px, sizeof (*state->opx) * (state->count + 1));
389   memcpy (state->opy, state->py, sizeof (*state->opx) * (state->count + 1));
390
391   return state;
392 }
393
394
395 /* Messes with gravity: permute "down" to be in a random direction.
396  */
397 static void
398 shake (b_state *state)
399 {
400   float a = state->accx;
401   float b = state->accy;
402   int i = random() % 4;
403
404   switch (i)
405     {
406     case 0:
407       state->accx = a;
408       state->accy = b;
409       break;
410     case 1:
411       state->accx = -a;
412       state->accy = -b;
413       break;
414     case 2:
415       state->accx = b;
416       state->accy = a;
417       break;
418     case 3:
419       state->accx = -b;
420       state->accy = -a;
421       break;
422     default:
423       abort();
424       break;
425     }
426
427   state->time_since_shake = 0;
428   recolor (state);
429 }
430
431
432 /* Look at the current time, and update state->time_since_shake.
433    Draw the FPS display if desired.
434  */
435 static void
436 check_wall_clock (b_state *state, float max_d)
437 {
438   state->frame_count++;
439   
440   if (state->time_tick++ > 20)  /* don't call gettimeofday() too often -- it's slow. */
441     {
442       struct timeval now;
443 # ifdef GETTIMEOFDAY_TWO_ARGS
444       struct timezone tzp;
445       gettimeofday(&now, &tzp);
446 # else
447       gettimeofday(&now);
448 # endif
449
450       if (state->last_time.tv_sec == 0)
451         state->last_time = now;
452
453       state->time_tick = 0;
454       if (now.tv_sec == state->last_time.tv_sec)
455         return;
456
457       state->time_since_shake += (now.tv_sec - state->last_time.tv_sec);
458
459 # ifdef HAVE_MOBILE     /* Always obey real-world gravity */
460       {
461         float a = fabs (fabs(state->accx) > fabs(state->accy)
462                         ? state->accx : state->accy);
463         int rot = current_device_rotation();
464         switch (rot) {
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;
469         default: break;
470         }
471       }
472 # endif /* HAVE_MOBILE */
473
474       if (state->fps_p) 
475         {
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;
480           
481           sprintf (state->fps_str, "Collisions: %.3f/frame  Max motion: %.3f",
482                    cps/fps, max_d);
483           
484           draw_fps_string(state);
485         }
486
487       state->frame_count = 0;
488       state->collision_count = 0;
489       state->last_time = now;
490     }
491 }
492
493 /* Erases the balls at their previous positions, and draws the new ones.
494  */
495 static void
496 repaint_balls (b_state *state)
497 {
498   int a;
499 # ifndef HAVE_JWXYZ
500   int x1a, x2a, y1a, y2a;
501 # endif
502   int x1b, x2b, y1b, y2b;
503   float max_d = 0;
504
505 #ifdef HAVE_JWXYZ       /* Don't second-guess Quartz's double-buffering */
506   XClearWindow (state->dpy, state->b);
507 #endif
508
509   for (a=1; a <= state->count; a++)
510     {
511       GC gc;
512 # ifndef HAVE_JWXYZ
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);
517 # endif
518
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);
523
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 */
528         {
529 /*        if (x1a != x1b || y1a != y1b)   -- leaves turds if we optimize this */
530             {
531               gc = state->erase_gc;
532               XFillArc (state->dpy, state->b, gc,
533                         x1a, y1a, x2a-x1a, y2a-y1a,
534                         0, 360*64);
535             }
536         }
537 #endif /* !HAVE_JWXYZ */
538
539       if (state->mouse_ball == a)
540         gc = state->draw_gc2;
541       else
542         gc = state->draw_gc;
543
544       XFillArc (state->dpy, state->b, gc,
545                 x1b, y1b, x2b-x1b, y2b-y1b,
546                 0, 360*64);
547
548       if (state->shake_p)
549         {
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;
556         }
557
558       state->opx[a] = state->px[a];
559       state->opy[a] = state->py[a];
560     }
561
562   if (state->fps_p
563 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
564       && (state->backb ? state->dbeclear_p : 1)
565 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
566       )
567     draw_fps_string(state);
568
569 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
570   if (state->backb)
571     {
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);
576     }
577   else
578 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
579   if (state->dbuf)
580     {
581       XCopyArea (state->dpy, state->b, state->window, state->erase_gc,
582                  0, 0, state->xgwa.width, state->xgwa.height, 0, 0);
583     }
584
585   if (state->shake_p && state->time_since_shake > 5)
586     {
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 */
590         {
591           shake (state);
592         }
593     }
594
595   check_wall_clock (state, max_d);
596 }
597
598
599 /* Implements the laws of physics: move balls to their new positions.
600  */
601 static void
602 update_balls (b_state *state)
603 {
604   int a, b;
605   float d, vxa, vya, vxb, vyb, dd, cdx, cdy;
606   float ma, mb, vca, vcb, dva, dvb;
607   float dee2;
608
609   check_window_moved (state);
610
611   /* If we're currently tracking the mouse, update that ball first.
612    */
613   if (state->mouse_ball != 0)
614     {
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] =
620         (0.1 *
621          (state->px[state->mouse_ball] - state->opx[state->mouse_ball]) *
622          state->tc);
623       state->vy[state->mouse_ball] =
624         (0.1 *
625          (state->py[state->mouse_ball] - state->opy[state->mouse_ball]) *
626          state->tc);
627     }
628
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++)
632       {
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]);
639          if (d < dee2)
640          {
641             state->collision_count++;
642             d = sqrt(d);
643             dd = state->r[a] + state->r[b] - d;
644
645             cdx = (state->px[b] - state->px[a]) / d;
646             cdy = (state->py[b] - state->py[a]) / d;
647
648             /* Move each ball apart from the other by half the
649              * 'collision' distance.
650              */
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;
655
656             ma = state->m[a];
657             mb = state->m[b];
658
659             vxa = state->vx[a];
660             vya = state->vy[a];
661             vxb = state->vx[b];
662             vyb = state->vy[b];
663
664             vca = vxa * cdx + vya * cdy; /* the component of each velocity */
665             vcb = vxb * cdx + vyb * cdy; /* along the axis of the collision */
666
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;
670
671             dva *= state->e; /* some energy lost to inelasticity */
672             dvb *= state->e;
673
674 #if 0
675             dva += (frand (50) - 25) / ma;   /* q: why are elves so chaotic? */
676             dvb += (frand (50) - 25) / mb;   /* a: brownian motion. */
677 #endif
678
679             vxa += dva * cdx;
680             vya += dva * cdy;
681             vxb += dvb * cdx;
682             vyb += dvb * cdy;
683
684             state->vx[a] = vxa;
685             state->vy[a] = vya;
686             state->vx[b] = vxb;
687             state->vy[b] = vyb;
688          }
689       }
690
691    /* Force all balls to be on screen.
692     */
693   for (a=1; a <= state->count; a++)
694     {
695       if (state->px[a] <= (state->xmin + state->r[a]))
696         {
697           state->px[a] = state->xmin + state->r[a];
698           state->vx[a] = -state->vx[a] * state->e;
699         }
700       if (state->px[a] >= (state->xmax - state->r[a]))
701         {
702           state->px[a] = state->xmax - state->r[a];
703           state->vx[a] = -state->vx[a] * state->e;
704         }
705       if (state->py[a] <= (state->ymin + state->r[a]))
706         {
707           state->py[a] = state->ymin + state->r[a];
708           state->vy[a] = -state->vy[a] * state->e;
709         }
710       if (state->py[a] >= (state->ymax - state->r[a]))
711         {
712           state->py[a] = state->ymax - state->r[a];
713           state->vy[a] = -state->vy[a] * state->e;
714         }
715     }
716
717   /* Apply gravity to all balls.
718    */
719   for (a=1; a <= state->count; a++)
720     if (a != state->mouse_ball)
721       {
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;
726       }
727 }
728
729
730 /* Handle X events, specifically, allow a ball to be picked up with the mouse.
731  */
732 static Bool
733 fluidballs_event (Display *dpy, Window window, void *closure, XEvent *event)
734 {
735   b_state *state = (b_state *) closure;
736
737   if (event->xany.type == ButtonPress)
738     {
739       int i, rx, ry;
740       XTranslateCoordinates (dpy, window, RootWindow (dpy, DefaultScreen(dpy)),
741                              event->xbutton.x, event->xbutton.y, &rx, &ry,
742                              &window);
743
744       if (state->mouse_ball != 0)  /* second down-click?  drop the ball. */
745         {
746           state->mouse_ball = 0;
747           return True;
748         }
749       else
750         {
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.
754            */
755           float max = state->max_radius * 4;
756           float step = max / 10;
757           float r2;
758           for (r2 = step; r2 < max; r2 += step) {
759             for (i = 1; i <= state->count; i++)
760               {
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];
764                 if (r2 > r) r = r2;
765                 if (d < r*r)
766                   {
767                     state->mouse_ball = i;
768                     return True;
769                   }
770               }
771           }
772         }
773       return True;
774     }
775   else if (event->xany.type == ButtonRelease)   /* drop the ball */
776     {
777       state->mouse_ball = 0;
778       return True;
779     }
780
781   return False;
782 }
783
784 static unsigned long
785 fluidballs_draw (Display *dpy, Window window, void *closure)
786 {
787   b_state *state = (b_state *) closure;
788   repaint_balls(state);
789   update_balls(state);
790   return state->delay;
791 }
792
793 static void
794 fluidballs_reshape (Display *dpy, Window window, void *closure, 
795                  unsigned int w, unsigned int h)
796 {
797 }
798
799 static void
800 fluidballs_free (Display *dpy, Window window, void *closure)
801 {
802   b_state *state = (b_state *) closure;
803   free (state);
804 }
805
806
807 static const char *fluidballs_defaults [] = {
808   ".background:         black",
809   ".foreground:         yellow",
810   ".textColor:          yellow",
811   "*mouseForeground:    white",
812   "*delay:              10000",
813   "*count:              300",
814   "*size:               25",
815   "*random:             True",
816   "*gravity:            0.01",
817   "*wind:               0.00",
818   "*elasticity:         0.97",
819   "*timeScale:          1.0",
820   "*shake:              True",
821   "*shakeThreshold:     0.015",
822   "*doubleBuffer:       True",
823 #ifdef HAVE_DOUBLE_BUFFER_EXTENSION
824   "*useDBE:             True",
825   "*useDBEClear:        True",
826 #endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
827 #ifdef HAVE_MOBILE
828   "*ignoreRotation:     True",
829 #endif
830   0
831 };
832
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" },
848   { 0, 0, 0, 0 }
849 };
850
851
852 XSCREENSAVER_MODULE ("FluidBalls", fluidballs)