Transformations and Functions
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:
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()
:
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.
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:
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.
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()
:
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:
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.
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:
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:
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:
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:
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:
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:
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:
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:
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:
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.
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
- 2D Transformations from the
official Processing tutorials. (Note that this tutorial is for Java
Processing, not p5.js. Java Processing uses the command
pushMatrix()
instead ofpush()
, andpopMatrix()
instead ofpop()
.) - p5.js reference for push()
- Function definitions in the Mozilla Javascript reference