2008-10-13

Procedural Drawing

Pretty pictures, again. There are now a number of "toys" with which to "recreate" the experience of turning on the old computer (C-64, Spectrum, Apple II or what had you) and being greeted with a BASIC prompt. I've written before about how SDL can give you a simple, direct (!) buffer to which to write pixels; the problem it has is that it does not know anything about drawing any object more complicated than a pixel. I'd like to explore some alternatives.

I was inspired by this image and wanted to replicate it. I happen to have Mathematica at hand, and sometimes it's my fall-back choice. After some fudging, I came up with:

wheel[m_Integer?Positive, n_Integer?Positive] :=
 With[{a = Pi/n},
  With[{ka = Cos[a], sa = Sin[a]},
   With[{ta = (ka + sa)/(ka - sa)},
    With[{d = ka + sa*ta},
     Graphics[
      Table[
       With[{r = 1/(ka - sa)^e},
        Table[
          With[{k = Cos[a (2 i + e)], s = Sin[a (2 i + e)]},
          {Hue[i/n, 0.5*(1 + e/m), 0.5*(2 - e/m)],
           Polygon[
            r {{k, s},
             d {k*ka - s*sa, s*ka + k*sa},
             ta {k, s}, 
             d {k*ka + s*sa, s*ka - k*sa},
             {k, s}}]}],
        {i, 0,  n - 1}]],
      {e, 0, m - 1}]]]]]]

I won't delve into the semantics of this function, except to say that it is pure (it returns a Graphics object), and that I tend to use immutable bindings introduced by With as much as possible, but this not always affords me readable code. An invocation of wheel[12, 16] generates the following:

color daisy drawn with Mathematica

Pretty! I was satisfied with this for maybe twelve hours. I thought that it could be nice if it could be animated, or something. Doing this in OCaml was out of the question; I have no intention of installing Cairo and a host of dependencies and bindings just to be able to. Using Processing would have been an alternative; I find it, however, crude and unresponsive. Flash would have been ideal, as I'm impressed with Krazydad's wizardry. This would have the unfortunate prerequisite of me having to learn to program Flash in the first place, which is something I have not in my plans for the foreseeable future. I could have used Java, I could…

I remembered I had downloaded Top Draw. I don't think it has animation capabilities; it sits, I think, squarely in the category of "toys". It has, however, a strong graphics model, with filters and compositing inherited from Quartz. Its documentation is succint but informative, and it's Javascript, so I would feel at home. It turns out that Mathematica's retained model means that I could forget about scaling and layout; with Top Draw, those considerations must be taken into account:

var bounds = desktop.bounds;
var cx = bounds.x + bounds.width  / 2;
var cy = bounds.y + bounds.height / 2;

I had a choice between showing most of the graphic or filling most of the screen; it's a matter of a min over a max:

var sz = Math.max(bounds.width, bounds.height) / 2;

Again, the drawing should be generated with a simple call:

desktop.fillLayer(new Color(0));
wheel(12, 16);

The function wheel is where the fun begins (pun not intended). It takes two parameters, the number m of layers and the number n of rhombs to place around the circle. It starts by validating the inputs:

function wheel(m, n) {
  if (m < 1 || n < 5) return;

Each rhomb has a color determined by its position on the graphic. Each would be placed two n-ths of the way around the circle, or α = π / n. Some trigonometry shows that the diagonal of the rhomb is t = (1 + tan α)/(1 - tan α), and d is the distance to the center of the rhomb. As you can see, the rhombs grow exponentially with a factor of 1/(cos α - sin α); in order to make the entire drawing fit the prescribed size I must calculate the starting radius r accordingly:

  var color = new Color();
  var a = Math.PI / n;
  var k = Math.cos(a), s = Math.sin(a);
  var t = (k + s) / (k - s);
  var d = k + s * t;
  var r = sz * Math.pow(k - s, m);

Each tier of rhombs e has a particular saturation and brightness computed as a fraction of the total:

  for (var e = 0; e != m; e++) {
    color.saturation = 0.5*(1 + e/m); // 0.5 -- 1.0
    color.brightness = 0.5*(2 - e/m); // 1.0 -- 0.5

The rhombs spiral out of the center, since each tier is started at an angle of α from the previous one. This makes the new rhombs interlock between a pair of preexisting rhombs at the current point x, y:

    for (var i = 0; i != n; i++) {
      var b = (2*i + e) * a;
      var x = r * Math.cos(b), y = r * Math.sin(b);

(As you might have noticed, all angles are relative to the semicircle; that is, they are actually half-angles). The hue is relative to the position around the circle:

      color.hue = i / n;
      desktop.fillStyle = color;
      desktop.beginPath();

Two of the rhomb's four corners are aligned with the current point; the two others are at an angle of α at either side. Instead of computing six sines and cosines, I use the formulas for the sum and difference of angles:

      desktop.moveTo(cx + x, cy + y);
      desktop.lineTo(cx + d * (k * x - s * y), cy + d * (k * y + s * x));
      desktop.lineTo(cx + t * x, cy + t * y);
      desktop.lineTo(cx + d * (k * x + s * y), cy + d * (k * y - s * x));
      desktop.lineTo(cx + x, cy + y);
      desktop.fill();
    }

As I've indicated above, the drawing is done relative to the center of the desktop. And that's it. It only remains to scale the radius for the next tier:

    r /= k - s;
  }
}

Now it is clear that is important that n > 4 so that 0 < cos α - sin α < 1. The result of this program is essentially identical to the previous one:

color daisy drawn with Top Draw

With the added bonus that now I can start playing with filters and tweaks in real-time until I'm satisfied, or tired, or both. Let the games begin!

2 comments:

Anonymous said...

I guess you could use Caml's Graphics module? It works out-of-the-box on Linux and Windows.

Btw: nice and inspiring picture!

Janne said...

I'm collecting simple graphics stubs here:

http://code.google.com/p/aihiot/

Source code to render a triangle using Graphics:

http://code.google.com/p/aihiot/source/browse/#svn/trunk/gfx/draw_triangle/ocaml