Tutorial 18: The Process Compendium (Part 3)

Behaviour 5: After moving off the surface, enter from the opposite edge

As we've already seen, Element 1 is the most common element used in the Process Compendium, accounting for 6 of the 15 processes for which descriptions have been provided. Element 2 is one of the next most common elements, along with Element 3, being used in Process 8, 9 and 11. Element 2 is also based on Form 1 (circle) but has just 2 behaviours (1 and 5). To implement the next few processes we'll start by looking at Behaviour 5. This behaviour specifies a different behaviour at the edges of the surface (display window) than the previously implemented Behaviour 2, which constrained elements to the surface. Behaviour 5 states that elements should "wrap around" the surface and when they move off one edge they should appear on the other. Clearly, any element that uses Behaviour 5 cannot also use Behaviour 2.

The implementation of Behaviour 5 is fairly simple but the consequences of using this behaviour make some other parts of the implementation more complex. Let's start with the easy part, implementing the behaviour. A simple to understand implementation of Behaviour 5 would be a series of tests, much like those we used for Behaviour 2:

    void behaviour5() {
      if (x <= 0) { x += width; }
      if (x >= width) { x -= width; }
      if (y <= 0) { y += height; }
      if (y >= height) { y += height; }
    }

    These tests check to see whether x or y are outside the bounds of the display window and if they are, they "wrap" the element around the surface by adding or subtracting the appropriate dimension so that the object will appear on the opposite side of the display surface.

    This implementation is perfectly fine and it has the advantage of being easily understood but there is another way to implement this behaviour, which given that this is something that is commonly used it is good to be familiar with. The following method also implements Behaviour 5 but it uses the modulus operator to do so:

      void behaviour5() {
        x = (x + width) % width;
        y = (y + height) % height;
      }

      There are two things to understand about how this function works. The first is that if x is bigger than width then x % width will return a value that is the same as x - width, i.e., taking the modulus will wrap the value of x around. The problem is that the modulus of a negative number, i.e., when x is less than zero will continue to be a negative number, which is not what we want. To correct for this, we add width to the value of x, which will ensure that x + width is always a positive value. Notice that adding the same value as we are taking the modulus of does not change the calculation for positive values of x or y.

      While all this mathematical trickery may seem unnecessary, once you understand the how it works it becomes very natural to use this code whenever you need a value to "wrap around" in this way. If you're not comfortable with it, simply use the conditional statements in your own code.

      To test this new behaviour we'll just use Behaviour 1 and Behaviour 5 and call the appropriate methods from the update() method. As we can see when we run the code below, the result is what we would expect for the most part, but there are a few quirks around the edges that we might want to correct:

      • 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);
       
          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++) {
            if (circles[i] != this) {
              Circle other = circles[i];
              if (touching(other)) {
                line(x, y, other.x, other.y);
              }
            }
          }
          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) {
          return dist(x, y, other.x, other.y);
        }
      }

      The biggest visual problem that we might want to correct with this implementation is that the circles appear to "pop" in and out of the screen as they move over the edges. We can fix that by changing how we draw the circles, but this won't affect the behaviour of the elements. A more subtle, but more serious problem, is that the distance between elements close to each when measured around the edges of the display are being calculated incorrectly, i.e., the distance calculation doesn't know anything about the wrapping around of the surface over the edges and this affects the behaviour of any functions that need to test distances whether circles are touching. The next two chapters will address these two problems.

      Drawing Over the Edges

      The visual problem of circles "popping" when they wrap around from one side of the display to the other can be solved by drawing the shape multiple times based on the current "true" position. Each of the offset locations should be width or height distance away from the original. In total, to cover all possible places that the element could appear partially on the screen we will draw a total of nine versions of the shape, using transformation functions to move the drawing code around.

        void draw() {
          // Setup drawing here...
         
          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();
            }
          }
         
          // Finish drawing here...
        }

        The result of adding this code is exactly what we want in terms of the display, the circles appear to wrap around, being drawn on multiple sides of the display at the same time where necessary:

        • 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)) {
                  line(x, y, other.x, other.y);
                }
              }
            }
            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) {
            return dist(x, y, other.x, other.y);
          }
        }

        NOTE: It might seem wasteful to draw the same shape nine times, when only a maximum of four of the shapes will be visible at any one time but checking to see whether or not we should draw each shape will actually be slower than letting the graphics system "cull" any unnecessary drawing commands.

        Calculating Distance Over Edges

        The remaining problem is that distances are not being calculated correctly between elements when the shortest distance between two elements is across on (or more) of the edges. To get a better understanding of what's going on, the following code has a smaller number of large circles:

        • 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();
          }
        }
         
        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)) {
                  line(x, y, other.x, other.y);
                }
              }
            }
            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) {
            return dist(x, y, other.x, other.y);
          }
        }

        Notice how the red lines between the centres of circles that are meant to indicate that two circles are touching doesn't get drawn between circles near the edge of the display when the centres of the circles are on opposite sides of the display. To fix this we'll need to rethink how we calculate the distance between two circles to account for the potential wrap-around. The following code achieves this by calculating the distance in x and y using the functions distanceX() and distanceY(). These functions check whether it is shorter to go over an edge by checking to see if the distance is greater than half the distance across the surface. The intuition here is that if a distance between two circles is greater than half the distance across the circle, it must be shorter to "go the other way around" and wrap around the edge of the screen. With the difference in x and y calculated to be the shortest possible, we can simple use the sqrt() function to calculate the total distance:

          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;
          }

          This is the main difference that will affect the behaviour of elements that use it and so is the most important part. But we should also fix the drawing code to ensure that lines between elements are properly drawn between there centres. To see what the problem is, here is the sketch from above with the new distance measuring code:

          • 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();
            }
          }
           
          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)) {
                    line(x, y, other.x, other.y);
                  }
                }
              }
              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;
            }
          }

          As you can see the red lines that are being drawn between circles that are touching over one of the edges are being drawn across the display because the draw code doesn't know anything about wrapping around over edges. The code below draws the red line relative to the centre of a circle towards the touching element, using distanceX() and distanceY() to calculate on the end point, this ensures that the line is drawn in the direction of the shortest path to the other circle.

            void draw() {
              pushStyle();
             
              // ... CODE TO DRAW CIRCLE HERE ...
             
              stroke(192, 0, 0, 64);
              for (int i = 0; i < circles.length; i++) {
                Circle other = circles[i];
                if (touching(other)) {
                  float dx = distanceX(other);
                  float dy = distanceY(other);
                  line(x, y, x + dx, y + dy);
                }
              }
              popStyle();
            }

            Putting it all together we get:

            • 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();
              }
             
              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;
              }
            }

            We now have the necessary behaviours to implement Element 2, which is used in Process 8 and others.

            Comments

            Nobody has said anything yet.