Tutorial 17: The Process Compendium (Part 2)

Continuing the Process Compendium

This tutorial builds on the previous tutorial where we built a class that represents Form 1 (circle) in the Process Compendium and added methods to it to implement Behaviour 1, 2, 3 and 4. This allowed us to model Element 1 in the Process Compendium and implement Process 4.

To expand our repertoire of elements and processes, this tutorial will look at implementing the another behaviour from the Process Compendium and explore two more processes that use Form 1.

The implementation of Process 5 was set as an exercise in the previous tutorial, so it won't be covered here. If you haven't already done so complete Tutorial 15 and attempt to implement Process 5 before continuing with this tutorial. Note that the implementation of Process 5 only requires that you add some additional drawing code to the draw() function for the sketch, not the Circle class.

Process 7

Before we start on implementing Process 6, practice working with the existing code and implement Process 7, which combines the varying shades of grey from Process 4 with the drawing of the perimeter and centre dot of Process 5.

Process 7
A rectangular surface filled with varying sizes of Element 1. Draw a line from the centers of Elements that are touching. Set the value of the shortest possible line to white and the longest to black, with varying grays representing values in between. Draw the perimeter of each Element as a black line and the center as a white dot.

Note: you might have already interpreted Process 5 to mean the same as this, in which case change your implementation of Process 5 to better match your understanding of the differences between Process 5 and Process 7.

Process 6

Process 6 is described in the Process Compendium as:

Process 6
Position three large circles on a rectangular surface. Set the center of each circle as the origin for a large group of Element 1. When an Element moves beyond the edge of its circle, return to the origin. Draw a line from the centers of Elements that are touching. Set the value of the shortest possible line to black and the longest to white, with varying grays representing values in between.

The drawing of lines between the centres of circles is very similar to Process 4 so the draw() function in our implementation of Process 6 will be the same:

    void draw() {
      update(); // Update all of the circles
      strokeWeight(0.5);
      for (int i = 0; i < circles.length; i++) {
        // Get a first circle
        Circle circle1 = circles[i];
        for (int j = i+1; j < circles.length; j++) {
          // Get a second circle
          Circle circle2 = circles[j];
          // If the circles are touching
          if (circle1.touching(circle2)) {
            // Calculate the grey value using the map function based on the distance between the circles
            stroke(map(circle1.distance(circle2), 0, circle1.radius + circle2.radius, 0, 255));
            // Draw a line between the centres of the circles
            line(circle1.x, circle1.y, circle2.x, circle2.y);
        }
      }
    }

    The two differences between Process 4 and Process 6 that we need to implement are: (1) the initialisation of elements at the centres of "three large circles"; and, (2) the movement of elements back to the centre of these circles once they move beyond the edge of them.

    Three Large Circles

    Let's start by implementing the initialisation of the elements at the centres of three large circles. As you have probably guessed we'll need to change the setup() function to implement this, but first we'll need some variables to hold the data about the large circles. We could implement these as simple variables:

      float x1, y1, r1; // Position and size of first large circle
      float x2, y2, r2; // Position and size of first large circle
      float x3, y3, r3; // Position and size of first large circle
       
      void setup() {
        // Other setup stuff here...
       
        x1 = random(width);
        y1 = random(height);
        r1 = random(0.4*width, 0.6*width);
        // repeat for x2, y2, r2, x3, y3, r3
       
        // Initialise elements here...
      }

      But this is all rather tediuous and we already have a Circle class, so why not use it?

        Circle largeCircle1, largeCircle2, largeCircle3;
         
        void setup() {
          // Other setup stuff here...
         
          largeCircle1 = new Circle(random(width), random(height), random(0.4*width, 0.6*width));
          largeCircle2 = new Circle(random(width), random(height), random(0.4*width, 0.6*width));
          largeCircle3 = new Circle(random(width), random(height), random(0.4*width, 0.6*width));
         
          // Initialise elements here...
        }

        Or we could put our large circles into an array, like this:

          Circle[] largeCircles;
           
          void setup() {
            // Other setup stuff here...
           
            largeCircles = new Circle[3];
            largeCircles[0] = new Circle(random(width), random(height), random(0.4*width, 0.6*width));
            largeCircles[1] = new Circle(random(width), random(height), random(0.4*width, 0.6*width));
            largeCircles[2] = new Circle(random(width), random(height), random(0.4*width, 0.6*width));
           
            // Initialise elements here...
          }

          The important thing to understand here is that we can use the Circle class without having to call the update() function. When we do this we're not using any of the behaviours that we've implemented and we're just using the class as a convenient container for the data related to a circle.

          Parents

          Once we have some large circles, we need to initialise "large group of Element 1" at the centres of each of these circles. We want to remember which circle each element belongs to, so we'll first make a small change to the Circle class to store a reference to a circle's "parent", like this:

            class Circle {
              float x;
              float y;
              float radius;
             
              float heading;
              float speed;
             
              Circle parent;
             
              // etc...
            }

            For convenience, we can also write a new constructor that takes a "parent" circle and uses it to initialise a new circle's position before storing it in the parent field, like this:

              class Circle {
                // Fields go here...
               
                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;
                }
               
                // etc...
              }

              Notice that this new constructor makes use of the old one, so that we don't repeat code unnecessarily. Also, notice that the old constructor has been updated slightly to set parent to null, to ensure that it always has a valid value, this is generally a good idea whenever you have fields in a class that might not be initialised. Once the old constructor has done it's thing the new constructor then sets the value for parent to the one that's been passed in.

              To use this new constructor we just need to change the loop that allocates the array and initialises the circles to go into it in the setup() function. This goes after the initialisation of any large circles so that they can be passed in to our circles that will be our elements in this process. The process description does not state that we need to assign the same number of elements to each of the circles, so this implementation simply uses the random() function to choose which of the circles each element is assigned to, which will mean that they are roughly the same but not exactly.

                int NUM_LARGE_CIRCLES = 3;
                int NUM_CIRCLES = 150;
                float MIN_RADIUS = 4;
                float MAX_RADIUS = 8;
                 
                Circle[] largeCircles;
                Circle[] circles;
                 
                void setup() {
                  // Other setup stuff here...
                 
                  largeCircles = new Circle[NUM_LARGE_CIRCLES];
                  for (int i = 0; i < largeCircles.length; i++) {
                    largeCircles[i] = new Circle(random(width), random(height), random(0.4*width, 0.6*width));
                  }
                 
                  circles = new Circle[NUM_CIRCLES];
                  for (int i = 0; i < circles.length; i++) {
                    int r = int(random(NUM_LARGE_CIRCLES)); // Choose a large circle at random
                    new Circle(largeCircles[r], random(MIN_RADIUS, MAX_RADIUS));
                  }
                }

                Respawn

                There is more thing that we can do to make implementing Process 6 really easy and that is to add a new method to the Circle class that will "respawn" the circle back to it's original position, the implementation of this method is very simple:

                  void respawn() {
                    if (parent != null) {
                      x = parent.x;
                      y = parent.y;
                    }
                  }

                  Notice that this method checks that parent is not null before accessing it. This is just a precaution to stop our sketch from crashing if we (accidentally?) call respawn() on a circle that doesn't have a parent.

                  How would you change the respawn() function to do something if the parent is null? Add the necessary code to print a warning to the console if this function gets called on a circle that doesn't have a parent. You shouldn't see this warning when you run this code at the moment but you could test it by calling respawn() on one of the large circles, which don't have a parent defined.

                  To use the respawn() method in our implementation of Process 6, we need to change the update() function for our sketch to check whether an element is still touching its parent, if it is no longer touching its parent then it is respawned back to it's original location:

                    void update() {
                      for (int i = 0; i < circles.length; i++) {
                        circles[i].update();
                        if (!circles[i].touching(circles[i].parent)) {
                          circles[i].respawn();
                        }
                      }
                    }

                    Implementing Process 6

                    And that's it, we've implemented all of the new things that we need for Process 6 and here's our complete implementation:

                    • Show Sketch
                    /** @peep sketchcode **/
                    int NUM_LARGE_CIRCLES = 3;
                    int NUM_CIRCLES = 150;
                    float MIN_RADIUS = 8;
                    float MAX_RADIUS = 6;
                     
                    float DELTA_ANGLE = TWO_PI/36;
                     
                    Circle[] largeCircles;
                    Circle[] circles;
                     
                    void setup() {
                      size(300, 300);
                      frameRate(10);
                      background(255);
                      smooth();
                      largeCircles = new Circle[NUM_LARGE_CIRCLES];
                      for (int i = 0; i < largeCircles.length; i++) {
                        largeCircles[i] = new Circle(random(width), random(height), random(0.4*width, 0.6*width));
                      }
                      circles = new Circle[NUM_CIRCLES];
                      for (int i = 0; i < circles.length; i++) {
                        int r = int(random(NUM_LARGE_CIRCLES)); // Choose a large circle at random
                        circles[i] = new Circle(largeCircles[r], random(MIN_RADIUS, MAX_RADIUS));
                      }
                    }
                     
                    void update() {
                      for (int i = 0; i < circles.length; i++) {
                        circles[i].update();
                        if (!circles[i].touching(circles[i].parent)) {
                          circles[i].respawn();
                        }
                      }
                    }
                     
                    void draw() {
                      update(); // Update all of the circles
                      strokeWeight(0.5);
                      for (int i = 0; i < circles.length; i++) {
                        // Get a first circle
                        Circle circle1 = circles[i];
                        for (int j = i+1; j < circles.length; j++) {
                          // Get a second circle
                          Circle circle2 = circles[j];
                          // If the circles are touching
                          if (circle1.touching(circle2)) {
                            // Calculate the grey value using the map function based on the distance between the circles
                            stroke(map(circle1.distance(circle2), 0, circle1.radius + circle2.radius, 0, 255));
                            // Draw a line between the centres of the circles
                            line(circle1.x, circle1.y, circle2.x, circle2.y);
                          }
                        }
                      }
                    }
                     
                    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);
                        pushMatrix();
                        translate(x, y);
                        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++) {
                          Circle other = circles[i];
                          if (touching(other)) {
                            line(x, y, other.x, other.y);
                          }
                        }
                        popStyle();
                      }
                     
                      void update() {
                        behaviour1();
                        behaviour2();
                        behaviour3();
                        // behaviour4();
                      }
                     
                      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 touching(Circle other) {
                        return (distance(other) < radius + other.radius);
                      }
                     
                      float distance(Circle other) {
                        return dist(x, y, other.x, other.y);
                      }
                    }

                    Process 10

                    Adapt the implementation of Process 6 to implement Process 10:

                    Process 10
                    Position a circle at the center of a rectangular surface. Set the center of the circle as the origin for a large group of Element 1. When an Element moves beyond the edge of its circle, return to the origin. Draw a line from the centers of Elements that are touching. Set the value of the shortest possible line to black and the longest to white, with varying grays representing values in between.

                    Use your implementations of the draw() function for Process 5 and Process 7 to implement new processes based on Process 6 (and Process 10) that draw the perimeter of each element as a black line and the center as a white dot and draw a gray line (with and without varying shades) from the centers of elements that are touching.

                    Process 13

                    Process 13 also uses Element 1. How would you start implementing this process based on the approach we've taken above?

                    Process 13
                    Bisect a rectangular surface and define the dividing line as the origin for a large group of Element 1. When each Element moves beyond the surface, move its position back to the origin. Draw a line from the centers of Elements that are touching. Set the value of the shortest possible line to black and the longest to white, with varying grays representing values in between.

                    Hint: While the description calls for a line, we can implement this as a large number of parents positioned along a line.

                    Comments

                    Nobody has said anything yet.