Jan 22: Functions; transformations
Our own functions
Once we start getting a complex sketch, there might be a lot of things going on in the body of the draw(). We can use comments to delineate things, but there's a better way: defining our own functions. Just like using variables with names to replace magic numbers, it helps us see what's going on if we package up a chunk of commands into a function. The following sketch illustrates.
void draw() { fadeAway(); drawRandomEllipse(); } // Paint a transparent black rectangle over window void fadeAway() { noStroke(); fill(0,5); rect(0,0,width,height); } // Draw an ellipse of random color and size at random position void drawRandomEllipse() { noStroke(); fill(random(255),random(255),random(255)); ellipse(random(width),random(height),random(20),random(20)); }
[applet]
Defining our own function works much the same way as filling in setup() and draw(), but we choose our own name (same rules as with variables. Now a glance at the draw() body now makes very clear what's going on in each frame. Furthermore, we've now got a handy fadeAway() function that we can easily plug into other sketches.
Drawing a random ellipse is a pretty simple thing anyway, but imagine instead that we wanted to package up all the work of drawing some complex object. The snowperson is a bit more than I want to do here, so let's stick with the boxy "C" shape from last week. We could define a function drawC(), analogous to drawRandomEllipse():
void drawC() { strokeWeight(5); fill(150,150,255); beginShape(); vertex(0,0); vertex(100,0); vertex(100,20); vertex(20,20); vertex(20,80); vertex(100,80); vertex(100,100); vertex(0,100); endShape(CLOSE); }
But what we really want to be able to do is to put the shape in different places and at different sizes (a monogrammer?), as we can with ellipse(). We can do just that by defining our own function that takes parameters telling us where to draw it and how big to draw it.
int sz = 25; // how big to draw void setup() { noStroke(); smooth(); } void draw() { // Nothing to do here (but need the blank function anyway) } void mousePressed() { fill(random(255),random(255),random(255)); drawC(mouseX, mouseY, sz); } void keyPressed() { if (key=='Z') { // Bigger if (sz<50) sz+=5; } else if (key=='z') { if (sz > 5) sz-=5; } } // Draw a letter "C" of size s, with the upper left at (x,y). void drawC(float x, float y, int s) { beginShape(); vertex(x, y); vertex(x+s, y); vertex(x+s, y+0.2*s); vertex(x+0.2*s, y+0.2*s); vertex(x+0.2*s, y+0.8*s); vertex(x+s, y+0.8*s); vertex(x+s, y+s); vertex(x, y+s); endShape(CLOSE); }
[applet]
Now we specify not just the name of our function, but also its parameters (names and types, like variable declarations). We can use the parameters within the body of the function, just like variables. We then call our function the same way we call built-in Processing functions. Details on function definition and parameters are in the Programming notes section.
In defining our drawC function, to draw the shape at a specified position, we simply use the variables x and y instead of 0 and 0 in the original sketch. To be able to draw in different sizes, we treat the coordinates in the original sketch as percentages, so that 100 is 1.0 of the size, 80 is 0.8 of the size, and so forth.
Shiffman, p. 109 (unfortunately not made into a downloadable example), shows a similar idea for drawing cars. One note is the use of a local variable named "offset". This variable is declared in the drawcar function, and therefore can only be used there. Local variables are a good way to temporarily keep track of something that will be used within the function, but nowhere else. (You can also use local variables in setup() and draw(); they're just functions with particular names.)
Another note is that Shiffman uses the function color() to package up red, green, and blue values into a single color value. This value is of a type called color (same name as the function). So instead of passing in three numbers, for red, green, and blue, he creates and passes a single color.
When we want to package up a calculation, rather than a set of drawing commands, we define a function that returns a value. For example, we could define a function that tells us whether or not the mouse is inside a rectangle, and use it in mouse-over and mouse press tests.
boolean buttonOn = false; void draw() { if (buttonOn) // On, so make it red fill(255,0,0); else if (inRectangle(25,40,50,20)) // Inside rectangle, so make it yellow fill(255,255,0); else // Outside rectangle, so make it green fill(0,255,0); rect(25,40,50,20); } void mousePressed() { if (inRectangle(25,40,50,20)) // Button inside rectangle, so invert button buttonOn = !buttonOn; } // Returns whether or not the mouse is within the rectangle with corner at (x1,y1) and size w * h boolean inRectangle(float x1, float y1, float w, float h) { return (mouseX>x1 && mouseX<x1+w && mouseY>y1 && mouseY<y1+h); }
[applet]
Shiffman 7-4 shows how to package up a slightly more complicated calculation, to find the distance between two points. Note that he again uses some local variables inside his function to temporarily remember things while the function is being calculated.
Translation and scaling
So far, we've always treated the origin, (0,0), as the upper-left corner, with the x and y axes extending to the right and down, stepping one pixel at a time. In the "C" monogrammer above, we had to do some addition and multiplication for each vertex, to put it at the right place and at the right size. It would be more convenient to just tell Processing to move the origin and change the overall scale, so that we don't have to keep doing that ourselves.
To do just that, Processing provides the functions translate() (given x and y offsets) and scale() (given x and y scaling factors). Give each of these operations a quick whirl, just plotting an ellipse before/after the operation.
We can use this technique to make the "drawC" function look exactly like our original static "C" sketch, without having to do the arithmetic for each vertex. While the arithmetic wasn't hard for the "C" shape, it could be for more complex shapes, and using transformations frees us from having to think about the ultimate position and size.
// Draw a letter "C" of size s, with the upper left at (x,y). void drawC(float x, float y, int s) { pushMatrix(); translate(x,y); scale(s/100.0,s/100.0); beginShape(); vertex(0,0); vertex(100,0); vertex(100,20); vertex(20,20); vertex(20,80); vertex(100,80); vertex(100,100); vertex(0,100); endShape(CLOSE); popMatrix(); }
[applet]
Note that the effects of these functions are cumulative, e.g., translate(10,5); translate(7,3); is the same as translate(17,8);. Thus if we aren't careful, we can lose our bearings. To help us out, Processing provides the pushMatrix() function to "remember" the current coordinate system, and popMatrix() function, to return back to it. We can push multiple coordinate systems, and they are treated in "last in first out" order, just like stacking plates on top of each other.
Orientation
Going one step further, we can even reorient the coordinate system, so that the axes point diagonally across the window, for example. The function rotate() does that, given the angle to rotate (in radians).
Give it a try, drawing a horizontal or vertical line before/after rotation. To rotate around some point other than the upper-left corner, we translate to that point first.
float a=0, da=0.05; // drawing angle and increment void setup() { fill(0,255,0); noStroke(); smooth(); textFont(loadFont("LucidaSans-18.vlw")); } void draw() { background(0); translate(width/2,height/2); rotate(a); text("hello", 0,0); a+=da; }
[applet]
A rotation transformation can be particularly useful when we want to have a sketch react not only to the position of the mouse, but also to the angle. We can use Processing's atan2() function to calculate the angle between a point and the origin. We provide as parameters the y and x coordinates, in that reversed order (recall from trig that the tangent is the opposite over the adjacent).
To compute an orientation with respect to some point other than the origin, we subtract out the coordinates of that point before calling atan2(), and then translate() to that point before calling rotate() with the computed angle.
void setup() { stroke(255); strokeWeight(10); } void draw() { background(0); float dx = mouseX - width/2; float dy = mouseY - height/2; float angle = atan2(dy, dx); pushMatrix(); translate(width/2,height/2); rotate(angle); line(0,0,25,0); popMatrix(); }
[applet]
Now for some freaky eyeballs, from the Processing example Basics | Math | Arctangent. It works exactly the same way, moving the center of the eyeball out along the x axis in the rotated coordinate system. Note the use of a function to package up the drawing of an eye, and the calling of this function with several different values for its parameters.
void setup() { size(200, 200); smooth(); noStroke(); } void draw() { background(50); drawEye( 50, 16, 80); drawEye( 64, 85, 40); drawEye( 90, 200, 120); drawEye(150, 44, 40); drawEye(175, 120, 80); } // Draw an eye of size s at position (x,y) void drawEye(float x, float y, float s) { // Make the eye look at the mouse float angle = atan2(mouseY-y, mouseX-x); pushMatrix(); translate(x, y); fill(255); ellipse(0, 0, s, s); rotate(angle); fill(153); ellipse(s/4, 0, s/2, s/2); popMatrix(); }
[applet]
Programming notes
- Function definition
- A function definition is of the form
The name should follow the same rules and style as for variables (alphanumeric, beginning with a letter, internally capitalized, avoiding Processing words). There need not be any parameters. The first type (the return type) can be "void", meaning that the function just executes statements and doesn't return any value.
type name(type1 paramName1, type2 paramName2, ...) { statements } - Return
- A function can return a value (e.g., like min()). The statement "return value;" stops the function right there, and returns the value to whoever called the function. A function that doesn't return a value can just call "return;" to stop right there.
- Scope
- We've seen several different variable scopes. Recall that global variables, declared before setup, can be used anywhere and hold their values across function calls. Local variables are declared within a function, are only available within the function, and are reinitialized each time. The same holds for function parameters. Note that we can reuse the same variable (or parameter) name in different functions -- they're totally separate.