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