Transformations and Functions

← back to Creative Computing

by Allison Parrish

In this tutorial, I’m going to show you how to use transformations. This is a handy and elegant technique for changing where and how shapes get drawn to the screen. I’m also going to show you the basics of how to write functions, which is an easy way to compartmentalize and simplify your code.

Translation

In previous examples, I’ve used the concept of an “offset” variable to change where on the screen a shape gets drawn. For example, here’s a sketch that displays a rudimentary face, whose position you can change by adjusting the xoffset and yoffset variables:

► run sketch ◼ stop sketch
let xoffset = 40;
let yoffset = 50;
function setup() {
  createCanvas(400, 400);
  noLoop();
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();
  // draw a face!
  ellipse(xoffset, yoffset, 40, 40);
  ellipse(xoffset+100, yoffset, 40, 40);
  arc(xoffset+50, yoffset+50, 100, 50, 0, PI);
}

This is fine, but there seems to be a lot of repetition: xoffset and yoffset are written several times.

You are familiar by this point with how much programmers despise having to type the same thing over and over, and in this case as with many other cases, programmers have devised a method for “factoring out” this repetition: the translate() function.

Before I go into the details of how translate() works, it might be helpful to see it in action. Here’s the same example as above, rewritten using translate():

► run sketch ◼ stop sketch
let xoffset = 40;
let yoffset = 50;
function setup() {
  createCanvas(400, 400);
  noLoop();
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();
  translate(xoffset, yoffset);
  // draw a face!
  ellipse(0, 0, 40, 40);
  ellipse(100, 0, 40, 40);
  arc(50, 50, 100, 50, 0, PI);
}

So what’s going on here? Briefly: the translate() function changes the “origin” point of the coordinate system. For any drawing functions you call after the call to translate(), the position of 0, 0 on the screen will be at whatever coordinates you specified in the call to translate().

The main benefit of the translate() command is that when you use it, the code that you write to draw shapes to the screen become effortlessly independent of position. You could copy and paste that bit of code that draws a shape into any other sketch, and as long as you used translate() to change the position beforehand, you wouldn’t have to change any of the code.

Here’s a version of the sketch above where the face follows the mouse. To make this work, I didn’t have to go in and add mouseX and mouseY to every drawing function; I just needed to change the translate() function.

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();
  translate(mouseX, mouseY);
  // draw a face!
  ellipse(0, 0, 40, 40);
  ellipse(100, 0, 40, 40);
  arc(50, 50, 100, 50, 0, PI);
}

Translates accumulate

Keeping that in mind, you might think: “Okay, let’s draw TWO faces! One that follows the mouse and one that stays still in the upper left-hand corner of the screen. I got this!” Your first attempt might look something like this:

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();

  // follow the mouse
  translate(mouseX, mouseY);
  // draw a face!
  ellipse(0, 0, 40, 40);
  ellipse(100, 0, 40, 40);
  arc(50, 50, 100, 50, 0, PI);

  // upper left-hand corner???
  translate(50, 50);
  // draw a face!
  ellipse(0, 0, 40, 40);
  ellipse(100, 0, 40, 40);
  arc(50, 50, 100, 50, 0, PI);
}

Wait whaaaaat. This is so weird. They’re both following the mouse. What gives?

Here’s the deal: transformations accumulate. The first call to translate() in the example above moves the origin to the position of the mouse. The second call doesn’t reset the origin to 50, 50, but instead sets the origin to 50, 50 relative to what the origin was already set to.

Pushing and popping the matrix

Despite the way that I framed that last example, the fact that transformations accumulate is usually a helpful behavior. (To see why this is the case, imagine trying to write the previous example on purpose, i.e., drawing two faces that follow the mouse, slightly offset from one another, in a world where the origin reset itself after each call to translate().)

But it is useful to be able to isolate transformations, so that part of the code will have its origin at one point on the screen, and another part of the code will have its origin elsewhere. The easiest means of doing this is with two functions: push() and pop().

The push() function says, “Hey, Processing. All of the calls to translate() from here on? Remember them, because I’m going to want to undo them later.” The pop() function says, “Hey, Processing. Remember how I asked you to keep track of those translations? Please undo them now. thx bye.”

To demonstrate, here’s the example from the section above, rewritten so that it displays as we originally wanted it to be displayed: with one face following the mouse, and another face always in the upper left-hand corner.

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();

  // follow the mouse
  push();
  translate(mouseX, mouseY);
  // draw a face!
  ellipse(0, 0, 40, 40);
  ellipse(100, 0, 40, 40);
  arc(50, 50, 100, 50, 0, PI);
  pop();

  // upper left-hand corner
  translate(50, 50);
  // draw a face!
  ellipse(0, 0, 40, 40);
  ellipse(100, 0, 40, 40);
  arc(50, 50, 100, 50, 0, PI);
}

Those two look like they’re having a lot of fun!

A function of one’s own

The most astute and laziest among us may have noted that the above example has some pretty egregious repetition: the code to draw the face occurs twice, verbatim, inside of draw(). It would be nice to be able to, somehow, factor out that repetition: to write that code just one time somewhere in our source code, give it a name, and then tell Javascript to run that code whenever we type that name, instead of having to copy and paste it every time we want to run the code. Right?

Right! Javascript provides a handy way of doing just this: you can define a function. A function is just a collection of statements that you’re giving a name to, so that you don’t have to type them over and over again.

We’ve been working a lot with functions so far, but up to this point you’ve only ever called functions that p5.js (or Javascript) provide you with. But it’s easy enough to define our own functions. The syntax looks like this:

function name_of_function() {
  statements
}

… where you need to replace name_of_function with the name that you want to give the function, and statements with whatever code you want to be in the function: this can be function calls, for loops, if statements, whatever you want.

NOTE: Function names need to obey the same rules as variable names. (In fact, behind the scenes, a function is just another kind of value that you can assign to a variable.)

When you include this code in your sketch, it’s called a “function definition.” You can put your function definitions anywhere in your source code. Personally, I prefer to put them at the bottom of the source code, after setup() and draw(). To “run” a function (i.e., to tell Javascript to execute the code inside of the function), type the name of the function followed by parentheses (())

Here’s a version of the above example that defines a function makeFace() with the code to draw a face, and then includes calls to that function inside of draw():

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();

  // follow the mouse
  push();
  translate(mouseX, mouseY);
  makeFace();
  pop();

  // upper left-hand corner
  translate(50, 50);
  makeFace();
}

function makeFace() {
  // draw a face!
  ellipse(0, 0, 40, 40);
  ellipse(100, 0, 40, 40);
  arc(50, 50, 100, 50, 0, PI);
}

But that’s not all! Now that our face-drawing code is a function, we can just call that function whenever we want, however we want. Here’s twenty damn faces drawn all over each other:

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
  noLoop();
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();

  for (let i = 0; i < 20; i++) {
    push();
    translate(width/2, i*20);
    makeFace();
    pop();
  }
}

function makeFace() {
  // draw a face!
  ellipse(0, 0, 40, 40);
  ellipse(100, 0, 40, 40);
  arc(50, 50, 100, 50, 0, PI);
}

Functions reduce the amount of repetition in your source code, but they also give you an opportunity to break your code up into logical units. The idea is that it’s easier to read a for loop like the one in the above example if there’s just a call to makeFace() in there: one quick glance at the code tells you what the loop does. It’s also conceptually easier for you to reuse code from one sketch to the next: all you need to do is copy and paste your function definitions (instead of having to hunt around in your draw() for the relevant bits).

Scale

The translate() function isn’t the only transformation that you can apply to your drawing commands. Another transformation is scale(), which changes the size of what gets drawn. While translate() controls where the origin of the coordinate system is, scale() controls the distance between each unit in the coordinate system.

Here’s a simple demonstration based on the previous example, which calls the makeFace() function three times: once at half scale, once at normal scale, and once at double scale.

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
  noLoop();
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();

  // small
  push();
  translate(30, 20);
  scale(0.5);
  makeFace();
  pop();

  // regular
  push();
  translate(30, 100);
  scale(1); // 1 is the default
  makeFace();
  pop();

  // large
  push();
  translate(30, 250);
  scale(2);
  makeFace();
  pop();

}

function makeFace() {
  // draw a face!
  ellipse(0, 0, 40, 40);
  ellipse(100, 0, 40, 40);
  arc(50, 50, 100, 50, 0, PI);
}

Here’s another example that draws two faces that follow the mouse and oscillate in size, again using the scale() transformation:

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();

  // left
  push();
  translate(mouseX - 50, mouseY);
  scale(0.5+sin(frameCount*0.1)*0.25);
  makeFace();
  pop();

  // right
  push();
  translate(mouseX + 50, mouseY);
  scale(0.5+cos(frameCount*0.05)*0.25);
  makeFace();
  pop();

}

function makeFace() {
  // draw a face!
  ellipse(0, 0, 40, 40);
  ellipse(100, 0, 40, 40);
  arc(50, 50, 100, 50, 0, PI);
}

And an example that draws fifty faces to the screen, all with random sizes and stroke colors:

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
  noLoop();
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();

  for (let i = 0; i < 50; i++) {
    push();
    stroke(random(255));
    translate(random(width), random(height));
    scale(random(1.5));
    makeFace();
    pop();
  }
}

function makeFace() {
  // draw a face!
  ellipse(0, 0, 40, 40);
  ellipse(100, 0, 40, 40);
  arc(50, 50, 100, 50, 0, PI);
}

The scale() function can scale the X and Y dimensions separately; to do so, include a second parameter in the function call. The first parameter specifies the X scale, and the second parameter specifies the Y scale. This has the effect of “stretching” the image in one or the other direction. To demonstrate, here’s the random faces example, except scaling randomly in both directions:

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
  noLoop();
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();

  for (let i = 0; i < 50; i++) {
    push();
    stroke(random(255));
    translate(random(width), random(height));
    scale(random(1.5), random(1.5));
    makeFace();
    pop();
  }
}

function makeFace() {
  // draw a face!
  ellipse(0, 0, 40, 40);
  ellipse(100, 0, 40, 40);
  arc(50, 50, 100, 50, 0, PI);
}

I swear I didn’t mean to make these examples so terrifying.

Rotation

The final transformation we’ll talk about is rotation. The rotate() function changes the orientation of the coordinate system. This is perhaps easiest to visualize with an example:

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
  noLoop();
}
function draw() {
  background(50);
  stroke(255);

  for (let i = 0; i <= 10; i++) {
    push();
    strokeWeight(i+1);
    rotate((PI/2) * (0.1 * i));
    line(0, 0, 350, 0);
    pop();
  }
}

As you can see, in each iteration of the for loop, the call to line() is exactly the same. The only difference is the rotation. At zero rotation, a line from (0, 0) appears to go from the origin to 350 pixels to the left. Rotating the coordinate system causes the origin to stay put, but the location of (350, 0) to be rotated clockwise.

The rotate() command interprets its parameter as a value in radians, meaning that the value of pi (~3.14) rotates halfway around the circle, and the value of two times pi (~6.28) rotates all the way. (Anything greater than that, and the rotation appears to start over again.)

Here’s an example that calls the makeFace() function and rotates the results using the mouse position:

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();

  push();
  rotate((mouseX/width)*(PI/2));
  makeFace();
  pop();
}

function makeFace() {
  // draw a face!
  ellipse(0, 0, 40, 40);
  ellipse(100, 0, 40, 40);
  arc(50, 50, 100, 50, 0, PI);
}

Rotation and translation

It’s kind of anticlimactic to have the face drawn there in the corner of the screen. How about we draw the face in the middle of the screen by calling translate() too? Here’s an attempt at doing so:

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();

  push();
  rotate((mouseX/width)*(PI/2));
  translate(150, 150);
  makeFace();
  pop();
}

function makeFace() {
  // draw a face!
  ellipse(0, 0, 40, 40);
  ellipse(100, 0, 40, 40);
  arc(50, 50, 100, 50, 0, PI);
}

… which is okay I guess but not quite right. What’s happening here is that we’re calling rotate() and then translate(), which means that the orientation of the coordinate system is changing first, and then the translation is happening inside the rotated coordinates. So the call to translate() isn’t moving 150 pixels right, then 150 pixels down; it’s moving 150 pixels along the orientation of the coordinate system, and then 150 pixels perpendicular to that.

If we do the translation first (so it happens in the default orientation system) and the rotation second, the result looks closer to what we want:

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();

  push();
  translate(width/2, height/2);
  rotate((mouseX/width)*(PI/2));
  makeFace();
  pop();
}

function makeFace() {
  // draw a face!
  ellipse(0, 0, 40, 40);
  ellipse(100, 0, 40, 40);
  arc(50, 50, 100, 50, 0, PI);
}

These two examples demonstrate the fact that because transformations accumulate, the order of transformations matters. Each transformation affects the coordinate system in a particular way, and subsequent transformations apply to the already-transformed coordinate system.

All of these changes can be difficult to visualize and reason about, so it’s worth your time to play around with the transformation functions and their orders to get a feel for how they work. A rule of thumb that usually works for simple cases: translate first (to move the origin to the desired spot), rotate second (to change the orientation of how the shape gets drawn) and scale last (to change the size of the units).

Designing around the origin

A common difficulty that beginner programmers face is this: they’ve written all of their drawing code so that the instructions for drawing figures are relative to (0, 0), and all of the shapes are drawn with that coordinate in the upper left-hand corner. Then those programmers are surprised when sketches like the example above behave strangely: intuitively, when we say “rotate this shape,” what we usually have in mind is “rotate this shape around its center.” But because the code has the origin (0, 0) assumed to be in its upper left-hand corner, calling rotate() will cause the shape to rotate around its upper left-hand corner.

In the case of the sketch above, the easiest way to fix this is to simply re-write the function so that the center of the face is at (0, 0). To do this, we’ll have to do something a bit weird and use negative numbers for some of the coordinates. Here’s a new version of the sketch above with an implementation of these changes:

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();

  push();
  translate(width/2, height/2);
  rotate((mouseX/width)*2*PI);
  makeFace();
  pop();
}

function makeFace() {
  // draw a face, centered on the origin
  ellipse(-50, -25, 40, 40);
  ellipse(50, -25, 40, 40);
  arc(0, 25, 100, 50, 0, PI);
}

The center of the rectangle

You may have noticed that some built-in Processing drawing functions are oriented around the upper left-hand corner of the shape. The rect() function, for example, uses the first two parameters by default as the X and Y positions of the upper left-hand corner of the rectangle, making rotation around the center of the rectangle kind of a pain. Fortunately, Processing supplies a function, rectMode(), which allows you to specify that you want those two parameters to be used for the center of the rectangle instead of the upper left-hand corner. Here’s an example that demonstrates the difference:

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();

  if (mouseIsPressed) {
    rectMode(CENTER);
  }
  else {
    rectMode(CORNER); // default
  }

  push();
  translate(width/2, height/2);
  rotate((mouseX/width)*2*PI);
  rect(0, 0, 300, 125);
  pop();
}

As you can see, calling rectMode(CENTER) makes Processing draw the rectangle with its center at the given coordinates, whereas rectMode(CORNER) (the default behavior) makes Processing draw the rectangle with its upper left-hand corner at the given coordinates.

Creepy denoument

In this example, we dozens of faces, all with random positions, sizes and rotations.

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
  noLoop();
}
function draw() {
  background(50);
  stroke(255);
  strokeWeight(8);
  noFill();
  for (let i = 0; i < 50; i++) {
    push();
    stroke(random(255));
    translate(random(width), random(height));
    rotate(random(2*PI));
    scale(random(1.5), random(1.5));
    makeFace();
    pop();
  }
}

function makeFace() {
  // draw a face, centered on the origin
  ellipse(-50, -25, 40, 40);
  ellipse(50, -25, 40, 40);
  arc(0, 25, 100, 50, 0, PI);
}

Further reading