http://packetstormsecurity.org/UNIX/admin/xscreensaver-4.03.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
16 /* cjb notes
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
23 #include <math.h>
24 #include "screenhack.h"
25 #include <stdio.h>
26
27
28 typedef struct {
29   Display *dpy;
30   Window window;
31   XWindowAttributes xgwa;
32
33   GC draw_gc;           /* most of the balls */
34   GC draw_gc2;          /* the ball being dragged with the mouse */
35   GC erase_gc;
36   XColor fg;
37   XColor fg2;
38
39   int count;            /* number of balls */
40   float xmin, ymin;     /* rectangle of window, relative to root */
41   float xmax, ymax;
42
43   int mouse_ball;       /* index of ball being dragged, or 0 if none. */
44
45   float tc;             /* time constant (time-warp multiplier) */
46   float accx;           /* horizontal acceleration (wind) */
47   float accy;           /* vertical acceleration (gravity) */
48
49   float *vx,  *vy;      /* current ball velocities */
50   float *px,  *py;      /* current ball positions */
51   float *opx, *opy;     /* previous ball positions */
52   float *r;             /* ball radiuses */
53
54   float *m;             /* ball mass, precalculated */
55   float e;              /* coefficient of friction, I think? */
56   float max_radius;     /* largest radius of any ball */
57
58   Bool random_sizes_p;  /* Whether balls should be various sizes up to max. */
59   Bool shake_p;         /* Whether to mess with gravity when things settle. */
60   float shake_threshold;
61   int time_since_shake;
62
63   Bool fps_p;           /* Whether to draw some text at the bottom. */
64   GC font_gc;
65   int font_height;
66   int font_baseline;
67   int frame_count;
68   int collision_count;
69
70 } b_state;
71
72
73
74 /* Finds the origin of the window relative to the root window, by
75    walking up the window tree until it reaches the top.
76  */
77 static void
78 window_origin (Display *dpy, Window window, int *x, int *y)
79 {
80   Window root, parent, *kids;
81   unsigned int nkids;
82   XWindowAttributes xgwa;
83   int wx, wy;
84   XGetWindowAttributes (dpy, window, &xgwa);
85
86   wx = xgwa.x;
87   wy = xgwa.y;
88
89   kids = 0;
90   *x = 0;
91   *y = 0;
92
93   if (XQueryTree (dpy, window, &root, &parent, &kids, &nkids))
94     {
95       if (parent && parent != root)
96         {
97           int px, py;
98           window_origin (dpy, parent, &px, &py);
99           wx += px;
100           wy += py;
101         }
102     }
103   if (kids) XFree (kids);
104   *x = wx;
105   *y = wy;
106 }
107
108
109 /* Queries the window position to see if the window has moved or resized.
110    We poll this instead of waiting for ConfigureNotify events, because
111    when the window manager moves the window, only one ConfigureNotify
112    comes in: at the end of the motion.  If we poll, we can react to the
113    new position while the window is still being moved.  (Assuming the WM
114    does OpaqueMove, of course.)
115  */
116 static void
117 check_window_moved (b_state *state)
118 {
119   float oxmin = state->xmin;
120   float oymin = state->ymin;
121   float oymax = state->ymax;
122   int wx, wy;
123   XGetWindowAttributes (state->dpy, state->window, &state->xgwa);
124   window_origin (state->dpy, state->window, &wx, &wy);
125   state->xmin = wx;
126   state->ymin = wy;
127   state->xmax = state->xmin + state->xgwa.width;
128   state->ymax = state->ymin + state->xgwa.height - state->font_height;
129
130   /* Only need to erase the window if the origin moved */
131   if (oxmin != state->xmin || oymin != state->ymin)
132     XClearWindow (state->dpy, state->window);
133   else if (state->fps_p && oymax != state->ymax)
134     XFillRectangle (state->dpy, state->window, state->erase_gc,
135                     0, state->xgwa.height - state->font_height,
136                     state->xgwa.width, state->font_height);
137
138 }
139
140
141 /* Returns the position of the mouse relative to the root window.
142  */
143 static void
144 query_mouse (b_state *state, int *x, int *y)
145 {
146   Window root1, child1;
147   int mouse_x, mouse_y, root_x, root_y;
148   unsigned int mask;
149   if (XQueryPointer (state->dpy, state->window, &root1, &child1,
150                      &root_x, &root_y, &mouse_x, &mouse_y, &mask))
151     {
152       *x = root_x;
153       *y = root_y;
154     }
155   else
156     {
157       *x = -9999;
158       *y = -9999;
159     }
160 }
161
162 /* Re-pick the colors of the balls, and the mouse-ball.
163  */
164 static void
165 recolor (b_state *state)
166 {
167   if (state->fg.flags)
168     XFreeColors (state->dpy, state->xgwa.colormap, &state->fg.pixel, 1, 0);
169   if (state->fg2.flags)
170     XFreeColors (state->dpy, state->xgwa.colormap, &state->fg2.pixel, 1, 0);
171
172   state->fg.flags  = DoRed|DoGreen|DoBlue;
173   state->fg.red    = 0x8888 + (random() % 0x8888);
174   state->fg.green  = 0x8888 + (random() % 0x8888);
175   state->fg.blue   = 0x8888 + (random() % 0x8888);
176
177   state->fg2.flags = DoRed|DoGreen|DoBlue;
178   state->fg2.red   = 0x8888 + (random() % 0x8888);
179   state->fg2.green = 0x8888 + (random() % 0x8888);
180   state->fg2.blue  = 0x8888 + (random() % 0x8888);
181
182   if (XAllocColor (state->dpy, state->xgwa.colormap, &state->fg))
183     XSetForeground (state->dpy, state->draw_gc,  state->fg.pixel);
184
185   if (XAllocColor (state->dpy, state->xgwa.colormap, &state->fg2))
186     XSetForeground (state->dpy, state->draw_gc2, state->fg2.pixel);
187 }
188
189
190 /* Initialize the state structure and various X data.
191  */
192 static b_state *
193 init_balls (Display *dpy, Window window)
194 {
195   int i;
196   float extx, exty;
197   b_state *state = (b_state *) calloc (1, sizeof(*state));
198   XGCValues gcv;
199
200   state->dpy = dpy;
201   state->window = window;
202
203   check_window_moved (state);
204
205   /* Select ButtonRelease events on the external window, if no other app has
206      already selected it (only one app can select it at a time: BadAccess. */
207   if (! (state->xgwa.all_event_masks & ButtonReleaseMask))
208     XSelectInput (state->dpy, state->window,
209                   state->xgwa.your_event_mask | ButtonReleaseMask);
210
211   gcv.foreground = get_pixel_resource("foreground", "Foreground",
212                                       state->dpy, state->xgwa.colormap);
213   gcv.background = get_pixel_resource("background", "Background",
214                                       state->dpy, state->xgwa.colormap);
215   state->draw_gc = XCreateGC (state->dpy, state->window,
216                               GCForeground|GCBackground, &gcv);
217
218   gcv.foreground = get_pixel_resource("mouseForeground", "MouseForeground",
219                                       state->dpy, state->xgwa.colormap);
220   state->draw_gc2 = XCreateGC (state->dpy, state->window,
221                                GCForeground|GCBackground, &gcv);
222
223   gcv.foreground = gcv.background;
224   state->erase_gc = XCreateGC (state->dpy, state->window,
225                                GCForeground|GCBackground, &gcv);
226
227   recolor (state);
228
229   extx = state->xmax - state->xmin;
230   exty = state->ymax - state->ymin;
231
232   state->count = get_integer_resource ("count", "Count");
233   if (state->count < 1) state->count = 20;
234
235   state->max_radius = get_float_resource ("size", "Size") / 2;
236   if (state->max_radius < 1.0) state->max_radius = 1.0;
237
238   state->random_sizes_p = get_boolean_resource ("random", "Random");
239
240   state->accx = get_float_resource ("wind", "Wind");
241   if (state->accx < -1.0 || state->accx > 1.0) state->accx = 0;
242
243   state->accy = get_float_resource ("gravity", "Gravity");
244   if (state->accy < -1.0 || state->accy > 1.0) state->accy = 0.01;
245
246   state->e = get_float_resource ("friction", "Friction");
247   if (state->e < 0.2 || state->e > 1.0) state->e = 0.97;
248
249   state->tc = get_float_resource ("timeScale", "TimeScale");
250   if (state->tc <= 0 || state->tc > 10) state->tc = 1.0;
251
252   state->shake_p = get_boolean_resource ("shake", "Shake");
253   state->shake_threshold = get_float_resource ("shakeThreshold",
254                                                "ShakeThreshold");
255
256   state->fps_p = get_boolean_resource ("doFPS", "DoFPS");
257   if (state->fps_p)
258     {
259       XFontStruct *font;
260       char *fontname = get_string_resource ("font", "Font");
261       const char *def_font = "fixed";
262       if (!fontname || !*fontname) fontname = (char *)def_font;
263       font = XLoadQueryFont (dpy, fontname);
264       if (!font) font = XLoadQueryFont (dpy, def_font);
265       if (!font) exit(-1);
266       gcv.font = font->fid;
267       gcv.foreground = get_pixel_resource("textColor", "Foreground",
268                                           state->dpy, state->xgwa.colormap);
269       state->font_gc = XCreateGC(dpy, window,
270                                  GCFont|GCForeground|GCBackground, &gcv);
271       state->font_height = font->ascent + font->descent;
272       state->font_baseline = font->descent;
273     }
274
275   state->m   = (float *) malloc (sizeof (*state->m)   * (state->count + 1));
276   state->r   = (float *) malloc (sizeof (*state->r)   * (state->count + 1));
277   state->vx  = (float *) malloc (sizeof (*state->vx)  * (state->count + 1));
278   state->vy  = (float *) malloc (sizeof (*state->vy)  * (state->count + 1));
279   state->px  = (float *) malloc (sizeof (*state->px)  * (state->count + 1));
280   state->py  = (float *) malloc (sizeof (*state->py)  * (state->count + 1));
281   state->opx = (float *) malloc (sizeof (*state->opx) * (state->count + 1));
282   state->opy = (float *) malloc (sizeof (*state->opy) * (state->count + 1));
283
284   for (i=1; i<=state->count; i++)
285     {
286       state->px[i] = frand(extx) + state->xmin;
287       state->py[i] = frand(exty) + state->ymin;
288       state->vx[i] = frand(0.2) - 0.1;
289       state->vy[i] = frand(0.2) - 0.1;
290
291       state->r[i] = (state->random_sizes_p
292                      ? ((0.2 + frand(0.8)) * state->max_radius)
293                      : state->max_radius);
294       /*state->r[i] = pow(frand(1.0), state->sizegamma) * state->max_radius;*/
295
296       /* state->m[i] = pow(state->r[i],2) * M_PI; */
297       state->m[i] = pow(state->r[i],3) * M_PI * 1.3333;
298     }
299
300   return state;
301 }
302
303
304 /* Messes with gravity: permute "down" to be in a random direction.
305  */
306 static void
307 shake (b_state *state)
308 {
309   float a = state->accx;
310   float b = state->accy;
311   int i = random() % 4;
312
313   switch (i)
314     {
315     case 0:
316       state->accx = a;
317       state->accy = b;
318       break;
319     case 1:
320       state->accx = -a;
321       state->accy = -b;
322       break;
323     case 2:
324       state->accx = b;
325       state->accy = a;
326       break;
327     case 3:
328       state->accx = -b;
329       state->accy = -a;
330       break;
331     default:
332       abort();
333       break;
334     }
335
336   state->time_since_shake = 0;
337   recolor (state);
338 }
339
340
341 /* Look at the current time, and update state->time_since_shake.
342    Draw the FPS display if desired.
343  */
344 static void
345 check_wall_clock (b_state *state, float max_d)
346 {
347   static int tick = 0;
348   state->frame_count++;
349
350   if (tick++ > 20)  /* don't call gettimeofday() too often -- it's slow. */
351     {
352       static struct timeval last = { 0, };
353       struct timeval now;
354 # ifdef GETTIMEOFDAY_TWO_ARGS
355       struct timezone tzp;
356       gettimeofday(&now, &tzp);
357 # else
358       gettimeofday(&now);
359 # endif
360
361       if (last.tv_sec == 0)
362         last = now;
363
364       tick = 0;
365       if (now.tv_sec == last.tv_sec)
366         return;
367
368       state->time_since_shake += (now.tv_sec - last.tv_sec);
369
370       if (state->fps_p)
371         {
372           static char buf[1024];
373           float elapsed = ((now.tv_sec  + (now.tv_usec  / 1000000.0)) -
374                            (last.tv_sec + (last.tv_usec / 1000000.0)));
375           float fps = state->frame_count / elapsed;
376           float cps = state->collision_count / elapsed;
377
378           sprintf (buf, " FPS: %.2f  Collisions: %.f/frame  Max motion: %.3f",
379                    fps, cps/fps, max_d);
380
381           XFillRectangle (state->dpy, state->window, state->erase_gc,
382                           0, state->xgwa.height - state->font_height,
383                           state->xgwa.width, state->font_height);
384           XDrawImageString (state->dpy, state->window, state->font_gc,
385                             0, state->xgwa.height - state->font_baseline,
386                             buf, strlen(buf));
387         }
388
389       state->frame_count = 0;
390       state->collision_count = 0;
391       last = now;
392     }
393 }
394
395
396 /* Erases the balls at their previous positions, and draws the new ones.
397  */
398 static void
399 repaint_balls (b_state *state)
400 {
401   int a;
402   int x1a, x2a, y1a, y2a;
403   int x1b, x2b, y1b, y2b;
404   float max_d = 0;
405
406   for (a=1; a <= state->count; a++)
407     {
408       GC gc;
409       x1a = (state->opx[a] - state->r[a] - state->xmin);
410       y1a = (state->opy[a] - state->r[a] - state->ymin);
411       x2a = (state->opx[a] + state->r[a] - state->xmin);
412       y2a = (state->opy[a] + state->r[a] - state->ymin);
413
414       x1b = (state->px[a] - state->r[a] - state->xmin);
415       y1b = (state->py[a] - state->r[a] - state->ymin);
416       x2b = (state->px[a] + state->r[a] - state->xmin);
417       y2b = (state->py[a] + state->r[a] - state->ymin);
418
419 /*      if (x1a != x1b || y1a != y1b)   -- leaves turds if we optimize this */
420         {
421           gc = state->erase_gc;
422           XFillArc (state->dpy, state->window, gc,
423                     x1a, y1a, x2a-x1a, y2a-y1a,
424                     0, 360*64);
425         }
426
427       if (state->mouse_ball == a)
428         gc = state->draw_gc2;
429       else
430         gc = state->draw_gc;
431
432       XFillArc (state->dpy, state->window, gc,
433                 x1b, y1b, x2b-x1b, y2b-y1b,
434                 0, 360*64);
435
436       if (state->shake_p)
437         {
438           /* distance this ball moved this frame */
439           float d = ((state->px[a] - state->opx[a]) *
440                      (state->px[a] - state->opx[a]) +
441                      (state->py[a] - state->opy[a]) *
442                      (state->py[a] - state->opy[a]));
443           if (d > max_d) max_d = d;
444         }
445
446       state->opx[a] = state->px[a];
447       state->opy[a] = state->py[a];
448     }
449
450   if (state->shake_p && state->time_since_shake > 5)
451     {
452       max_d /= state->max_radius;
453       if (max_d < state->shake_threshold ||  /* when its stable */
454           state->time_since_shake > 30)      /* or when 30 secs has passed */
455         {
456           shake (state);
457         }
458     }
459
460   check_wall_clock (state, max_d);
461 }
462
463
464 /* Implements the laws of physics: move balls to their new positions.
465  */
466 static void
467 update_balls (b_state *state)
468 {
469   int a, b;
470   float d, vxa, vya, vxb, vyb, dd, cdx, cdy;
471   float ma, mb, vela, velb, vela1, velb1;
472   float dee2;
473
474   check_window_moved (state);
475
476   /* If we're currently tracking the mouse, update that ball first.
477    */
478   if (state->mouse_ball != 0)
479     {
480       int mouse_x, mouse_y;
481       query_mouse (state, &mouse_x, &mouse_y);
482       state->px[state->mouse_ball] = mouse_x;
483       state->py[state->mouse_ball] = mouse_y;
484       state->vx[state->mouse_ball] =
485         (0.1 *
486          (state->px[state->mouse_ball] - state->opx[state->mouse_ball]) *
487          state->tc);
488       state->vy[state->mouse_ball] =
489         (0.1 *
490          (state->py[state->mouse_ball] - state->opy[state->mouse_ball]) *
491          state->tc);
492     }
493
494   /* For each ball, compute the influence of every other ball. */
495   for (a=1; a <= state->count; a++)
496     if (a != state->mouse_ball)
497       for (b=1; b <= state->count; b++)
498         if (a != b)
499           {
500             d = ((state->px[a] - state->px[b]) *
501                  (state->px[a] - state->px[b]) +
502                  (state->py[a] - state->py[b]) *
503                  (state->py[a] - state->py[b]));
504             dee2 = (state->r[a] + state->r[b]) *
505                    (state->r[a] + state->r[b]);
506             if (d < dee2)
507               {
508                 state->collision_count++;
509                 d = sqrt(d);
510                 dd = state->r[a] + state->r[b] - d;
511                 /* A pair of balls that have already collided in this
512                  * current frame (and therefore touching each other)
513                  * should not have another collision calculated, hence
514                  * the fallthru if "dd ~= 0.0".
515                  */
516                 if ((dd < -0.01) || (dd > 0.01))
517                   {
518                     cdx = (state->px[b] - state->px[a]) / d;
519                     cdy = (state->py[b] - state->py[a]) / d;
520
521                     /* Move each ball apart from the other by half the
522                      * 'collision' distance.
523                      */
524                     state->px[a] -= 0.5 * dd * cdx;
525                     state->py[a] -= 0.5 * dd * cdy;
526                     state->px[b] += 0.5 * dd * cdx;
527                     state->py[b] += 0.5 * dd * cdy;
528
529                     ma = state->m[a];
530                     mb = state->m[b];
531                     vxa = state->vx[a];
532                     vya = state->vy[a];
533                     vxb = state->vx[b];
534                     vyb = state->vy[b];
535
536                     vela = sqrt((vxa * vxa) + (vya * vya));
537                     velb = sqrt((vxb * vxb) + (vyb * vyb));
538
539                     vela1 = vela * ((ma - mb) / (ma + mb)) +
540                             velb * ((2 * mb) / (ma + mb));
541                     velb1 = vela * ((2 * ma) / (ma + mb)) +
542                             velb * ((mb - ma) / (ma + mb));
543
544                     vela1 *= state->e; /* "air resistance" */
545                     velb1 *= state->e;
546 #if 0
547                     vela1 += (frand (50) - 25) / ma; /* brownian motion */
548                     velb1 += (frand (50) - 25) / mb;
549 #endif
550                     state->vx[a] = -cdx * vela1;
551                     state->vy[a] = -cdy * vela1;
552                     state->vx[b] = cdx * velb1;
553                     state->vy[b] = cdy * velb1;
554                   }
555               }
556           }
557
558   /* Force all balls to be on screen.
559    */
560   for (a=1; a <= state->count; a++)
561     {
562       if (state->px[a] <= (state->xmin + state->r[a]))
563         {
564           state->px[a] = state->xmin + state->r[a];
565           state->vx[a] = -state->vx[a] * state->e;
566         }
567       if (state->px[a] >= (state->xmax - state->r[a]))
568         {
569           state->px[a] = state->xmax - state->r[a];
570           state->vx[a] = -state->vx[a] * state->e;
571         }
572       if (state->py[a] <= (state->ymin + state->r[a]))
573         {
574           state->py[a] = state->ymin + state->r[a];
575           state->vy[a] = -state->vy[a] * state->e;
576         }
577       if (state->py[a] >= (state->ymax - state->r[a]))
578         {
579           state->py[a] = state->ymax - state->r[a];
580           state->vy[a] = -state->vy[a] * state->e;
581         }
582     }
583
584   /* Apply gravity to all balls.
585    */
586   for (a=1; a <= state->count; a++)
587     if (a != state->mouse_ball)
588       {
589         state->vx[a] += state->accx * state->tc;
590         state->vy[a] += state->accy * state->tc;
591         state->px[a] += state->vx[a] * state->tc;
592         state->py[a] += state->vy[a] * state->tc;
593       }
594 }
595
596
597 /* Handle X events, specifically, allow a ball to be picked up with the mouse.
598  */
599 static void
600 handle_events (b_state *state)
601 {
602   XSync (state->dpy, False);
603   while (XPending (state->dpy))
604     {
605       XEvent event;
606       XNextEvent (state->dpy, &event);
607       if (event.xany.type == ButtonPress)
608         {
609           int i;
610           float fmx = event.xbutton.x_root;
611           float fmy = event.xbutton.y_root;
612           if (state->mouse_ball != 0)  /* second down-click?  drop the ball. */
613             {
614               state->mouse_ball = 0;
615               return;
616             }
617           else
618             for (i=1; i <= state->count; i++)
619               {
620                 float d = ((state->px[i] - fmx) * (state->px[i] - fmx) +
621                            (state->py[i] - fmy) * (state->py[i] - fmy));
622                 float r = state->r[i];
623                 if (d < r*r)
624                   {
625                     state->mouse_ball = i;
626                     return;
627                   }
628               }
629         }
630       else if (event.xany.type == ButtonRelease)   /* drop the ball */
631         {
632           state->mouse_ball = 0;
633           return;
634         }
635       else if (event.xany.type == ConfigureNotify)
636         {
637           /* This is redundant, since we poll this every iteration. */
638           check_window_moved (state);
639         }
640
641       screenhack_handle_event (state->dpy, &event);
642     }
643 }
644
645 \f
646 char *progclass = "FluidBalls";
647
648 char *defaults [] = {
649   ".background:         black",
650   ".textColor:          yellow",
651   ".font:               -*-helvetica-*-r-*-*-*-180-*-*-p-*-*-*",
652   "*delay:              10000",
653   "*count:              300",
654   "*size:               25",
655   "*random:             True",
656   "*gravity:            0.01",
657   "*wind:               0.00",
658   "*friction:           0.8",
659   "*timeScale:          1.0",
660   "*doFPS:              False",
661   "*shake:              True",
662   "*shakeThreshold:     0.015",
663   0
664 };
665
666 XrmOptionDescRec options [] = {
667   { "-delay",           ".delay",       XrmoptionSepArg, 0 },
668   { "-count",           ".count",       XrmoptionSepArg, 0 },
669   { "-size",            ".size",        XrmoptionSepArg, 0 },
670   { "-count",           ".count",       XrmoptionSepArg, 0 },
671   { "-gravity",         ".gravity",     XrmoptionSepArg, 0 },
672   { "-wind",            ".wind",        XrmoptionSepArg, 0 },
673   { "-friction",        ".friction",    XrmoptionSepArg, 0 },
674   { "-fps",             ".doFPS",       XrmoptionNoArg, "True" },
675   { "-no-fps",          ".doFPS",       XrmoptionNoArg, "False" },
676   { "-shake",           ".shake",       XrmoptionNoArg, "True" },
677   { "-no-shake",        ".shake",       XrmoptionNoArg, "False" },
678   { "-random",          ".random",      XrmoptionNoArg, "True" },
679   { "-nonrandom",       ".random",      XrmoptionNoArg, "False" },
680   { 0, 0, 0, 0 }
681 };
682
683 void
684 screenhack (Display *dpy, Window window)
685 {
686   b_state *state = init_balls(dpy, window);
687   int delay = get_integer_resource ("delay", "Integer");
688
689   while (1)
690     {
691       repaint_balls(state);
692       update_balls(state);
693
694       XSync (dpy, False);
695       handle_events (state);
696       if (delay) usleep (delay);
697     }
698 }