http://ftp.x.org/contrib/applications/xscreensaver-2.23.tar.gz
[xscreensaver] / hacks / attraction.c
1 /* xscreensaver, Copyright (c) 1992, 1995, 1996, 1997
2  *  Jamie Zawinski <jwz@netscape.com>
3  *
4  * Permission to use, copy, modify, distribute, and sell this software and its
5  * documentation for any purpose is hereby granted without fee, provided that
6  * the above copyright notice appear in all copies and that both that
7  * copyright notice and this permission notice appear in supporting
8  * documentation.  No representations are made about the suitability of this
9  * software for any purpose.  It is provided "as is" without express or 
10  * implied warranty.
11  */
12
13 /* Simulation of a pair of quasi-gravitational fields, maybe sorta kinda
14    a little like the strong and weak electromagnetic forces.  Derived from
15    a Lispm screensaver by John Pezaris <pz@mit.edu>.  Mouse control and
16    viscosity added by "Philip Edward Cutone, III" <pc2d+@andrew.cmu.edu>.
17
18    John sez:
19
20    The simulation started out as a purely accurate gravitational simulation,
21    but, with constant simulation step size, I quickly realized the field being
22    simulated while grossly gravitational was, in fact, non-conservative.  It
23    also had the rather annoying behavior of dealing very badly with colliding
24    orbs.  Therefore, I implemented a negative-gravity region (with two
25    thresholds; as I read your code, you only implemented one) to prevent orbs
26    from every coming too close together, and added a viscosity factor if the
27    speed of any orb got too fast.  This provides a nice stable system with
28    interesting behavior.
29
30    I had experimented with a number of fields including the van der Waals
31    force (very interesting orbiting behavior) and 1/r^3 gravity (not as
32    interesting as 1/r^2).  An even normal viscosity (rather than the
33    thresholded version to bleed excess energy) is also not interesting.
34    The 1/r^2, -1/r^2, -10/r^2 thresholds proved not only robust but also
35    interesting -- the orbs never collided and the threshold viscosity fixed
36    the non-conservational problem.
37
38    Philip sez:
39    > An even normal viscosity (rather than the thresholded version to
40    > bleed excess energy) is also not interesting.
41
42    unless you make about 200 points.... set the viscosity to about .8
43    and drag the mouse through it.   it makes a nice wave which travels
44    through the field.
45
46    And (always the troublemaker) Joe Keane <jgk@jgk.org> sez:
47
48    Despite what John sez, the field being simulated is always conservative.
49    The real problem is that it uses a simple hack, computing acceleration
50    *based only on the starting position*, instead of a real differential
51    equation solver.  Thus you'll always have energy coming out of nowhere,
52    although it's most blatant when balls get close together.  If it were
53    done right, you wouldn't need viscosity or artificial limits on how
54    close the balls can get.
55  */
56
57 #include <stdio.h>
58 #include <math.h>
59 #include "screenhack.h"
60 #include "spline.h"
61
62 struct ball {
63   double x, y;
64   double vx, vy;
65   double dx, dy;
66   double mass;
67   int size;
68   int pixel_index;
69   int hue;
70 };
71
72 static struct ball *balls;
73 static int npoints;
74 static int threshold;
75 static int delay;
76 static int global_size;
77 static int segments;
78 static Bool glow_p;
79 static Bool orbit_p;
80 static XPoint *point_stack;
81 static int point_stack_size, point_stack_fp;
82 static XColor *colors;
83 static int ncolors;
84 static int fg_index;
85 static int color_shift;
86
87 /*flip mods for mouse interaction*/
88 static Bool mouse_p;
89 int mouse_x, mouse_y, mouse_mass, root_x, root_y;
90 static double viscosity;
91
92 static enum object_mode {
93   ball_mode, line_mode, polygon_mode, spline_mode, spline_filled_mode,
94   tail_mode
95 } mode;
96
97 static GC draw_gc, erase_gc;
98
99 #define MAX_SIZE 16
100
101 #define min(a,b) ((a)<(b)?(a):(b))
102 #define max(a,b) ((a)>(b)?(a):(b))
103
104 static void
105 init_balls (Display *dpy, Window window)
106 {
107   int i;
108   XWindowAttributes xgwa;
109   XGCValues gcv;
110   int xlim, ylim, midx, midy, r, vx, vy;
111   double th;
112   Colormap cmap;
113   char *mode_str;
114   XGetWindowAttributes (dpy, window, &xgwa);
115   xlim = xgwa.width;
116   ylim = xgwa.height;
117   cmap = xgwa.colormap;
118   midx = xlim/2;
119   midy = ylim/2;
120   r = get_integer_resource ("radius", "Integer");
121   if (r <= 0 || r > min (xlim/2, ylim/2))
122     r = min (xlim/2, ylim/2) - 50;
123   vx = get_integer_resource ("vx", "Integer");
124   vy = get_integer_resource ("vy", "Integer");
125   npoints = get_integer_resource ("points", "Integer");
126   if (npoints < 1)
127     npoints = 3 + (random () % 5);
128   balls = (struct ball *) malloc (npoints * sizeof (struct ball));
129   segments = get_integer_resource ("segments", "Integer");
130   if (segments < 0) segments = 1;
131   threshold = get_integer_resource ("threshold", "Integer");
132   if (threshold < 0) threshold = 0;
133   delay = get_integer_resource ("delay", "Integer");
134     if (delay < 0) delay = 0;
135   global_size = get_integer_resource ("size", "Integer");
136   if (global_size < 0) global_size = 0;
137   glow_p = get_boolean_resource ("glow", "Boolean");
138   orbit_p = get_boolean_resource ("orbit", "Boolean");
139   color_shift = get_integer_resource ("colorShift", "Integer");
140   if (color_shift <= 0) color_shift = 5;
141
142   /*flip mods for mouse interaction*/
143   mouse_p = get_boolean_resource ("mouse", "Boolean");
144   mouse_mass = get_integer_resource ("mouseSize", "Integer");
145   mouse_mass =  mouse_mass *  mouse_mass *10;
146
147   viscosity = get_float_resource ("viscosity", "Float");
148
149   mode_str = get_string_resource ("mode", "Mode");
150   if (! mode_str) mode = ball_mode;
151   else if (!strcmp (mode_str, "balls")) mode = ball_mode;
152   else if (!strcmp (mode_str, "lines")) mode = line_mode;
153   else if (!strcmp (mode_str, "polygons")) mode = polygon_mode;
154   else if (!strcmp (mode_str, "tails")) mode = tail_mode;
155   else if (!strcmp (mode_str, "splines")) mode = spline_mode;
156   else if (!strcmp (mode_str, "filled-splines")) mode = spline_filled_mode;
157   else {
158     fprintf (stderr,
159              "%s: mode must be balls, lines, tails, polygons, splines, or\n\
160         filled-splines, not \"%s\"\n",
161              progname, mode_str);
162     exit (1);
163   }
164
165   if (mode != ball_mode && mode != tail_mode) glow_p = False;
166   
167   if (mode == polygon_mode && npoints < 3)
168     mode = line_mode;
169
170   ncolors = get_integer_resource ("colors", "Colors");
171   if (ncolors < 2) ncolors = 2;
172   if (ncolors <= 2) mono_p = True;
173   colors = 0;
174
175   if (!mono_p)
176     {
177       fg_index = 0;
178       switch (mode)
179         {
180         case ball_mode:
181           if (glow_p)
182             {
183               int H = random() % 360;
184               double S1 = 0.25;
185               double S2 = 1.00;
186               double V = frand(0.25) + 0.75;
187               colors = (XColor *) malloc(sizeof(*colors) * (ncolors+1));
188               make_color_ramp (dpy, cmap, H, S1, V, H, S2, V, colors, &ncolors,
189                                False, True, False);
190             }
191           else
192             {
193               ncolors = npoints;
194               colors = (XColor *) malloc(sizeof(*colors) * (ncolors+1));
195               make_random_colormap (dpy, xgwa.visual, cmap, colors, &ncolors,
196                                     True, True, False, True);
197             }
198           break;
199         case line_mode:
200         case polygon_mode:
201         case spline_mode:
202         case spline_filled_mode:
203         case tail_mode:
204           colors = (XColor *) malloc(sizeof(*colors) * (ncolors+1));
205           make_smooth_colormap (dpy, xgwa.visual, cmap, colors, &ncolors,
206                                 True, False, True);
207           break;
208         default:
209           abort ();
210         }
211     }
212
213   if (!mono_p && ncolors <= 2)
214     {
215       if (colors) free (colors);
216       colors = 0;
217       mono_p = True;
218     }
219
220   if (mode != ball_mode)
221     {
222       int size = (segments ? segments : 1);
223       point_stack_size = size * (npoints + 1);
224       point_stack = (XPoint *) calloc (point_stack_size, sizeof (XPoint));
225       point_stack_fp = 0;
226     }
227
228   gcv.line_width = (mode == tail_mode
229                     ? (global_size ? global_size : (MAX_SIZE * 2 / 3))
230                     : 1);
231   gcv.cap_style = (mode == tail_mode ? CapRound : CapButt);
232
233   if (mono_p)
234     gcv.foreground = get_pixel_resource("foreground", "Foreground", dpy, cmap);
235   else
236     gcv.foreground = colors[fg_index].pixel;
237   draw_gc = XCreateGC (dpy, window, GCForeground|GCLineWidth|GCCapStyle, &gcv);
238
239   gcv.foreground = get_pixel_resource("background", "Background", dpy, cmap);
240   erase_gc = XCreateGC (dpy, window, GCForeground|GCLineWidth|GCCapStyle,&gcv);
241
242
243 #define rand_size() min (MAX_SIZE, 8 + (random () % (MAX_SIZE - 9)))
244
245   if (orbit_p && !global_size)
246     /* To orbit, all objects must be the same mass, or the math gets
247        really hairy... */
248     global_size = rand_size ();
249
250   th = frand (M_PI+M_PI);
251   for (i = 0; i < npoints; i++)
252     {
253       int new_size = (global_size ? global_size : rand_size ());
254       balls [i].dx = 0;
255       balls [i].dy = 0;
256       balls [i].size = new_size;
257       balls [i].mass = (new_size * new_size * 10);
258       balls [i].x = midx + r * cos (i * ((M_PI+M_PI) / npoints) + th);
259       balls [i].y = midy + r * sin (i * ((M_PI+M_PI) / npoints) + th);
260       if (! orbit_p)
261         {
262           balls [i].vx = vx ? vx : ((6.0 - (random () % 11)) / 8.0);
263           balls [i].vy = vy ? vy : ((6.0 - (random () % 11)) / 8.0);
264         }
265       if (mono_p || mode != ball_mode)
266         balls [i].pixel_index = -1;
267       else if (glow_p)
268         balls [i].pixel_index = 0;
269       else
270         balls [i].pixel_index = random() % ncolors;
271     }
272
273   if (orbit_p)
274     {
275       double a = 0;
276       double v;
277       double v_mult = get_float_resource ("vMult", "Float");
278       if (v_mult == 0.0) v_mult = 1.0;
279
280       for (i = 1; i < npoints; i++)
281         {
282           double _2ipi_n = (2 * i * M_PI / npoints);
283           double x = r * cos (_2ipi_n);
284           double y = r * sin (_2ipi_n);
285           double distx = r - x;
286           double dist2 = (distx * distx) + (y * y);
287           double dist = sqrt (dist2);
288           double a1 = ((balls[i].mass / dist2) *
289                        ((dist < threshold) ? -1.0 : 1.0) *
290                        (distx / dist));
291           a += a1;
292         }
293       if (a < 0.0)
294         {
295           fprintf (stderr, "%s: domain error: forces on balls too great\n",
296                    progname);
297           exit (-1);
298         }
299       v = sqrt (a * r) * v_mult;
300       for (i = 0; i < npoints; i++)
301         {
302           double k = ((2 * i * M_PI / npoints) + th);
303           balls [i].vx = -v * sin (k);
304           balls [i].vy =  v * cos (k);
305         }
306     }
307
308   if (mono_p) glow_p = False;
309   XClearWindow (dpy, window);
310 }
311
312 static void
313 compute_force (int i, double *dx_ret, double *dy_ret)
314 {
315   int j;
316   double x_dist, y_dist, dist, dist2;
317   *dx_ret = 0;
318   *dy_ret = 0;
319   for (j = 0; j < npoints; j++)
320     {
321       if (i == j) continue;
322       x_dist = balls [j].x - balls [i].x;
323       y_dist = balls [j].y - balls [i].y;
324       dist2 = (x_dist * x_dist) + (y_dist * y_dist);
325       dist = sqrt (dist2);
326               
327       if (dist > 0.1) /* the balls are not overlapping */
328         {
329           double new_acc = ((balls[j].mass / dist2) *
330                             ((dist < threshold) ? -1.0 : 1.0));
331           double new_acc_dist = new_acc / dist;
332           *dx_ret += new_acc_dist * x_dist;
333           *dy_ret += new_acc_dist * y_dist;
334         }
335       else
336         {               /* the balls are overlapping; move randomly */
337           *dx_ret += (frand (10.0) - 5.0);
338           *dy_ret += (frand (10.0) - 5.0);
339         }
340     }
341
342   if (mouse_p)
343     {
344       x_dist = mouse_x - balls [i].x;
345       y_dist = mouse_y - balls [i].y;
346       dist2 = (x_dist * x_dist) + (y_dist * y_dist);
347       dist = sqrt (dist2);
348         
349       if (dist > 0.1) /* the balls are not overlapping */
350         {
351           double new_acc = ((mouse_mass / dist2) *
352                             ((dist < threshold) ? -1.0 : 1.0));
353           double new_acc_dist = new_acc / dist;
354           *dx_ret += new_acc_dist * x_dist;
355           *dy_ret += new_acc_dist * y_dist;
356         }
357       else
358         {               /* the balls are overlapping; move randomly */
359           *dx_ret += (frand (10.0) - 5.0);
360           *dy_ret += (frand (10.0) - 5.0);
361         }
362     }
363 }
364
365 static void
366 run_balls (Display *dpy, Window window)
367 {
368   int last_point_stack_fp = point_stack_fp;
369   static int tick = 500, xlim, ylim;
370   static Colormap cmap;
371   int i;
372
373   /*flip mods for mouse interaction*/
374   Window  root1, child1;
375   unsigned int mask;
376   if (mouse_p)
377     {
378       XQueryPointer(dpy, window, &root1, &child1,
379                     &root_x, &root_y, &mouse_x, &mouse_y, &mask);
380     }
381
382   if (tick++ == 500)
383     {
384       XWindowAttributes xgwa;
385       XGetWindowAttributes (dpy, window, &xgwa);
386       tick = 0;
387       xlim = xgwa.width;
388       ylim = xgwa.height;
389       cmap = xgwa.colormap;
390     }
391
392   /* compute the force of attraction/repulsion among all balls */
393   for (i = 0; i < npoints; i++)
394     compute_force (i, &balls[i].dx, &balls[i].dy);
395
396   /* move the balls according to the forces now in effect */
397   for (i = 0; i < npoints; i++)
398     {
399       double old_x = balls[i].x;
400       double old_y = balls[i].y;
401       double new_x, new_y;
402       int size = balls[i].size;
403       balls[i].vx += balls[i].dx;
404       balls[i].vy += balls[i].dy;
405
406       /* don't let them get too fast: impose a terminal velocity
407          (actually, make the medium have friction) */
408       if (balls[i].vx > 10)
409         {
410           balls[i].vx *= 0.9;
411           balls[i].dx = 0;
412         }
413       else if (viscosity != 1)
414         {
415           balls[i].vx *= viscosity;
416         }
417
418       if (balls[i].vy > 10)
419         {
420           balls[i].vy *= 0.9;
421           balls[i].dy = 0;
422         }
423       else if (viscosity != 1)
424         {
425           balls[i].vy *= viscosity;
426         }
427
428       balls[i].x += balls[i].vx;
429       balls[i].y += balls[i].vy;
430
431       /* bounce off the walls */
432       if (balls[i].x >= (xlim - balls[i].size))
433         {
434           balls[i].x = (xlim - balls[i].size - 1);
435           if (balls[i].vx > 0)
436             balls[i].vx = -balls[i].vx;
437         }
438       if (balls[i].y >= (ylim - balls[i].size))
439         {
440           balls[i].y = (ylim - balls[i].size - 1);
441           if (balls[i].vy > 0)
442             balls[i].vy = -balls[i].vy;
443         }
444       if (balls[i].x <= 0)
445         {
446           balls[i].x = 0;
447           if (balls[i].vx < 0)
448             balls[i].vx = -balls[i].vx;
449         }
450       if (balls[i].y <= 0)
451         {
452           balls[i].y = 0;
453           if (balls[i].vy < 0)
454             balls[i].vy = -balls[i].vy;
455         }
456
457       new_x = balls[i].x;
458       new_y = balls[i].y;
459
460       if (!mono_p)
461         {
462           if (mode == ball_mode)
463             {
464               if (glow_p)
465                 {
466                   /* make color saturation be related to particle
467                      acceleration. */
468                   double limit = 0.5;
469                   double s, fraction;
470                   double vx = balls [i].dx;
471                   double vy = balls [i].dy;
472                   if (vx < 0) vx = -vx;
473                   if (vy < 0) vy = -vy;
474                   fraction = vx + vy;
475                   if (fraction > limit) fraction = limit;
476
477                   s = 1 - (fraction / limit);
478                   balls[i].pixel_index = (ncolors * s);
479                 }
480               XSetForeground (dpy, draw_gc,
481                               colors[balls[i].pixel_index].pixel);
482             }
483         }
484
485       if (mode == ball_mode)
486         {
487           XFillArc (dpy, window, erase_gc, (int) old_x, (int) old_y,
488                     size, size, 0, 360*64);
489           XFillArc (dpy, window, draw_gc,  (int) new_x, (int) new_y,
490                     size, size, 0, 360*64);
491         }
492       else
493         {
494           point_stack [point_stack_fp].x = new_x;
495           point_stack [point_stack_fp].y = new_y;
496           point_stack_fp++;
497         }
498     }
499
500   /* draw the lines or polygons after computing all points */
501   if (mode != ball_mode)
502     {
503       point_stack [point_stack_fp].x = balls [0].x; /* close the polygon */
504       point_stack [point_stack_fp].y = balls [0].y;
505       point_stack_fp++;
506       if (point_stack_fp == point_stack_size)
507         point_stack_fp = 0;
508       else if (point_stack_fp > point_stack_size) /* better be aligned */
509         abort ();
510       if (!mono_p)
511         {
512           static int tick = 0;
513           if (tick++ == color_shift)
514             {
515               tick = 0;
516               fg_index = (fg_index + 1) % ncolors;
517               XSetForeground (dpy, draw_gc, colors[fg_index].pixel);
518             }
519         }
520     }
521
522   switch (mode)
523     {
524     case ball_mode:
525       break;
526     case line_mode:
527       if (segments > 0)
528         XDrawLines (dpy, window, erase_gc, point_stack + point_stack_fp,
529                     npoints + 1, CoordModeOrigin);
530       XDrawLines (dpy, window, draw_gc, point_stack + last_point_stack_fp,
531                   npoints + 1, CoordModeOrigin);
532       break;
533     case polygon_mode:
534       if (segments > 0)
535         XFillPolygon (dpy, window, erase_gc, point_stack + point_stack_fp,
536                       npoints + 1, (npoints == 3 ? Convex : Complex),
537                       CoordModeOrigin);
538       XFillPolygon (dpy, window, draw_gc, point_stack + last_point_stack_fp,
539                     npoints + 1, (npoints == 3 ? Convex : Complex),
540                     CoordModeOrigin);
541       break;
542     case tail_mode:
543       {
544         for (i = 0; i < npoints; i++)
545           {
546             int index = point_stack_fp + i;
547             int next_index = (index + (npoints + 1)) % point_stack_size;
548             XDrawLine (dpy, window, erase_gc,
549                        point_stack [index].x,
550                        point_stack [index].y,
551                        point_stack [next_index].x,
552                        point_stack [next_index].y);
553
554             index = last_point_stack_fp + i;
555             next_index = (index - (npoints + 1)) % point_stack_size;
556             if (next_index < 0) next_index += point_stack_size;
557             if (point_stack [next_index].x == 0 &&
558                 point_stack [next_index].y == 0)
559               continue;
560             XDrawLine (dpy, window, draw_gc,
561                        point_stack [index].x,
562                        point_stack [index].y,
563                        point_stack [next_index].x,
564                        point_stack [next_index].y);
565           }
566       }
567       break;
568     case spline_mode:
569     case spline_filled_mode:
570       {
571         static spline *s = 0;
572         if (! s) s = make_spline (npoints);
573         if (segments > 0)
574           {
575             for (i = 0; i < npoints; i++)
576               {
577                 s->control_x [i] = point_stack [point_stack_fp + i].x;
578                 s->control_y [i] = point_stack [point_stack_fp + i].y;
579               }
580             compute_closed_spline (s);
581             if (mode == spline_filled_mode)
582               XFillPolygon (dpy, window, erase_gc, s->points, s->n_points,
583                             (s->n_points == 3 ? Convex : Complex),
584                             CoordModeOrigin);
585             else
586               XDrawLines (dpy, window, erase_gc, s->points, s->n_points,
587                           CoordModeOrigin);
588           }
589         for (i = 0; i < npoints; i++)
590           {
591             s->control_x [i] = point_stack [last_point_stack_fp + i].x;
592             s->control_y [i] = point_stack [last_point_stack_fp + i].y;
593           }
594         compute_closed_spline (s);
595         if (mode == spline_filled_mode)
596           XFillPolygon (dpy, window, draw_gc, s->points, s->n_points,
597                         (s->n_points == 3 ? Convex : Complex),
598                         CoordModeOrigin);
599         else
600           XDrawLines (dpy, window, draw_gc, s->points, s->n_points,
601                       CoordModeOrigin);
602       }
603       break;
604     default:
605       abort ();
606     }
607
608   XSync (dpy, True);
609 }
610
611 \f
612 char *progclass = "Attraction";
613
614 char *defaults [] = {
615   ".background: black",
616   ".foreground: white",
617   "*mode:       balls",
618   "*points:     0",
619   "*size:       0",
620   "*colors:     200",
621   "*threshold:  100",
622   "*delay:      10000",
623   "*glow:       false",
624   "*mouseSize:  10",
625   "*mouse:      false",
626   "*viscosity:  1",
627   "*orbit:      false",
628   "*colorShift: 3",
629   "*segments:   500",
630   "*vMult:      0.9",
631   0
632 };
633
634 XrmOptionDescRec options [] = {
635   { "-mode",            ".mode",        XrmoptionSepArg, 0 },
636   { "-colors",          ".colors",      XrmoptionSepArg, 0 },
637   { "-points",          ".points",      XrmoptionSepArg, 0 },
638   { "-color-shift",     ".colorShift",  XrmoptionSepArg, 0 },
639   { "-threshold",       ".threshold",   XrmoptionSepArg, 0 },
640   { "-segments",        ".segments",    XrmoptionSepArg, 0 },
641   { "-delay",           ".delay",       XrmoptionSepArg, 0 },
642   { "-size",            ".size",        XrmoptionSepArg, 0 },
643   { "-radius",          ".radius",      XrmoptionSepArg, 0 },
644   { "-vx",              ".vx",          XrmoptionSepArg, 0 },
645   { "-vy",              ".vy",          XrmoptionSepArg, 0 },
646   { "-vmult",           ".vMult",       XrmoptionSepArg, 0 },
647   { "-mouse-size",      ".mouseSize",   XrmoptionSepArg, 0 },
648   { "-mouse",           ".mouse",       XrmoptionNoArg, "true" },
649   { "-nomouse",         ".mouse",       XrmoptionNoArg, "false" },
650   { "-viscosity",       ".viscosity",   XrmoptionSepArg, 0 },
651   { "-glow",            ".glow",        XrmoptionNoArg, "true" },
652   { "-noglow",          ".glow",        XrmoptionNoArg, "false" },
653   { "-orbit",           ".orbit",       XrmoptionNoArg, "true" },
654   { 0, 0, 0, 0 }
655 };
656
657 void
658 screenhack (Display *dpy, Window window)
659 {
660   init_balls (dpy, window);
661   while (1)
662     {
663       run_balls (dpy, window);
664       if (delay) usleep (delay);
665     }
666 }