Tutorial 20: The Process Compendium (Part 4)

The Challenge of Process 8

To implement Process 8, it would seem that we just need to change the drawing code for our sketch from the implementation in the previous chapter:

Process 8
A rectangular surface densely filled with instances of Element 2, each with a different size, speed, and direction. Display the intersections by drawing a circle at each point of contact. Set the size of each circle relative to the distance between the centers of the overlapping Elements. Draw the smallest possible circle as black and largest as white, with varying grays representing sizes in between.

Unfortunately, this isn't quite as easy as it sounds. The difficult part to implement in the description of Process 8 is determining "each point of contact" between two touching circles. Fortunately, we don't have to do the hard work of figuring out the mathematics, a quick search of the Internet reveals plenty of web sites and forums where people have talked about how to do this. But we do have to be careful when implementing this type of thing.

The Intersection of Two Circles

One of the most useful sites on the Internet for this type of thing is by Paul Bourke, at the University of Western Australia, who has amassed a large collection of articles on geometry that he has described in ways that make it relatively simple to implement them. The page on the intersection of two circles provides the information that we need to implement the necessary calculation of the contact points. Here's the diagram that Paul uses to describe the calculation:

Intersection of Two Circles by Paul Bourke

This type of diagram helps to show what the problem is and provides a useful reference. In this diagram we have two circles, one centred at P0 (x0, y0) with radius r0 and the other centred at P1 (x1, y1) with radius r1. The goal is to calculate the points of contact between the circles denoted by P3 (x3, y3), which are both h distance apart from the point P2 (x2, y2), which is the mid-point of the intersection between the two circles, along the line that is perpendicular to the centre line. The distance from P0 to P2 is a and the distance from P1 to P2 is b. The total distance between P0 and P1 is d, i.e., d = a + b.

The diagram illustrates the general case when two circles are touching but in practice, most of the circles that we will draw won't be touching and we won't have to do most of the calculations. So, the first thing we want to do is check whether the two circles are touching. We could use our touching() method but because we're going to use d in more than one place in the following calculation we'll just use the distance() method that we implemented before. Here are the things that Paul Bourke says that we determine based on the value of d:

  • If d > r0 + r1 then there are no solutions, the circles are separate
  • If d < |r0 - r1| then there are no solutions because one circle is contained within the other
  • If d = 0 and r0 = r1 then the circles are coincident and there are an infinite number of solutions

Using Processing's standard dist() function, we can convert these tests into code as follows:

    // Assume that we already have the variables x0, y0, r0, x1, y1, r1
    float d = dist(x0, y0, x1, y1);
    if (d > r0 + r1) { println("No solutions, circles are separate"); }
    if (d < abs(r0 - r1)) { println("No solutions, one circle inside the other"); }
    if (d == 0 && r0 == r1) { println("Infinite solutions, circles are coincident"); }

    More practically, in our implementation, we'll want to return early from a function that calculates the intersection of two circles. We'll start by writing this function in a general way first, and then convert it to work with our Circle class.

      float[] intersectCircles(float x0, float y0, float r0, float x1, float y1, float r1) {
        float d = dist(x0, y0, x1, y1);
        if (d > r0 + r1) { return null; }
        if (d < abs(r0 - r1)) { return null; }
        if (d == 0 && r0 == r1) { return null; }
        // etc...
      }

      This code doesn't use Circle class because we want to focus on the tests at the moment, but we'll be able to change that very easily once we've figured out the rest of the test. You can read through the description of the calculation on Paul Bourke's web page, I'll spare you the maths here though. The important thing is that by manipulating the formula, we can arrive at a point where we can calculate the value of a using variables that we can pass to our formula, namely:

      a = (r02 - r12 + d2) / 2d

      Which is easy to convert into the next line of our code:

        float[] intersectCircles(float x0, float y0, float r0, float x1, float y1, float r1) {
          float d = dist(x0, y0, x1, y1);
          if (d > r0 + r1) { return null; }
          if (d < abs(r0 - r1)) { return null; }
          if (d == 0 && r0 == r1) { return null; }
         
          float a = (r0*r0 - r1*r1 + d*d) / 2*d;
         
          // etc...
        }

        The next thing to calculate is h and because:

        h2 = r02 - a2

        we can do this with the following code:

          float h = sqrt(r0*r0 - a*a);

          Continuing with Paul Bourke's description of the calculation we come to the next line that we can implement, calculating the position of P2:

          P2 = P0 + a(P1 - P0) / d

          Which we can break down into calculating x2 and y2:

          x2 = x0 + a(x1 - x0) / d
          y2 = y0 + a(y1 - y0) / d

          So this is two more lines that we can implement:

            float[] intersectCircles(float x0, float y0, float r0, float x1, float y1, float r1) {
              float d = dist(x0, y0, x1, y1);
              if (d > r0 + r1) { return null; }
              if (d < abs(r0 - r1)) { return null; }
              if (d == 0 && r0 == r1) { return null; }
             
              float a = (r0*r0 - r1*r1 + d*d) / 2*d;
              float h = sqrt(r0*r0 - a*a);
              float x2 = x0 + a * (x1 - x0) / d;
              float y2 = y0 + a * (y1 - y0) / d;
             
              // etc...
            }

            Finally, we can calculate the value for the intersection points, based on the formula given. Paul Bourke calls both of these points P3 (x3, y3) and provides a pair of formulas for x3 and y3 that can be interpreted in two different ways. For clarity, we'll call these two point P3 (x3, y3) and P4 (x4, y4):

            x3 = x2 + h(y1 - y0) / d
            y3 = y2 - h(x1 - x0) / d
            x4 = x2 - h(y1 - y0) / d
            y4 = y2 + h(x1 - x0) / d

            Implementing these four lines completes the calculation:

              float[] intersectCircles(float x0, float y0, float r0, float x1, float y1, float r1) {
                float d = dist(x0, y0, x1, y1);
                if (d > r0 + r1) { return null; }
                if (d < abs(r0 - r1)) { return null; }
                if (d == 0 && r0 == r1) { return null; }
               
                float a = (r0*r0 - r1*r1 + d*d) / 2*d;
                float x2 = x0 + a * (x1 - x0) / d;
                float y2 = y0 + a * (y1 - y0) / d;
               
                float x3 = x2 + h * (y1 - y0) / d;
                float y3 = y2 - h * (x1 - x0) / d;
                float x4 = x2 - h * (y1 - y0) / d;
                float y4 = y2 + h * (x1 - x0) / d;
               
                float[] intersectionPoints = {x3, y3, x4, y4};
                return intersectionPoints;
              }

              The last two lines of this function illustrate something we haven't done before, i.e., return an array from a function. In this case, we're packaging up the intersection points into an array of floats and returning them. We need to do this because a function can only return one thing and we need a way to put the four numbers we've calculated together to do so. This isn't very elegant, because it requires that the code calling the function knows how to unpack the array to get at the numbers again, but it's simple and works well in cases like this.

              We can test that this works with a simple sketch that uses this function:

              • Show Code
              /** @peep sketch */
              // Implementation based on information available at http://paulbourke.net/geometry/2circle/
               
              float[] intersectCircles(float x0, float y0, float r0, float x1, float y1, float r1) {
                float d = dist(x0, y0, x1, y1);
                if (d > r0 + r1) return null; // no solutions
                if (d < abs(r0 - r1)) return null; // no solutions
                if ((d == 0) && (r0 == r1)) return null; // infinite number of solutions
               
                float a = (r0*r0 - r1*r1 + d*d) / (2*d);
                float h = sqrt(r0*r0 - a*a);
                float x2 = x0 + a * (x1 - x0) / d;
                float y2 = y0 + a * (y1 - y0) / d;
                float x3 = x2 + h * (y1 - y0) / d;
                float y3 = y2 - h * (x1 - x0) / d;
                float x4 = x2 - h * (y1 - y0) / d;
                float y4 = y2 + h * (x1 - x0) / d;
               
                float[] intersectionPoints = {x3, y3, x4, y4};
                return intersectionPoints;
              }
               
              void setup() {
                size(400, 400);
                strokeWeight(0.5);
                rectMode(CENTER);
                ellipseMode(RADIUS);
                noLoop();
              }
               
              void mouseReleased() { redraw(); }
               
              void draw() {
                background(255);
               
                noFill();
                stroke(0);
               
                float x0, y0, r0, x1, y1, r1;
               
                do {
                  x0 = random(0.2*width, 0.8*width);
                  y0 = random(0.2*height, 0.8*height);
                  r0 = random(0.1*width, 0.3*width);
               
                  x1 = random(0.2*width, 0.8*width);
                  y1 = random(0.2*height, 0.8*height);
                  r1 = random(0.1*width, 0.3*width);
                } while (dist(x0, y0, x1, y1) > (r0 + r1));
               
                ellipse(x0, y0, r0, r0);
                ellipse(x1, y1, r1, r1);
               
                float[] intersectionPoints = intersectCircles(x0, y0, r0, x1, y1, r1);
                if (intersectionPoints != null) {
                  float x3 = intersectionPoints[0];
                  float y3 = intersectionPoints[1];
                  float x4 = intersectionPoints[2];
                  float y4 = intersectionPoints[3];
               
                  float x2 = (x3 + x4) / 2;
                  float y2 = (y3 + y4) / 2;
                  stroke(0, 0, 192);
                  line(x0, y0, x2, y2);
                  line(x1, y1, x2, y2);
                  ellipse(x2, y2, 2, 2);
               
                  stroke(0, 0, 192);
                  line(x0, y0, x3, y3);
                  line(x0, y0, x4, y4);
                  line(x1, y1, x3, y3);
                  line(x1, y1, x4, y4);
                  line(x3, y3, x4, y4);
               
                  stroke(0, 0, 192);
                  fill(255, 0, 0, 64);
                  ellipse(x3, y3, 4, 4);
                  ellipse(x4, y4, 4, 4);
                }
              }

              Click to generate another pair of randomly chosen circles.

              Implementing Process 8

              With the mathematics out of the way, we can now get back to implementing Process 8, here is the description again:

              Process 8
              A rectangular surface densely filled with instances of Element 2, each with a different size, speed, and direction. Display the intersections by drawing a circle at each point of contact. Set the size of each circle relative to the distance between the centers of the overlapping Elements. Draw the smallest possible circle as black and largest as white, with varying grays representing sizes in between.
              • Show Sketch
              /** @peep sketchcode **/
              int NUM_CIRCLES = 10;
              float MIN_RADIUS = 30;
              float MAX_RADIUS = 40;
               
              float DELTA_ANGLE = TWO_PI/36;
               
              Circle[] circles;
               
              void setup() {
                size(300, 300);
                frameRate(10);
                smooth();
                circles = new Circle[NUM_CIRCLES];
                for (int i = 0; i < circles.length; i++) {
                  circles[i] = new Circle(random(width), random(height), random(MIN_RADIUS, MAX_RADIUS));
                }
              }
               
              void update() {
                for (int i = 0; i < circles.length; i++) {
                  circles[i].update();
                }
              }
               
              void draw() {
                update();
                background(255);
                for (int i = 0; i < circles.length; i++) {
                  circles[i].draw();
                  for (int j = i+1; j < circles.length; j++) {
                    float intersectionPoints = intersectCircles(circles[i].x, circles[i].y, circles[i].radius, circles[j].x, circles[j].y, circles[j].radius);
                    if (intersectionPoints != null) {
                      float x3 = intersectionPoints[0];
                      float y3 = intersectionPoints[1];
                      float x4 = intersectionPoints[2];
                      float y4 = intersectionPoints[3];
                      float d = circles[i].distance(circles[j]);
                      noFill();
                      stroke(192, 0, 0, 64);
                      ellipse(x3, y3, d/2, d/2);
                      ellipse(x4, y4, d/2, d/2);
                    }
                  }
                }
              }
               
              float[] intersectCircles(float x0, float y0, float r0, float x1, float y1, float r1) {
                float d = dist(x0, y0, x1, y1);
                if (d > r0 + r1) return null; // no solutions
                if (d < abs(r0 - r1)) return null; // no solutions
                if ((d == 0) && (r0 == r1)) return null; // infinite number of solutions
               
                float a = (r0*r0 - r1*r1 + d*d) / (2*d);
                float h = sqrt(r0*r0 - a*a);
                float x2 = x0 + a * (x1 - x0) / d;
                float y2 = y0 + a * (y1 - y0) / d;
                float x3 = x2 + h * (y1 - y0) / d;
                float y3 = y2 - h * (x1 - x0) / d;
                float x4 = x2 - h * (y1 - y0) / d;
                float y4 = y2 + h * (x1 - x0) / d;
               
                float[] intersectionPoints = {x3, y3, x4, y4};
                return intersectionPoints;
              }
               
              class Circle {
                float x;
                float y;
                float radius;
               
                float heading;
                float speed;
               
                Circle parent;
               
                Circle(Circle _parent, float _radius) {
                  this(_parent.x, _parent.y, _radius);
                  parent = _parent;
                }
               
                Circle(float _x, float _y, float _radius) {
                  x = _x;
                  y = _y;
                  radius = _radius;
                  heading = random(TWO_PI);
                  speed = 1;
                  parent = null;
                }
               
                void respawn() {
                  if (parent != null) {
                    x = parent.x;
                    y = parent.y;
                  }
                }
               
                void draw() {
                  pushStyle();
                  noFill();
                  stroke(0);
                  strokeWeight(1);
                  ellipseMode(RADIUS);
               
                  for (int dx = x-width; dx <= x+width; dx += width) {
                    for (int dy = y-height; dy <= y+height; dy += height) {
                      pushMatrix();
                      translate(dx, dy);
                      rotate(heading);
                      ellipse(0, 0, radius, radius);
                      line(0, 0, radius, 0);
                      popMatrix();
                    }
                  }
               
                  stroke(192, 0, 0, 64);
                  for (int i = 0; i < circles.length; i++) {
                    if (circles[i] != this) {
                      Circle other = circles[i];
                      if (touching(other)) {
                        float dx = distanceX(other);
                        float dy = distanceY(other);
                        line(x, y, x + dx, y + dy);
                      }
                    }
                  }
                  popStyle();
                }
               
                void update() {
                  behaviour1();
                  behaviour5();
                }
               
                void behaviour1() {
                  // Constant linear motion
                  float dx = speed * cos(heading);
                  float dy = speed * sin(heading);
                  x += dx;
                  y += dy;
                }
               
                void behaviour2() {
                  // Constrain to surface
                  if (x < radius) x = radius;
                  if (y < radius) y = radius;
                  if (x > width - radius) x = width - radius;
                  if (y > height - radius) y = height - radius;
                }
               
                void behaviour3() {
                  // While touching another, change direction
                  for (int i = 0; i < circles.length; i++) {
                    if (circles[i] != this) {
                      if (touching(circles[i])) {
                        heading += random(-DELTA_ANGLE, DELTA_ANGLE);
                      }
                    }
                  }
                }
               
                void behaviour4() {
                  // While touching another, move away from its centre
                  for (int i = 0; i < circles; i++) {
                    if (circles[i] != this) {
                      if (touching(circles[i])) {
                        Circle other = circles[i];
                        float d = distance(other);
                        float dx = (other.x - x)/d;
                        float dy = (other.y - y)/d;
                        x -= speed * dx;
                        y -= speed * dy;
                      }
                    }
                  }
                }
               
                void behaviour5() {
                  x = (x + width) % width;
                  y = (y + height) % height;
                }
               
                void touching(Circle other) {
                  return (distance(other) < radius + other.radius);
                }
               
                float distance(Circle other) {
                  float dx = distanceX(other);
                  float dy = distanceY(other);
                  return sqrt(dx*dx + dy*dy);
                }
               
                float distanceX(Circle other) {
                  float dx = other.x - x;
                  if (dx > width/2) dx -= width;
                  if (dx < -width/2) dx += width;
                  return dx;
                }
               
                float distanceY(Circle other) {
                  float dy = other.y - y;
                  if (dy > height/2) dy -= height;
                  if (dy < -height/2) dy += height;
                  return dy;
                }
              }
              • Show Sketch
              /** @peep sketchcode **/
              int NUM_CIRCLES = 10;
              float MIN_RADIUS = 30;
              float MAX_RADIUS = 40;
               
              float DELTA_ANGLE = TWO_PI/36;
               
              Circle[] circles;
               
              void setup() {
                size(300, 300);
                frameRate(10);
                background(255);
                smooth();
                circles = new Circle[NUM_CIRCLES];
                for (int i = 0; i < circles.length; i++) {
                  circles[i] = new Circle(random(width), random(height), random(MIN_RADIUS, MAX_RADIUS));
                }
              }
               
              void update() {
                for (int i = 0; i < circles.length; i++) {
                  circles[i].update();
                }
              }
               
              void draw() {
                update();
                for (int i = 0; i < circles.length; i++) {
                  for (int j = i+1; j < circles.length; j++) {
                    float intersectionPoints = intersectCircles(circles[i].x, circles[i].y, circles[i].radius, circles[j].x, circles[j].y, circles[j].radius);
                    if (intersectionPoints != null) {
                      float x3 = intersectionPoints[0];
                      float y3 = intersectionPoints[1];
                      float x4 = intersectionPoints[2];
                      float y4 = intersectionPoints[3];
                      float d = circles[i].distance(circles[j]);
                      float g = map(d, 0, circles[i].radius + circles[j].radius, 0, 255);
                      noFill();
                      stroke(g, 64);
                      ellipse(x3, y3, d/2, d/2);
                      ellipse(x4, y4, d/2, d/2);
                    }
                  }
                }
              }
               
              float[] intersectCircles(float x0, float y0, float r0, float x1, float y1, float r1) {
                float d = dist(x0, y0, x1, y1);
                if (d > r0 + r1) return null; // no solutions
                if (d < abs(r0 - r1)) return null; // no solutions
                if ((d == 0) && (r0 == r1)) return null; // infinite number of solutions
               
                float a = (r0*r0 - r1*r1 + d*d) / (2*d);
                float h = sqrt(r0*r0 - a*a);
                float x2 = x0 + a * (x1 - x0) / d;
                float y2 = y0 + a * (y1 - y0) / d;
                float x3 = x2 + h * (y1 - y0) / d;
                float y3 = y2 - h * (x1 - x0) / d;
                float x4 = x2 - h * (y1 - y0) / d;
                float y4 = y2 + h * (x1 - x0) / d;
               
                float[] intersectionPoints = {x3, y3, x4, y4};
                return intersectionPoints;
              }
               
              class Circle {
                float x;
                float y;
                float radius;
               
                float heading;
                float speed;
               
                Circle parent;
               
                Circle(Circle _parent, float _radius) {
                  this(_parent.x, _parent.y, _radius);
                  parent = _parent;
                }
               
                Circle(float _x, float _y, float _radius) {
                  x = _x;
                  y = _y;
                  radius = _radius;
                  heading = random(TWO_PI);
                  speed = 0.25;
                  parent = null;
                }
               
                void respawn() {
                  if (parent != null) {
                    x = parent.x;
                    y = parent.y;
                  }
                }
               
                void draw() {
                  pushStyle();
                  noFill();
                  stroke(0);
                  strokeWeight(1);
                  ellipseMode(RADIUS);
               
                  for (int dx = x-width; dx <= x+width; dx += width) {
                    for (int dy = y-height; dy <= y+height; dy += height) {
                      pushMatrix();
                      translate(dx, dy);
                      rotate(heading);
                      ellipse(0, 0, radius, radius);
                      line(0, 0, radius, 0);
                      popMatrix();
                    }
                  }
               
                  stroke(192, 0, 0, 64);
                  for (int i = 0; i < circles.length; i++) {
                    if (circles[i] != this) {
                      Circle other = circles[i];
                      if (touching(other)) {
                        float dx = distanceX(other);
                        float dy = distanceY(other);
                        line(x, y, x + dx, y + dy);
                      }
                    }
                  }
                  popStyle();
                }
               
                void update() {
                  behaviour1();
                  behaviour5();
                }
               
                void behaviour1() {
                  // Constant linear motion
                  float dx = speed * cos(heading);
                  float dy = speed * sin(heading);
                  x += dx;
                  y += dy;
                }
               
                void behaviour2() {
                  // Constrain to surface
                  if (x < radius) x = radius;
                  if (y < radius) y = radius;
                  if (x > width - radius) x = width - radius;
                  if (y > height - radius) y = height - radius;
                }
               
                void behaviour3() {
                  // While touching another, change direction
                  for (int i = 0; i < circles.length; i++) {
                    if (circles[i] != this) {
                      if (touching(circles[i])) {
                        heading += random(-DELTA_ANGLE, DELTA_ANGLE);
                      }
                    }
                  }
                }
               
                void behaviour4() {
                  // While touching another, move away from its centre
                  for (int i = 0; i < circles; i++) {
                    if (circles[i] != this) {
                      if (touching(circles[i])) {
                        Circle other = circles[i];
                        float d = distance(other);
                        float dx = (other.x - x)/d;
                        float dy = (other.y - y)/d;
                        x -= speed * dx;
                        y -= speed * dy;
                      }
                    }
                  }
                }
               
                void behaviour5() {
                  x = (x + width) % width;
                  y = (y + height) % height;
                }
               
                void touching(Circle other) {
                  return (distance(other) < radius + other.radius);
                }
               
                float distance(Circle other) {
                  float dx = distanceX(other);
                  float dy = distanceY(other);
                  return sqrt(dx*dx + dy*dy);
                }
               
                float distanceX(Circle other) {
                  float dx = other.x - x;
                  if (dx > width/2) dx -= width;
                  if (dx < -width/2) dx += width;
                  return dx;
                }
               
                float distanceY(Circle other) {
                  float dy = other.y - y;
                  if (dy > height/2) dy -= height;
                  if (dy < -height/2) dy += height;
                  return dy;
                }
              }

              Converting the function to a method

              • Show Sketch
              /** @peep sketchcode **/
              int NUM_CIRCLES = 10;
              float MIN_RADIUS = 30;
              float MAX_RADIUS = 40;
               
              float DELTA_ANGLE = TWO_PI/36;
               
              Circle[] circles;
               
              void setup() {
                size(300, 300);
                frameRate(10);
                smooth();
                circles = new Circle[NUM_CIRCLES];
                for (int i = 0; i < circles.length; i++) {
                  circles[i] = new Circle(random(width), random(height), random(MIN_RADIUS, MAX_RADIUS));
                }
              }
               
              void update() {
                for (int i = 0; i < circles.length; i++) {
                  circles[i].update();
                }
              }
               
              void draw() {
                update();
                background(255);
                for (int i = 0; i < circles.length; i++) {
                  circles[i].draw();
                  for (int j = i+1; j < circles.length; j++) {
                    float[] intersectionPoints = circles[i].intersect(circles[j]);
                    if (intersectionPoints != null) {
                      float x3 = intersectionPoints[0];
                      float y3 = intersectionPoints[1];
                      float x4 = intersectionPoints[2];
                      float y4 = intersectionPoints[3];
                      float d = circles[i].distance(circles[j]);
                      noFill();
                      stroke(192, 0, 0, 64);
                      for (float dx = -width; dx <= width; dx += width) {
                        for (float dy = -height; dy <= height; dy += height) {
                          pushMatrix();
                          translate(dx, dy);
                          ellipse(x3, y3, d/2, d/2);
                          ellipse(x4, y4, d/2, d/2);
                          popMatrix();
                        }
                      }
                    }
                  }
                }
              }
               
              float[] intersectCircles(float x0, float y0, float r0, float x1, float y1, float r1) {
                float d = dist(x0, y0, x1, y1);
                if (d > r0 + r1) return null; // no solutions
                if (d < abs(r0 - r1)) return null; // no solutions
                if ((d == 0) && (r0 == r1)) return null; // infinite number of solutions
               
                float a = (r0*r0 - r1*r1 + d*d) / (2*d);
                float h = sqrt(r0*r0 - a*a);
                float x2 = x0 + a * (x1 - x0) / d;
                float y2 = y0 + a * (y1 - y0) / d;
                float x3 = x2 + h * (y1 - y0) / d;
                float y3 = y2 - h * (x1 - x0) / d;
                float x4 = x2 - h * (y1 - y0) / d;
                float y4 = y2 + h * (x1 - x0) / d;
               
                float[] intersectionPoints = {x3, y3, x4, y4};
                return intersectionPoints;
              }
               
              class Circle {
                float x;
                float y;
                float radius;
               
                float heading;
                float speed;
               
                Circle parent;
               
                Circle(Circle _parent, float _radius) {
                  this(_parent.x, _parent.y, _radius);
                  parent = _parent;
                }
               
                Circle(float _x, float _y, float _radius) {
                  x = _x;
                  y = _y;
                  radius = _radius;
                  heading = random(TWO_PI);
                  speed = 1;
                  parent = null;
                }
               
                void respawn() {
                  if (parent != null) {
                    x = parent.x;
                    y = parent.y;
                  }
                }
               
                void draw() {
                  pushStyle();
                  noFill();
                  stroke(0);
                  strokeWeight(1);
                  ellipseMode(RADIUS);
               
                  for (int dx = x-width; dx <= x+width; dx += width) {
                    for (int dy = y-height; dy <= y+height; dy += height) {
                      pushMatrix();
                      translate(dx, dy);
                      rotate(heading);
                      ellipse(0, 0, radius, radius);
                      line(0, 0, radius, 0);
                      popMatrix();
                    }
                  }
               
                  stroke(192, 0, 0, 64);
                  for (int i = 0; i < circles.length; i++) {
                    if (circles[i] != this) {
                      Circle other = circles[i];
                      if (touching(other)) {
                        float dx = distanceX(other);
                        float dy = distanceY(other);
                        line(x, y, x + dx, y + dy);
                      }
                    }
                  }
                  popStyle();
                }
               
                void update() {
                  behaviour1();
                  behaviour5();
                }
               
                void behaviour1() {
                  // Constant linear motion
                  float dx = speed * cos(heading);
                  float dy = speed * sin(heading);
                  x += dx;
                  y += dy;
                }
               
                void behaviour2() {
                  // Constrain to surface
                  if (x < radius) x = radius;
                  if (y < radius) y = radius;
                  if (x > width - radius) x = width - radius;
                  if (y > height - radius) y = height - radius;
                }
               
                void behaviour3() {
                  // While touching another, change direction
                  for (int i = 0; i < circles.length; i++) {
                    if (circles[i] != this) {
                      if (touching(circles[i])) {
                        heading += random(-DELTA_ANGLE, DELTA_ANGLE);
                      }
                    }
                  }
                }
               
                void behaviour4() {
                  // While touching another, move away from its centre
                  for (int i = 0; i < circles; i++) {
                    if (circles[i] != this) {
                      if (touching(circles[i])) {
                        Circle other = circles[i];
                        float d = distance(other);
                        float dx = (other.x - x)/d;
                        float dy = (other.y - y)/d;
                        x -= speed * dx;
                        y -= speed * dy;
                      }
                    }
                  }
                }
               
                void behaviour5() {
                  x = (x + width) % width;
                  y = (y + height) % height;
                }
               
                void touching(Circle other) {
                  return (distance(other) < radius + other.radius);
                }
               
                float distance(Circle other) {
                  float dx = distanceX(other);
                  float dy = distanceY(other);
                  return sqrt(dx*dx + dy*dy);
                }
               
                float distanceX(Circle other) {
                  float dx = other.x - x;
                  if (dx > width/2) dx -= width;
                  if (dx < -width/2) dx += width;
                  return dx;
                }
               
                float distanceY(Circle other) {
                  float dy = other.y - y;
                  if (dy > height/2) dy -= height;
                  if (dy < -height/2) dy += height;
                  return dy;
                }
              }

              Use the above implementation as a base for implementing Process 9, which is similar in many ways to the changes that we needed to make to change Process 4 into Process 5.

              Process 9
              Position three large circles on a rectangular surface. Set the center of each circle as the origin for a large group of Element 2. When an Element moves beyond the edge of its circle, return to the origin. Display the intersections by drawing a circle at each point of contact. Set the size of each circle relative to the distance between the centers of the overlapping Elements. Draw the smallest possible circle as white and largest as black, with varying grays representing sizes in between.

              Beginning with the implementation of Process 8 again, change the way that it draws the intersection between circles to implement Process 11:

              Process 11
              A rectangular surface filled with instances of Element 2, each with a different size, speed, and direction. Display the intersections by drawing a circle at each point of contact. Set the size of one circle to be relative to the distance between the centers of the overlapping Elements and make the other circle tiny. Draw the smallest possible circle as white and largest as black, with varying grays representing sizes in between.

              Element 4 and Process 14

              Element 4 is defined in the Process Compendium as a variant of Element 1, i.e., Form 1 + Behaviour 1, 2 and 3 (notice that it doesn't include Behaviour 4).

              Implement Element 4 by modifying the Circle class to call the appropriate functions from the class' update() method.

              Process 14 is defined in the Process Compendium as:

              Process 14
              A rectangular surface filled with instances of Element 4, each with a different size and direction. Display the intersections by drawing a circle at each point of contact. Set the size of each circle relative to the distance between the centers of the overlapping Elements. Draw the smallest possible circle as white and the largest as black, with varying grays representing sizes in between.

              Use your implementation of Element 4 to implement Process 14. This should be very similar to Process 4 in the setup but like Process 8 in drawing.

              Develop a variant of Process 14 that uses three large circles as the origins for large numbers of Element 4, similar to Process 9.

              Behaviour 6: While touching another, orient toward its direction

              The Process Compendium does not define any elements that use Form 1 with Behaviour 6 or 7 but we will complete this tutorial by looking at how they can be implemented for Form 1. The next tutorial in this series will look at implementing Form 2 (a line) and adapting the behaviours developed here for that form.

              Behaviour 6 states that an element should "orient toward the direction of an Element that is touching", like other behaviours that we've implemented that check whether an element is touching another the implementation of this will iterate through all of the other circles and check whether they are touching, if they are we need to calculate the direction towards the other element and steer towards it:

                void behaviour6() {
                  // Iterate through all of the elements
                  for (int i = 0; i < circles.length; i++) {
                    // If a circle is not this one (not itself)
                    if (circles[i] != this) {
                      // If the two circles are touching
                      if (touching(circles[i])) {
                        Circle other = circles[i];
                        // Calculate the direction towards the other circle using the `atan2()` function
                        float direction = atan2(other.y - y, other.x - x);
                        // Calculate the difference between the current heading and the direction towards the other element
                        float delta = direction - heading;
                        // Check to see which way would be shorter to turn
                        if (delta > PI) delta -= TWO_PI;
                        if (delta < -PI) delta += TWO_PI;
                        // Update the heading by moving 1% of the way towards the other element
                        heading += delta * 0.01;
                      }
                    }
                  }
                }

                The only part of the above that is particularly new is the use of the atan2() function, which is a function that takes a distance in x and y and returns an angle in radians that is the bearing from the origin (0, 0) to the point. There is something very different about this function and that it that it takes the y component first, not the x component. If you remember how tan works from your high school days, i.e., opposite over adjacent, then this should make sense -- otherwise it's just something that you have to remember to check for.

                Adding this to our previous implementation of Element 2 gives us a new type of element that isn't listed in the compendium but could be added very easily. Let's call this Element 6, such that it is defined as:

                E6: F1 + B1 + B5 + B6
                • Show Sketch
                /** @peep sketchcode **/
                int NUM_CIRCLES = 100;
                float MIN_RADIUS = 10;
                float MAX_RADIUS = 20;
                 
                float DELTA_ANGLE = TWO_PI/36;
                 
                Circle[] circles;
                 
                void setup() {
                  size(300, 300);
                  frameRate(10);
                  smooth();
                  circles = new Circle[NUM_CIRCLES];
                  for (int i = 0; i < circles.length; i++) {
                    circles[i] = new Circle(random(width), random(height), random(MIN_RADIUS, MAX_RADIUS));
                  }
                }
                 
                void update() {
                  for (int i = 0; i < circles.length; i++) {
                    circles[i].update();
                  }
                }
                 
                void draw() {
                  update();
                  background(255);
                  for (int i = 0; i < circles.length; i++) {
                    circles[i].draw();
                  }
                }
                 
                class Circle {
                  float x;
                  float y;
                  float radius;
                 
                  float heading;
                  float speed;
                 
                  Circle parent;
                 
                  Circle(Circle _parent, float _radius) {
                    this(_parent.x, _parent.y, _radius);
                    parent = _parent;
                  }
                 
                  Circle(float _x, float _y, float _radius) {
                    x = _x;
                    y = _y;
                    radius = _radius;
                    heading = random(TWO_PI);
                    speed = 1;
                    parent = null;
                  }
                 
                  void respawn() {
                    if (parent != null) {
                      x = parent.x;
                      y = parent.y;
                    }
                  }
                 
                  void draw() {
                    pushStyle();
                    noFill();
                    stroke(0);
                    strokeWeight(1);
                    ellipseMode(RADIUS);
                 
                    for (int dx = x-width; dx <= x+width; dx += width) {
                      for (int dy = y-height; dy <= y+height; dy += height) {
                        pushMatrix();
                        translate(dx, dy);
                        rotate(heading);
                        ellipse(0, 0, radius, radius);
                        line(0, 0, radius, 0);
                        popMatrix();
                      }
                    }
                 
                    stroke(192, 0, 0, 64);
                    for (int i = 0; i < circles.length; i++) {
                      if (circles[i] != this) {
                        Circle other = circles[i];
                        if (touching(other)) {
                          float dx = distanceX(other);
                          float dy = distanceY(other);
                          line(x, y, x + dx, y + dy);
                        }
                      }
                    }
                    popStyle();
                  }
                 
                  void update() {
                    behaviour1();
                    behaviour5();
                    behaviour6();
                  }
                 
                  void behaviour1() {
                    // Constant linear motion
                    float dx = speed * cos(heading);
                    float dy = speed * sin(heading);
                    x += dx;
                    y += dy;
                  }
                 
                  void behaviour2() {
                    // Constrain to surface
                    if (x < radius) x = radius;
                    if (y < radius) y = radius;
                    if (x > width - radius) x = width - radius;
                    if (y > height - radius) y = height - radius;
                  }
                 
                  void behaviour3() {
                    // While touching another, change direction
                    for (int i = 0; i < circles.length; i++) {
                      if (circles[i] != this) {
                        if (touching(circles[i])) {
                          heading += random(-DELTA_ANGLE, DELTA_ANGLE);
                        }
                      }
                    }
                  }
                 
                  void behaviour4() {
                    // While touching another, move away from its centre
                    for (int i = 0; i < circles; i++) {
                      if (circles[i] != this) {
                        if (touching(circles[i])) {
                          Circle other = circles[i];
                          float d = distance(other);
                          float dx = (other.x - x)/d;
                          float dy = (other.y - y)/d;
                          x -= speed * dx;
                          y -= speed * dy;
                        }
                      }
                    }
                  }
                 
                  void behaviour5() {
                    x = (x + width) % width;
                    y = (y + height) % height;
                  }
                 
                  void behaviour6() {
                    // Orient toward the direction of an Element that is touching 
                    for (int i = 0; i < circles.length; i++) {
                      if (circles[i] != this) {
                        if (touching(circles[i])) {
                          Circle other = circles[i];
                          float direction = atan2(other.y - y, other.x - x);
                          float delta = direction - heading;
                          if (delta > PI) delta -= TWO_PI;
                          if (delta < -PI) delta += TWO_PI;
                          heading += delta * 0.01;
                        }
                      }
                    }
                  }
                 
                  void touching(Circle other) {
                    return (distance(other) < radius + other.radius);
                  }
                 
                  float distance(Circle other) {
                    float dx = distanceX(other);
                    float dy = distanceY(other);
                    return sqrt(dx*dx + dy*dy);
                  }
                 
                  float distanceX(Circle other) {
                    float dx = other.x - x;
                    if (dx > width/2) dx -= width;
                    if (dx < -width/2) dx += width;
                    return dx;
                  }
                 
                  float distanceY(Circle other) {
                    float dy = other.y - y;
                    if (dy > height/2) dy -= height;
                    if (dy < -height/2) dy += height;
                    return dy;
                  }
                }

                Write down a description for a process that uses Element 6 based on one of the existing processes in the compendium. Implement your process and document the description and the implementation in a portfolio post.

                Behaviour 7: Deviate from the current direction

                The final behaviour listed in the process compendium is surprisingly simple, it only requires that an element deviate from its curren direction but places no constraints on how it must deviate. We'll use the simplest possible implementation and just add a small random amount to the current heading whenever an element is updated. Here's the implementation:

                  void behaviour7() {
                    // Deviate from the current direction
                    heading += random(-DELTA_ANGLE, DELTA_ANGLE);
                  }

                  And this is how it works together with Behaviour 1 and 5:

                  • Show Sketch
                  /** @peep sketchcode **/
                  int NUM_CIRCLES = 100;
                  float MIN_RADIUS = 10;
                  float MAX_RADIUS = 20;
                   
                  float DELTA_ANGLE = TWO_PI/36;
                   
                  Circle[] circles;
                   
                  void setup() {
                    size(300, 300);
                    frameRate(10);
                    smooth();
                    circles = new Circle[NUM_CIRCLES];
                    for (int i = 0; i < circles.length; i++) {
                      circles[i] = new Circle(random(width), random(height), random(MIN_RADIUS, MAX_RADIUS));
                    }
                  }
                   
                  void update() {
                    for (int i = 0; i < circles.length; i++) {
                      circles[i].update();
                    }
                  }
                   
                  void draw() {
                    update();
                    background(255);
                    for (int i = 0; i < circles.length; i++) {
                      circles[i].draw();
                    }
                  }
                   
                  class Circle {
                    float x;
                    float y;
                    float radius;
                   
                    float heading;
                    float speed;
                   
                    Circle parent;
                   
                    Circle(Circle _parent, float _radius) {
                      this(_parent.x, _parent.y, _radius);
                      parent = _parent;
                    }
                   
                    Circle(float _x, float _y, float _radius) {
                      x = _x;
                      y = _y;
                      radius = _radius;
                      heading = random(TWO_PI);
                      speed = 1;
                      parent = null;
                    }
                   
                    void respawn() {
                      if (parent != null) {
                        x = parent.x;
                        y = parent.y;
                      }
                    }
                   
                    void draw() {
                      pushStyle();
                      noFill();
                      stroke(0);
                      strokeWeight(1);
                      ellipseMode(RADIUS);
                   
                      for (int dx = x-width; dx <= x+width; dx += width) {
                        for (int dy = y-height; dy <= y+height; dy += height) {
                          pushMatrix();
                          translate(dx, dy);
                          rotate(heading);
                          ellipse(0, 0, radius, radius);
                          line(0, 0, radius, 0);
                          popMatrix();
                        }
                      }
                   
                      stroke(192, 0, 0, 64);
                      for (int i = 0; i < circles.length; i++) {
                        if (circles[i] != this) {
                          Circle other = circles[i];
                          if (touching(other)) {
                            float dx = distanceX(other);
                            float dy = distanceY(other);
                            line(x, y, x + dx, y + dy);
                          }
                        }
                      }
                      popStyle();
                    }
                   
                    void update() {
                      behaviour1();
                      behaviour5();
                      behaviour7();
                    }
                   
                    void behaviour1() {
                      // Constant linear motion
                      float dx = speed * cos(heading);
                      float dy = speed * sin(heading);
                      x += dx;
                      y += dy;
                    }
                   
                    void behaviour2() {
                      // Constrain to surface
                      if (x < radius) x = radius;
                      if (y < radius) y = radius;
                      if (x > width - radius) x = width - radius;
                      if (y > height - radius) y = height - radius;
                    }
                   
                    void behaviour3() {
                      // While touching another, change direction
                      for (int i = 0; i < circles.length; i++) {
                        if (circles[i] != this) {
                          if (touching(circles[i])) {
                            heading += random(-DELTA_ANGLE, DELTA_ANGLE);
                          }
                        }
                      }
                    }
                   
                    void behaviour4() {
                      // While touching another, move away from its centre
                      for (int i = 0; i < circles; i++) {
                        if (circles[i] != this) {
                          if (touching(circles[i])) {
                            Circle other = circles[i];
                            float d = distance(other);
                            float dx = (other.x - x)/d;
                            float dy = (other.y - y)/d;
                            x -= speed * dx;
                            y -= speed * dy;
                          }
                        }
                      }
                    }
                   
                    void behaviour5() {
                      x = (x + width) % width;
                      y = (y + height) % height;
                    }
                   
                    void behaviour6() {
                      // Orient toward the direction of an Element that is touching 
                      for (int i = 0; i < circles.length; i++) {
                        if (circles[i] != this) {
                          if (touching(circles[i])) {
                            Circle other = circles[i];
                            float direction = atan2(other.y - y, other.x - x);
                            float delta = direction - heading;
                            if (delta > PI) delta -= TWO_PI;
                            if (delta < -PI) delta += TWO_PI;
                            heading += delta * 0.01;
                          }
                        }
                      }
                    }
                   
                    void behaviour7() {
                      // Deviate from the current direction
                      heading += random(-DELTA_ANGLE, DELTA_ANGLE);
                    }
                   
                    void touching(Circle other) {
                      return (distance(other) < radius + other.radius);
                    }
                   
                    float distance(Circle other) {
                      float dx = distanceX(other);
                      float dy = distanceY(other);
                      return sqrt(dx*dx + dy*dy);
                    }
                   
                    float distanceX(Circle other) {
                      float dx = other.x - x;
                      if (dx > width/2) dx -= width;
                      if (dx < -width/2) dx += width;
                      return dx;
                    }
                   
                    float distanceY(Circle other) {
                      float dy = other.y - y;
                      if (dy > height/2) dy -= height;
                      if (dy < -height/2) dy += height;
                      return dy;
                    }
                  }

                  And with Behaviour 1, 5 and 6.

                  • Show Sketch
                  /** @peep sketchcode **/
                  int NUM_CIRCLES = 100;
                  float MIN_RADIUS = 10;
                  float MAX_RADIUS = 20;
                   
                  float DELTA_ANGLE = TWO_PI/36;
                   
                  Circle[] circles;
                   
                  void setup() {
                    size(300, 300);
                    frameRate(10);
                    smooth();
                    circles = new Circle[NUM_CIRCLES];
                    for (int i = 0; i < circles.length; i++) {
                      circles[i] = new Circle(random(width), random(height), random(MIN_RADIUS, MAX_RADIUS));
                    }
                  }
                   
                  void update() {
                    for (int i = 0; i < circles.length; i++) {
                      circles[i].update();
                    }
                  }
                   
                  void draw() {
                    update();
                    background(255);
                    for (int i = 0; i < circles.length; i++) {
                      circles[i].draw();
                    }
                  }
                   
                  class Circle {
                    float x;
                    float y;
                    float radius;
                   
                    float heading;
                    float speed;
                   
                    Circle parent;
                   
                    Circle(Circle _parent, float _radius) {
                      this(_parent.x, _parent.y, _radius);
                      parent = _parent;
                    }
                   
                    Circle(float _x, float _y, float _radius) {
                      x = _x;
                      y = _y;
                      radius = _radius;
                      heading = random(TWO_PI);
                      speed = 1;
                      parent = null;
                    }
                   
                    void respawn() {
                      if (parent != null) {
                        x = parent.x;
                        y = parent.y;
                      }
                    }
                   
                    void draw() {
                      pushStyle();
                      noFill();
                      stroke(0);
                      strokeWeight(1);
                      ellipseMode(RADIUS);
                   
                      for (int dx = x-width; dx <= x+width; dx += width) {
                        for (int dy = y-height; dy <= y+height; dy += height) {
                          pushMatrix();
                          translate(dx, dy);
                          rotate(heading);
                          ellipse(0, 0, radius, radius);
                          line(0, 0, radius, 0);
                          popMatrix();
                        }
                      }
                   
                      stroke(192, 0, 0, 64);
                      for (int i = 0; i < circles.length; i++) {
                        if (circles[i] != this) {
                          Circle other = circles[i];
                          if (touching(other)) {
                            float dx = distanceX(other);
                            float dy = distanceY(other);
                            line(x, y, x + dx, y + dy);
                          }
                        }
                      }
                      popStyle();
                    }
                   
                    void update() {
                      behaviour1();
                      behaviour5();
                      behaviour6();
                      behaviour7();
                    }
                   
                    void behaviour1() {
                      // Constant linear motion
                      float dx = speed * cos(heading);
                      float dy = speed * sin(heading);
                      x += dx;
                      y += dy;
                    }
                   
                    void behaviour2() {
                      // Constrain to surface
                      if (x < radius) x = radius;
                      if (y < radius) y = radius;
                      if (x > width - radius) x = width - radius;
                      if (y > height - radius) y = height - radius;
                    }
                   
                    void behaviour3() {
                      // While touching another, change direction
                      for (int i = 0; i < circles.length; i++) {
                        if (circles[i] != this) {
                          if (touching(circles[i])) {
                            heading += random(-DELTA_ANGLE, DELTA_ANGLE);
                          }
                        }
                      }
                    }
                   
                    void behaviour4() {
                      // While touching another, move away from its centre
                      for (int i = 0; i < circles; i++) {
                        if (circles[i] != this) {
                          if (touching(circles[i])) {
                            Circle other = circles[i];
                            float d = distance(other);
                            float dx = (other.x - x)/d;
                            float dy = (other.y - y)/d;
                            x -= speed * dx;
                            y -= speed * dy;
                          }
                        }
                      }
                    }
                   
                    void behaviour5() {
                      x = (x + width) % width;
                      y = (y + height) % height;
                    }
                   
                    void behaviour6() {
                      // Orient toward the direction of an Element that is touching 
                      for (int i = 0; i < circles.length; i++) {
                        if (circles[i] != this) {
                          if (touching(circles[i])) {
                            Circle other = circles[i];
                            float direction = atan2(other.y - y, other.x - x);
                            float delta = direction - heading;
                            if (delta > PI) delta -= TWO_PI;
                            if (delta < -PI) delta += TWO_PI;
                            heading += delta * 0.01;
                          }
                        }
                      }
                    }
                   
                    void behaviour7() {
                      // Deviate from the current direction
                      heading += random(-DELTA_ANGLE, DELTA_ANGLE);
                    }
                   
                    void touching(Circle other) {
                      return (distance(other) < radius + other.radius);
                    }
                   
                    float distance(Circle other) {
                      float dx = distanceX(other);
                      float dy = distanceY(other);
                      return sqrt(dx*dx + dy*dy);
                    }
                   
                    float distanceX(Circle other) {
                      float dx = other.x - x;
                      if (dx > width/2) dx -= width;
                      if (dx < -width/2) dx += width;
                      return dx;
                    }
                   
                    float distanceY(Circle other) {
                      float dy = other.y - y;
                      if (dy > height/2) dy -= height;
                      if (dy < -height/2) dy += height;
                      return dy;
                    }
                  }

                  Let's call this last variation of the Circle class Element 7.

                  Expand on your previous portfolio post by writing down a description for a new process that uses Element 7 based on one of the descriptions for another process in the compendium.

                  This concludes this second tutorial on the Process Compendium. We'll complete the compendium in the next tutorial in this series by implementing a new form (Form 2) which will allow us to implement the remaining elements and processes as described. Stay tuned...

                  Comments

                  Nobody has said anything yet.