2D Transformations
This tutorial is for Processing's Python Mode. If you see any errors or have comments, please let us know. This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
Processing has built-in functions that make it easy for you
to have objects in a sketch move, spin, and grow
or shrink. This tutorial will introduce you to the
Translation: Moving the Grid
As you know, your Processing window works like a piece of graph paper. When you
want to draw something, you specify its coordinates on the graph.
Here is a simple rectangle drawn with the code
If you want to move the rectangle 60 units right and 80 units down,
you can just change the coordinates by adding to the x and y
starting point:
But there is a more interesting way to do it: move the graph paper instead. If you move the graph paper 60 units right and 80 units down, you will get exactly the same visual result. Moving the coordinate system is called translation. The important thing to notice in the preceding diagram is that, as far as the rectangle is concerned, it hasn’t moved at all. Its upper left corner is still at (20,20). When you use transformations, the things you draw never change position; the coordinate system does. Here is code that draws the rectangle in red by changing its coordinates, then draws it in blue by moving the grid. The rectangles are translucent so that you can see that they are (visually) at the same place. Only the method used to move them has changed. Copy and paste this code into Processing and give it a try. def setup(): size(200, 200) background(255) noStroke() # draw the original position in gray fill(192) rect(20, 20, 40, 40) # draw a translucent red rectangle by changing the coordinates fill(255, 0, 0, 128) rect(20 + 60, 20 + 80, 40, 40) # draw a translucent blue rectangle by translating the grid fill(0, 0, 255, 128) pushMatrix() translate(60, 80) rect(20, 20, 40, 40) popMatrix()
Let’s look at the translation code in more detail.
Yes, you could have done a What’s the Advantage?
You may be thinking that picking up the coordinate system and moving it
is a lot more trouble than just adding to coordinates. For a simple example
like the rectangle, you are correct. But let’s take an example of
where def setup(): size(400, 100) background(255) for i in xrange(10,350,50): house(i, 20) This is the code for drawing the house by changing its position. Look at all the additions that you have to keep track of. def house(x, y): triangle(x + 15, y, x, y + 15, x + 30, y + 15) rect(x, y + 15, 30, 30) rect(x + 12, y + 30, 10, 15)
Compare that to the version of the function that uses def house(x, y): pushMatrix() translate(x, y) triangle(15, 0, 0, 15, 30, 15) rect(0, 15, 30, 30) rect(12, 30, 10, 15) popMatrix() Rotation
In addition to moving the grid, you can also rotate it with the
Since most people think in degrees, Processing has a built-in def setup(): size(200, 200) background(255) smooth() fill(192) noStroke() rect(40, 40, 40, 40) pushMatrix() rotate(radians(45)) fill(0) rect(40, 40, 40, 40) popMatrix() Hey, what happened? How come the square got moved and cut off? The answer is: the square did not move. The grid was rotated. Here is what really happened. As you can see, on the rotated coordinate system, the square still has its upper left corner at (40, 40). Rotating the Correct WayThe correct way to rotate the square is to:
And here is the code and its result, without the grid marks. def setup(): size(200, 200) background(255) smooth() fill(192) noStroke() rect(40, 40, 40, 40) pushMatrix() # move the origin to the pivot point translate(40, 40) # then pivot the grid rotate(radians(45)) # and draw the square at the origin fill(0) rect(0, 0, 40, 40) popMatrix() And here is a program that generates a wheel of colors by using rotation. The screenshot is reduced to save space. def setup(): size(200, 200) background(255) smooth() noStroke() def draw(): if (frameCount % 10 == 0): fill(frameCount * 3 % 255, frameCount * 5 % 255, frameCount * 7 % 255) pushMatrix() translate(100, 100) rotate(radians(frameCount * 2 % 360)) rect(0, 0, 80, 20) popMatrix() ScalingThe final coordinate system transformation is scaling, which changes the size of the grid. Take a look at this example, which draws a square, then scales the grid to twice its normal size, and draws it again. def setup(): size(200,200) background(255) stroke(128) rect(20, 20, 40, 40) stroke(0) pushMatrix() scale(2.0) rect(20, 20, 40, 40) popMatrix() First, you can see that the square appears to have moved. It hasn’t, of course. Its upper left corner is still at (20, 20) on the scaled-up grid, but that point is now twice as far away from the origin as it was in the original coordinate system. You can also see that the lines are thicker. That’s no optical illusion—the lines really are twice as thick, because the coordinate system has been scaled to double its size.
There is no law saying that you have to scale the x and y dimensions
equally. Try using Order MattersWhen you do multiple transformations, the order makes a difference. A rotation followed by a translate followed by a scale will not give the same results as a translate followed by a rotate by a scale. Here is some sample code and the results. def setup(): size(200, 200) background(255) smooth() line(0, 0, 200, 0) # draw axes line(0, 0, 0, 200) pushMatrix() fill(255, 0, 0) # red square rotate(radians(30)) translate(70, 70) scale(2.0) rect(0, 0, 20, 20) popMatrix() pushMatrix() fill(255) # white square translate(70, 70) rotate(radians(30)) scale(2.0) rect(0, 0, 20, 20) popMatrix() The Transformation Matrix
Every time you do a rotation, translation, or scaling, the information
required to do the transformation is accumulated into a table of
numbers. This table, or matrix has only a few rows
and columns, yet, through the miracle of mathematics, it contains all the
information needed to do any series of transformations. And that’s
why the Push and PopWhat, about the push and pop part of the names? These come from a computer concept known as a stack, which works like a spring-loaded tray dispenser in a cafeteria. When someone returns a tray to the stack, its weight pushes the platform down. When someone needs a tray, he takes it from the top of the stack, and the remaining trays pop up a little bit. In a similar manner,
Note: in Processing, the coordinate system is restored to its original state
(origin at the upper left of the window, no rotation, and no scaling) every
time that the Three-dimensional Transforms
If you are working in three dimensions, you can call the
For rotation, call the Case Study: An Arm-Waving RobotLet’s use these transformations to animate a blue robot waving its arms. Rather than try to write it all at once, we will do the work in stages. The first step is to draw the robot without any animation.
The robot is modeled on
this
drawing, although it will not look as charming.
First, we draw the robot so that its
left and top side touch the x and y axes. That
will allow us to use When we refer to left and right in this drawing, we mean your left and right (the left and right side of your monitor), not the robot’s left and right. def setup(): size(200, 200) background(255) smooth() drawRobot() def drawRobot(): noStroke() fill(38, 38, 200) rect(20, 0, 38, 30) # head rect(14, 32, 50, 50) # body rect(0, 32, 12, 37) # left arm rect(66, 32, 12, 37) # right arm rect(22, 84, 16, 50) # left leg rect(40, 84, 16, 50) # right leg fill(222, 222, 249) ellipse(30, 12, 12, 12) # left eye ellipse(47, 12, 12, 12) # right eye The next step is to identify the points where the arms pivot. That is shown in this drawing. The pivot points are (12, 32) and (66, 32). Note: the term “center of rotation” is a more formal term for the pivot point.
Now, separate the code for drawing the left and right
arms, and move the center of rotation for each arm to the origin, because
you always rotate around the (0, 0) point. To save space,
we are not repeating the code for def drawRobot(): noStroke() fill(38, 38, 200) rect(20, 0, 38, 30) # head rect(14, 32, 50, 50) # body drawLeftArm() drawRightArm() rect(22, 84, 16, 50) # left leg rect(40, 84, 16, 50) # right leg fill(222, 222, 249) ellipse(30, 12, 12, 12) # left eye ellipse(47, 12, 12, 12) # right eye def drawLeftArm(): pushMatrix() translate(12, 32) rect(-12, 0, 12, 37) popMatrix() def drawRightArm(): pushMatrix() translate(66, 32) rect(0, 0, 12, 37) popMatrix() Now test to see if the arms rotate properly. Rather than attempt a full animation, we will just rotate the left side arm 135 degrees and the right side arm -45 degrees as a test. Here is the code that needs to be added, and the result. The left side arm is cut off because of the window boundaries, but we’ll fix that in the final animation. def drawLeftArm(): pushMatrix() translate(12, 32) rotate(radians(135)) rect(-12, 0, 12, 37) # left arm popMatrix() def drawRightArm(): pushMatrix() translate(66, 32) rotate(radians(-45)) rect(0, 0, 12, 37) # right arm popMatrix() Now we complete the program by putting in the animation. The left arm has to rotate from 0° to 135° and back. Since the arm-waving is symmetric, the right-arm angle will always be the negative value of the left-arm angle. To make things simple, we will go in increments of 5 degrees. armAngle = 0 angleChange = 5 ANGLE_LIMIT = 135 def setup(): size(200, 200) smooth() frameRate(30) def draw(): global armAngle, angleChange, ANGLE_LIMIT print armAngle background(255) pushMatrix() translate(50, 50) # place robot so arms are always on screen drawRobot() armAngle += angleChange # if the arm has moved past its limit, # reverse direction and set within limits. if (armAngle > ANGLE_LIMIT or armAngle < 0): angleChange = -angleChange armAngle += angleChange popMatrix() def drawRobot(): noStroke() fill(38, 38, 200) rect(20, 0, 38, 30) # head rect(14, 32, 50, 50) # body drawLeftArm() drawRightArm() rect(22, 84, 16, 50) # left leg rect(40, 84, 16, 50) # right leg fill(222, 222, 249) ellipse(30, 12, 12, 12) # left eye ellipse(47, 12, 12, 12) # right eye def drawLeftArm(): global armAngle pushMatrix() translate(12, 32) rotate(radians(armAngle)) rect(-12, 0, 12, 37) # left arm popMatrix() def drawRightArm(): global armAngle pushMatrix() translate(66, 32) rotate(radians(-armAngle)) rect(0, 0, 12, 37) # right arm popMatrix() Case Study: Interactive RotationInstead of having the arms move on their own, we will modify the program so that the arms follow the mouse while the mouse button is pressed. Instead of just writing the program at the keyboard, we first think about the problem and figure out what the program needs to do. Since the two arms move independently of one another, we need to have one variable for each arm’s angle. It’s easy to figure out which arm to track. If the mouse is at the left side of the robot’s center, track the left arm; otherwise, track the right arm.
The remaining problem is to figure out the angle of rotation. Given the
pivot point position and the mouse position, how do you determine the
angle of a line connecting those two points? The answer comes from the
But what about finding the angle of a line that doesn’t start from the origin, such as the line from (10, 37) to (48, 59)? No problem; it’s the same as the angle of a line from (0, 0) to (48-10, 59-37). In general, to find the angle of the line from (x0, y0) to (x1, y1), calculate atan2(y1 - y0, x1 - x0)
Because this is a new concept, rather than integrate it into the robot
program, you should write a
simple test program to see that you understand how def setup(): size(200, 200) def draw(): angle = atan2(mouseY - 100, mouseX - 100) background(255) pushMatrix() translate(100, 100) rotate(angle) rect(0, 0, 50, 10) popMatrix()
That works great. What happens if we draw the rectangle so it is
taller than it is wide? Change the preceding code to read
At this point, we can write the final version of the
arm-tracking program. We start off with definitions
of constants and variables. The number 39
in the definition of # Where upper left of robot appears on screen ROBOT_X = 50 ROBOT_Y = 50 # The robot's midpoint and arm pivot points MIDPOINT_X = 39 LEFT_PIVOT_X = 12 RIGHT_PIVOT_X = 66 PIVOT_Y = 32 leftArmAngle = 0.0 rightArmAngle = 0.0 def setup(): size(200, 200) smooth() frameRate(30)
The def draw(): """ These variables are for mouseX and mouseY, adjusted to be relative to the robot's coordinate system instead of the window's coordinate system. """ global leftArmAngle, rightArmAngle background(255) pushMatrix() translate(ROBOT_X, ROBOT_Y) # place robot so arms are always on screen if (mousePressed): mX = mouseX - ROBOT_X mY = mouseY - ROBOT_Y if (mX < MIDPOINT_X): # left side of robot leftArmAngle = atan2(mY - PIVOT_Y, mX - LEFT_PIVOT_X) - HALF_PI else: rightArmAngle = atan2(mY - PIVOT_Y, mX - RIGHT_PIVOT_X) - HALF_PI; drawRobot() popMatrix()
The def drawLeftArm(): pushMatrix() translate(12, 32) rotate(leftArmAngle) rect(-12, 0, 12, 37) # left arm popMatrix() def drawRightArm(): pushMatrix() translate(66, 32) rotate(rightArmAngle) rect(0, 0, 12, 37) # right arm popMatrix() You can download the files from this tutorial, including the program that made the grid diagrams.
This tutorial is for Processing's Python Mode. If you see any errors or have comments, please let us know. This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. |
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License