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