Design Computation
Render as Slideshow | Accordion

Associativity

This module covers ideas relating to the linking of geometries together through parameters and properties, the latter being quantities calculated from parameters, which allows us to mimic much of the functionality from software like Revit.

This requires a fundamental understanding of functions in Processing, which is also our first opportunity to begin redefining the language that we speak from something purely computational to something more closely related to a design problem. By defining our own functions, we can start to move to a higher level of understanding and expression than mathematics, logic, and geometry.

Readings

  1. In Processing
  2. Excerpt from D’Arcy Thompson’s on Growth and Form, “The Theory of Transformations.” Each “transformation” can be considered a “function” that maps from one set of coordinates to another.

Assignment

See the associativity assignment here.

Functions

Structuring code is important, especially with regards to your thinking.

So far we’ve redefined functions built into Processing: setup(), draw(), mousePressed(), etc., which are event-driven. They execute when the user does something, or “triggers” the event. These functions handle occurrences that are asynchronous. That is, they don’t happen predictably on the computer’s internal clock cycle.

Customizing Functions

Functions (also called methods) are a structural method of organizing code that is executed as a set.

In general, a function, or sometimes “subroutine”, is a set of instructions defined under a single name. Functions are intended to be executed many times under different conditions. They are used for organization, clarity of expression, and convenience, i.e. you don’t have to write all the instructions over and over when you need them.

Analogy

Functions are analogous to typed commands in AutoCAD or Rhino, and functions in mathematics, where numbers go in and numbers come out (e.g. y=f(x) ).

setup() and draw()

We have actually already been defining functions the entire time. setup() and draw() are special functions that are empty by default. In order to write a sketch, we have to give those functions a set of instructions to refer to, between the braces: { … }.

Functions as Types

In the computational world, a “function” is just another type of data, but its contents aren’t an integer or floating point number, but a set of instructions. Some languages allow us to use functions in even more flexible ways.

When using our own customized functions, we have to go through a similar process as with variables to declare and initialize them, although we use different terminology. Instead of declaring, initializing, and assigning, we “define” functions and “call” them.

But it’s still about giving something a name so we can control it.

Defining Functions


void setup(){
  size(700,400);
}

void draw(){
  background( 255 );
  drawCircleInSquare();
}

void drawCircleInSquare(){
  rect(mouseX-25,mouseY-25,50,50);
  ellipse(mouseX,mouseY,50,50);
}

Here, we “define” a function by using a familiar syntax, just like setup() and draw().

In this case, we have defined a function called drawCircleInSquare, which contains two lines of code. It simply does what the name implies, drawing a circle centered on a square, centered on the mouse.

We “call” our function like calling any other function in Processing, by writing the name, a set of parentheses, and a semicolon.

Functions as Control Mechanisms

Again, using a function is about setting aside a set of instructions under a single name. But once we start considering how Processing executes the code, we can see that when Processing reaches the line where we ask it to call our function, Processing “jumps” to the definition of our function’s code and starts executing the lines inside that code block.

When Processing reaches the end of the function, it returns to where the function was originally called, and continues along its way.

Adding Parameters (Arguments) to Functions

The syntax for declaring a function with parameters follows the same notations as in the API.

Functions are parameterizable just as those that come with Processing.

Functions like line() accept parameters (also called “arguments”). You have to tell the line() function where the endpoints of the line are in terms of coordinate values. Technically speaking, you “passed” the function four values.

If you want to pass parameters to your own functions, you have to define their types and the names of the variables to use to represent those values in the interior of the function.


void setup(){
  size(700, 400);
  noStroke();
  fill(0);
}

void draw(){
  background(255);
  drawPulsatingCircle( 100, 100 );
  drawPulsatingCircle( 500, 200 );  // Not drawn.
  drawPulsatingCircle( mouseX, mouseY );
}

void drawPulsatingCircle( float x, float y ){
  float radius = sin( radians(frameCount) )*100;
  
  // x, y, and radius are floats that are
  // only valid inside this code block.

  ellipse( x, y, radius, radius );
}

// x, y, and radius are not "defined" here.

Returning a Function Early

The keyword “return” aborts a function early, and “returns” to the place where it was called.


void setup(){
  size(700, 400);
  noStroke();
  fill(0);
}

void draw(){
  background(255);
  drawPulsatingCircle( 100, 100 );
  drawPulsatingCircle( 500, 200 );  // Not drawn.
  drawPulsatingCircle( mouseX, mouseY );
}

void drawPulsatingCircle( float x, float y ){
  // x, y, and radius are floats that are
  // only valid inside this code block.

  float radius = sin( radians(frameCount) )*100;
  
  /* 
    Doesn't draw a circle if it's on the right
    side of the sketch. Just demonstrates how
    to use 'return' inside a function.
  */
  if( x > width/2 ) return;
  
  ellipse( x, y, radius, radius );
}

// x, y, and radius have no meaning here.

Functions That Return Values

You’re just seeing the internal mechanism behind Processing’s own functions.

Functions can perform any set of operations, calculating, drawing, etc.

Functions can also manipulate data. These functions are like math functions, but you don’t have do deal just with numbers, but all kinds of data (Strings, objects, etc.).

Returning a value is just a matter of using the “return” keyword. Giving it a value or variable returns that variable to the calling function.

‘void’ means the function doesn’t return a value, i.e. it doesn’t compute something and then give you the answer. It just does something.


int x = 100;

void setup(){
  size(700,400);
}

void draw(){
  background(255);
  
  stroke(0);
  line( mouseX, 0, mouseX, height );
  ellipse( mouseX, mouseY, 10, 10 );
  
  float avg = average( x, mouseX );
  
  line( x, 0, x, height );
  
  stroke(255,0,0);
  line( avg, 0, avg, height );
}

float average( float a, float b ){
  return (a+b)/2.0;
}

void mousePressed(){
  x = mouseX;
}

Note on Variable Scope

Variables are only good within the code block in which they are defined. In Processing, so-called “global” variables are those declared outside setup(), draw(), or any other function. They are available and valid variables for any function, in any code block, etc.

But those variables declared as function parameters or within functions are only valid in that code block and all contained code blocks. This is their “scope.”


float globalVariable = 10;

void setup(){
  size(700,400);
  int variableValidInSetupOnly = 5;
  println( variableValidInSetupOnly + globalVariable );
}

void draw(){
  background( 255 );
  globalVariable = myFunc( mouseX, mouseY );
  println( globalVariable );
}

float myFunc( int x, int y ){
  float sum = x + y;
  sum += globalVariable;
  return sum;
}

// sum is not valid here.

Functions as Associations

A function like in the previous example, that calculates a value, can be used as an associative method. It can define a constraint between geometries, such as found in those in Revit or especially Grasshopper.

Just by redefining the code a little bit, we can turn a function that calculates the average between two numbers and turn it into a midpoint constraint:


int x = 100;
int y = 100;

void setup(){
  size(700,400);
}

void draw(){
  background(255);
  
  stroke(0);
  line( x, y, mouseX, mouseY );
  ellipse( mouseX, mouseY, 10, 10 );
  
  float midX = average( x, mouseX );
  float midY = average( y, mouseY );
  
  // The geometries below "depend" on midX and midY.
  stroke(255,0,0);
  ellipse( midX, midY, 10, 10 );
  line( 100, height-100, midX, midY );
}

float average( float a, float b ){
  return (a+b)/2.0;
}

void mousePressed(){
  x = mouseX;
  y = mouseY;
}

Associative Geometry

This is our first example of associative geometry, something that Grasshopper is very good at. Using Processing and functions, we can begin to mimic the structure implied by a Grasshopper diagram, which simply represents the scripts that Grasshopper will execute.

Associative Geometry How-To

The technique involves calculating values that associate one set of geometries to another. We go from desired geometry to data, then to properties (which we calculate from data), then back to geometry.


int x = 100;
int y = 100;

void setup(){
  size( 700, 400 );
  smooth();
}

void draw(){
  background(255);
  
  // Original geometry, a line.
  stroke(0);
  line(x, y, mouseX, mouseY);
  
  // Calculate something from the data that describes the line,
  // namely, the coordinates of the endpoints.
  float t = 0.5*sin(radians(frameCount))+0.5;
  float x1 = linePoint( x, mouseX, t );
  float y1 = linePoint( y, mouseY, t );
  
  // Draw something from the new "properties", or calculated values.
  stroke(255,0,0);
  ellipse( x1, y1, 10, 10 );
}

float linePoint( float x1, float x2, float t){
  float dx = x2 - x1;
  t = constrain(t,0.0,1.0);
  return t*dx + x1;
}

Bezier and Curve Associations

Some of Processing’s functions enable us to parameterize geometries to a finer degree than just the regular function definitions. The bezier() and curve() functions have corollary functions that let us find any point along the length of them, just like functions in Rhino and Grasshopper.

Dependencies

It’s very important to understand how the data “flows” through our sketches. The dependencies of data values on others are analogous to relationships in a parametric design.