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
// 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 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...

parent = _parent;
}

Circle(float _x, float _y, float _radius) {
x = _x;
y = _y;
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;

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

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

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
// 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 speed;

Circle parent;

parent = _parent;
}

Circle(float _x, float _y, float _radius) {
x = _x;
y = _y;
speed = 1;
parent = null;
}

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

void draw() {
pushStyle();
noFill();
stroke(0);
strokeWeight(1);
pushMatrix();
translate(x, y);
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
}

void behaviour3() {
// While touching another, change direction
for (int i = 0; i < circles.length; i++) {
if (circles[i] != this) {
if (touching(circles[i])) {
}
}
}
}

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) {
}

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.