Course Home | Documentation | Lab Hours/Tutoring | Projects | Quizzes | Schedule | Submit

Saint Louis University

Computer Science 1050
Introduction to Computer Science: Multimedia

Michael Goldwasser

Spring 2016

Dept. of Math & Computer Science

Lecture Notes: User-defined Functions


Motivation

From the beginning of this course, we have been relying on calls to functions that are defined as part of Processing, such as background, rect, and fill. Note well that different functions require different sets of parameters to be sent by the caller. For example, noFill() does not require any parameters while rect(x,y,w,h) expects four parameters to be sent to respectively describe the coordinates of the corner and the width and height

Processing also provides a mechanism for programmers to define new functions which can then be used in their sketches. These user-defined functions serve two important purposes:


Example

Consider the following sketch displaying a target that allows the user to add circles with mouse presses:

We might implement such a sketch as follows:
void setup() {
  size(400, 400);
  background(0);
  fill(255, 0, 0);
  ellipse(width/2, height/2, 300, 300);
  fill(255);
  ellipse(width/2, height/2, 200, 200);
  fill(255, 0, 0);
  ellipse(width/2, height/2, 100, 100);
  fill(0, 255, 0);  // green
  noStroke();
}

void draw() {
}

void mousePressed() {
  ellipse(mouseX, mouseY, 10, 10);
}

Now consider that we wish to add functionality where the user can clear the target by pressing any key. To "clear" the target, we will effectively have to redraw the entire seen from scratch. We could do this within the keyPressed() function, as follows:

void setup() {
  size(400, 400);
  background(0);
  fill(255, 0, 0);
  ellipse(width/2, height/2, 300, 300);
  fill(255);
  ellipse(width/2, height/2, 200, 200);
  fill(255, 0, 0);
  ellipse(width/2, height/2, 100, 100);
  fill(0, 255, 0);  // green
  noStroke();
}

void draw() {
}

void mousePressed() {
  ellipse(mouseX, mouseY, 10, 10);
}

void keyPressed() {
  background(0);
  fill(255, 0, 0);
  ellipse(width/2, height/2, 300, 300);
  fill(255);
  ellipse(width/2, height/2, 200, 200);
  fill(255, 0, 0);
  ellipse(width/2, height/2, 100, 100);
  fill(0, 255, 0);  // green
  noStroke();
}

However, this is a very poor design to have effectively copies such a significant portion of the source code into a second place. It is wasteful, and if we ever decided we wanted to change some aspect of the look (such as the color or number of rings, or color of the subsequent marks), we would have to make such changes in both setup and keyPressed.

A User-Defined Function
The better design is to recognize that there is a repeated task in our script and instead to define those behaviors in a user-defined function, perhaps named something meaningful such as reset. Such a program would appear as follows:

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

void draw() {
}

void mousePressed() {
  ellipse(mouseX, mouseY, 10, 10);
}

void keyPressed() {
  reset();
}

// our new function
void reset() {
  background(0);
  fill(255, 0, 0);
  ellipse(width/2, height/2, 300, 300);
  fill(255);
  ellipse(width/2, height/2, 200, 200);
  fill(255, 0, 0);
  ellipse(width/2, height/2, 100, 100);
  fill(0, 255, 0);  // green
  noStroke();
}

Download the .pde file


An Example with Parameters

Consider the following image.

This image could be produced by the explicit set of lines and circles indicated as follows:

line(35, 150, 50, 120);
line(65, 150, 50, 120);
line(50, 120, 50, 90);
line(50, 105, 35, 90);
line(50, 105, 65, 90);
ellipse(50, 75, 30, 30);

line(90, 180, 100, 160);
line(110, 180, 100, 160);
line(100, 160, 100, 140);
line(100, 150, 90, 140);
line(100, 150, 110, 140);
ellipse(100, 130, 20, 20);

line(135, 150, 150, 120);
line(165, 150, 150, 120);
line(150, 120, 150, 90);
line(150, 105, 135, 90);
line(150, 105, 165, 90);
ellipse(150, 75, 30, 30);

However, that is a very naïve way to describe what is happening, and it would not allow us to easily make adjustments such as moving the locations of the three people, or in easily introducing additional people into such a diagram. There are clearly some repetitive actions happening, but not such an obvious reptition that is easily expressed with a loop. While each person consists of five lines and an ellipse, the parameters that are used to describe the geometry differ in various ways. What is happening is that each of the three people is being drawn at a particular location and with a particular scale. We prefer to organize our code around such an abstraction.

We consider the task of drawing a single person. As a point of reference, we will consider the point (x,y) that is centered directly beneath the person, and a width w that defines the separation between the persons feet. We can then base other measurements relative to that same width, viewing the geometry of a person as follows:

Now we can define our own function to draw such a person as follows:

void person(int x, int y, int w) {
  line(x-w/2, y, x, y-w);          // left leg
  line(x+w/2, y, x, y-w);          // right leg
  line(x, y-w, x, y-2*w);          // trunk
  line(x, y-1.5*w, x-w/2, y-2*w);  // left arm
  line(x, y-1.5*w, x+w/2, y-2*w);  // right arm
  ellipse(x, y-2.5*w, w, w);       // head
}
Notice the declaration of expected integer parameters x, y, and w. A caller must supply those values each time the person() function is called. With this function at our disposal, our original drawing of three people can be created as:
void setup() {
  size(200,200);
  background(255);
  person(50, 150, 30);
  person(100, 180, 20);
  person(150, 150, 30);
}
Download the complete .pde file

Additional discussion

It is worth noting that the implementation of the person() function given above does not explicitly set the stroke weight, stroke color, or fill color. In the above example, they were the default of black stroke with weight 1, and a white fill for the ellipse. However, if those properties are set differently before a given call to the function, then this will affect the drawing that takes place within the function. For example, we can show variety by varying our above setup function as follows:

void setup() {
  size(200,200);
  background(255);
  strokeWeight(3);
  person(50, 150, 30);
  stroke(255, 0, 0);
  person(100, 180, 20);
  fill(0, 255, 0);
  person(150, 150, 30);
}

This results in the following image. Note well that the increased stroke weight was in effect for all three calls to the function, the red stroke color was in effect for the second and third call, and the green fill color was in effect for only the third call.

Having this ability for the caller to customize the drawing properties prior to a call to the person() function provides greater flexibility but if not a well understood expectation of the behavior of the function, this could easily cause some confusion. A different design for such a function could be to explicitly force an appearance with black stroke color and white fill, and perhaps even a fixed stroke weight (or perhaps the stroke weight could be left at the discretion of the caller). One such implementation for that function would be:

void person(int x, int y, int w) {
  stroke(0);        // black lines
  strokeWeight(1);  // thin lines
  fill(255);        // white fill for head

  line(x-w/2, y, x, y-w);          // left leg
  line(x+w/2, y, x, y-w);          // right leg
  line(x, y-w, x, y-2*w);          // trunk
  line(x, y-1.5*w, x-w/2, y-2*w);  // left arm
  line(x, y-1.5*w, x+w/2, y-2*w);  // right arm
  ellipse(x, y-2.5*w, w, w);       // head
}

However it is worth noting that such a function will have the side effect of changing those drawing properties in the caller's context, and this might be unexpected by a programmer using the function. For example, if executing the following

fill(255, 0, 0);
rect(25,25,25,25);
person(50, 100, 30);
rect(125,25,25,25);
the first rectangle will be drawn filled with red but the second rectangle will be drawn filled with white (because the fill color was changed during the call to person).

A more advanced version of such a function could make use of Processing's pushStyle() and popStyle() functions. Essentially, a call to pushStyle() saves a copy of all style settings currently in effect and then you can later revert back to that style with a call to popStyle(). Using those for our person() implementation would appear as

void person(int x, int y, int w) {
  pushStyle():      // SAVE all style settings in effect before this call

  stroke(0);        // black lines
  strokeWeight(1);  // thin lines
  fill(255);        // white fill for head

  line(x-w/2, y, x, y-w);          // left leg
  line(x+w/2, y, x, y-w);          // right leg
  line(x, y-w, x, y-2*w);          // trunk
  line(x, y-1.5*w, x-w/2, y-2*w);  // left arm
  line(x, y-1.5*w, x+w/2, y-2*w);  // right arm
  ellipse(x, y-2.5*w, w, w);       // head

  popStyle():      // revert back to the settings that had been in effect before this call
}


An Example with a Return Value

The formal syntax for a function definition is as follows:
returnType functionName(parameters) {
    functionBody
}
Up until now, all of our examples thus far have used the keyword void as the return type. For example, we have defined function such as:
void setup() {
  // ...
}

void draw() {
  // ...
}

void reset() {
  // ...
}

void person(int x, int y, int w) {
  // ...
}
When any of these functions are called, important actions take place, but there is no information passed back to the caller of the function.

However, functions can sometimes be used not to immediately cause any outward drawing effects, but instead to perform internal computations and to return some calculated information to the caller. We have already seen a few such functions that are built-in to Processing. For example, the dist() function is a useful one that computes the distance between two points. Its formal signature has the form:

float dist(float x1, float y1, float x2, float y2) {
  // ...
}
The caller is expected to send four floating-point numbers as parameters, and the result of the function is a single floating-point number that represents the distance between points (x1,y1) and (x2,y2).

We can similarly create our own user-defined functions that return information. This can serve similar purposes that we have seen, either to better abstract away some portions of our code or to take advantage of the need for some forms of repetitiveness.

As a motivating example, we revisit our "hot corner" example from a previous module. In that setting, we had defined a virtual 20x20 corner of the screen and we specifically wanted to detect a situation in which the current mouse position was within the corner yet the previous mouse position was not within the corner. We implemented that rule with a rather complex and subtle compound boolean expression, stated as:

  if (mouseX <= 20 && mouseY >= height-20 && (pmouseX > 20 || pmouseY < height-20)) {
While this works, the parenthesization is key and it would have been easy to have misstated that logic. A far more intuitive way to think about the logic would be to have a concept of a position being "hot" and then to write that condition as
  if (isHot(mouseX,mouseY) && !isHot(pmouseX,pmouseY)) {
Note well the use of the exclamation point which is a logical negation operator, to express our desired that the previous mouse position not be hot.

Of course, there is no built-in definition of such an isHot() function, but we could define one for our needs. This is a function that should accept an x and y coordinate as parameters and which should return a boolean value as its return value (with true designating that the point is in the hot corner, and false designating that the point is not in the hot corner. Such a function definition might appear as follows:

// we consider a position "hot" if it is close enough to the bottom-left corner
boolean isHot(int x, int y) {
  return (x <= 20 && y >= height-20);
}
You may download the complete .pde file which implements the hot-corner task using such an approach and which further defines the value 20 as a global variable, to make it easier to change.


Michael Goldwasser
CSCI 1050, Spring 2016
Last modified: Tuesday, 16 February 2016
Course Home | Documentation | Lab Hours/Tutoring | Projects | Quizzes | Schedule | Submit