OO Windchime

In this post, we will generalize the square “windchime” from scotw-005 to a regular polygon with an arbitrary number of sides. In so doing, we will learn how to write classes in SuperCollider as well as about an interesting algorithm for deciding whether a given point lies inside a regular n-gon. If you’d like to follow along, the code for the whole project is on GitHub.

The SC documentation on writing classes is quite good, so what follows is just a quick review of the syntax. Note that class definitions cannot be executed in the IDE. Instead, class definitions are given the suffix .sc and placed in the Extensions directory (on OS X, this is Library/Application Support/SuperCollider/Extensions). After this, you must recompile the class library (command-shift-L).

Here is how a class is defined:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
ClassSubNaught {
  var a;
  var <b;
  var >c;
  var <>d;

  *new {|a,b,c,d|
    ^super.newCopyArgs(a,b,c,d)
  }

  addFoo {|foo|
    ^(a + foo)
  }
}

This code defines a class called ClassSubNaught (by convention, classes are capitalized) with variables a, b, c, and d. The < and > tokens allow for the automatic generation of getter and setter methods. Thus, b has a getter method, c has a setter method, and d has both getter and setter methods (a is private). Class methods are denoted by the * symbol. The super keyword refers to the parent object, which in this case is Object. The method newCopyArgs creates a new instance and copys the arguments to the class variables in the order they were defined. Finally, the carat (^) indicates the return value. If no return value is indicated, then the class or instance is returned. The addFoo method takes an argument foo and returns the sum of a and foo.

A better wind chime

With that out of the way, let’s generalize the windchime from scotw-005 from a rectangle to an n-sided polygon, with arbitrary n. Because the edges of our box are no longer parallel to the coordinate axes, our intuition is no longer able to “guess” a solution, and we have to proceed a bit more carefully.

It seems natural that a WindChime object might be decomposed into Ball and Polygon objects. Here’s a ball:

1
2
3
4
5
6
7
8
Ball2 {
  var <>position;
  var <>velocity;

  *new { |pos, vel|
    ^super.newCopyArgs(pos, vel);
  }
}

Because the SuperCollider class library already contains a Ball class, we call this one Ball2. A ball is composed of a position and a velocity, both of which will be represented by the Point class, which is simply an x,y pair plus some convenience methods. Both the position and velocity attributes have getters and setters as indicated by the <> tokens in the declarations.

The polygon class is a bit more complicated:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Polygon {
  var <sides, <radius, <points, <perps;

  *new {|n, width, height|
    var radius = min(width/2, height/2);
    var points = Array.fill(n, {|i|
      radius * Point(cos((2pi*i)/n), sin((2pi*i)/n))
    });
    var perps = Array.fill(n, {|i|
      var firstPoint = points[i];
      var secondPoint = points.wrapAt(i+1);
      var tempPoint = firstPoint + secondPoint;
      tempPoint / tempPoint.rho;
    });
    ^super.newCopyArgs(n, radius, points, perps);
  }
}

We capture sides, the number of sides, radius, the radius of the polygon, points, an array containing the vertices of the polygon, and perps, an array of unit perpendiculars to each side. (The reason for this last one will become clear in a little bit.) The arguments to the constructor are the number of sides and the width and height of the containing window. From these, we can do some math to generate the variables we need in order to initialize the instance.

Finally, we create a WindChime class which contains a Ball2 and a polygon plus a few other variables which we will need later. Note the use of an custom init method to initialize these variables

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
WindChime {
  var <>ball;
  var <>polygon;
  var <klang;
  var sector;
  var loudness;
  var brightness;

  *new {|b, p|
    ^super.newCopyArgs(b, p).init;
  }

  init {
    klang = false;
    loudness = 0;
    brightness = 0;
    sector = this.sector;
  }
}

Collisions

As before, we need to do two things:

  1. Figure out if the ball hit a wall
  2. If so, adjust the velocity appropriately

We will do these things within the methods of the WindChime class. With a rectangular box, the answers to these questions are easily guessed. With an arbitrary regular polygon, some thinking is required.

Let’s start with the situation in which the ball is comfortably inside the polygon. In this case, the new position is simply the old position incremented by the velocity vector. When we are close to an edge, however, this calculated position will sometimes be outside the polygon. In these cases, we can say that a collision has occured. Thus, the key task is to determine if the ball is inside or outside the polygon. We do this with the help of a rather clever algorithm for determining if a point is inside or outside a given triangle1.

As it happens, we only need to use one-third of this algorithm. First note that a regular n-gon can be decomposed into n congruent triangles:

polygon

We can easily determine which triangle we are in by looking at the angular component of the position vector. Once we’ve identified the appropriate triangle, we use the method above to figure out which side of the line AB we are on.

If we detect a collision, then the next task is to adjust the velocity appropriately. The easiest way to see what needs to be done is to decompose the velocity vector into parallel and perpendicular components with respect to the polygon edge. We want to leave the parallel component unchanged and reverse the sign of the perpendicular component. We can find the perpendicular component by projecting the velocity along the unit perpendicular that we calculated along with the polygon vertices. We then subtract twice this amount from the previous velocity.

Sound!

The collision detection all takes place in the updateAll method of the WindChime class. This method takes an argument called playFunc which is a function to be called when a collision is detected. In this case, our playFunc generates a synth node, resulting in a sound being played. However, one can imagine other scenarios.

Garbage Collection

In the video above, you can see a seven-sided wind chime in action. Not discussed in this post is a simple Routine that provides a periodic wind gust. I think you’ll agree that it’s significantly more interesting than the original square wind chime. While our code also became significantly more complicated, we were able to hide much of this complexity by separating out much of it into classes. If we were to do this for every project, however, the class library would quickly become bloated. In a future post, we’ll look at another approach: storing functions in environment variables.


  1. Note that this technique assumes that you are working in 3D space, which is required for the cross product to be defined. However, we can use the same technique in 2D space by simply assuming that the z-coordinate is 0. This is explained in more detail here