Transformations

Resources:

Shiffman - 9.1 Transformation Pt. 1 - p5.js
Shiffman - 9.2 Transformation Pt. 2 - p5.js
Shiffman - 9.3 Transformation Pt. 3 - p5.js

p5 Reference - translate()
p5 Reference - totate()
p5 Reference - scale()
p5 Reference - push()

Transformations

p5 has a novel way of dealing with transformations. It makes it seem a little difficult to do things like simple rotation at first, but its usefulness becomes apparent when you're trying to do nested systems for example, like a solar system. The main syntax we'll use for transformations is translate(), rotate(), scale(), push(), and pop().

The translate() function moves our origin point to somewhere else on the canvas. This becomes the new (0,0), and becomes the pivot point to use for rotations. The code below first moves the origin, then draws a rect() at the origin. It's the same as simply writing rect(50, 50, 100, 50);

Why would we want to do this? Because transformations are cumulative. In other words, this method makes it easy to make geometry relative to other geometry. Try making another rectangle without changing the translation. What happens if you replace the translate(50, 50) with translate(mouseX, mouseY)? Transformations do get reset to (0,0) on each pass of the draw() loop.

rotate()

The other main reason to do use transformations is for rotation. We need to specify a pivot point, because otherwise the rotate() function will assume the canvas (0,0) point (top left corner) to be the pivot point.

By default, p5 wants to calculate angles using radians as opposed to degrees. We can change that by declaring angleMode(DEGREES); in the setup() function.

Objects are always rotated around their relative position to the origin and positive numbers rotate objects in a clockwise direction. Transformations apply to everything that happens after and subsequent calls to the function accumulates the effect. For example, calling rotate(HALF_PI) and then rotate(HALF_PI) is the same as rotate(PI). All tranformations are reset when draw() begins again.

Technically, rotate() multiplies the current transformation matrix by a rotation matrix. This function can be further controlled by the push() and pop().

scale()

Increases or decreases the size of a shape by expanding and contracting vertices. Objects always scale from their relative origin to the coordinate system. Scale values are specified as decimal percentages. For example, the function call scale(2.0) increases the dimension of a shape by 200%.

Transformations apply to everything that happens after and subsequent calls to the function multiply the effect. For example, calling scale(2.0) and then scale(1.5) is the same as scale(3.0). If scale() is called within draw(), the transformation is reset when the loop begins again.

push() and pop()

The push() function saves the current drawing style settings and transformations, while pop() restores these settings. Note that these functions are always used together. They allow you to change the style and transformation settings and later return to what you had. When a new state is started with push(), it builds on the current style and transform information. The push() and pop() functions can be embedded to provide more control. (See the second example for a demonstration.)

push() stores information related to the current transformation state and style settings controlled by the following functions: fill(), stroke(), tint(), strokeWeight(), strokeCap(), strokeJoin(), imageMode(), rectMode(), ellipseMode(), colorMode(), textAlign(), textFont(), textMode(), textSize(), textLeading().

Notice that the styling that was applied within the custom drawing state is not applied to the first ellipse or the last.

Transformation Matrix

"Unfortunately, no one can be told what the Matrix is. You have to see it for yourself." - Morpheus

Transformations are geometrical instructions that can be encoded in a matrix. The size of a transformation matrix corresponds to its degrees of freedom. A default 2D transformation matrix with no rotation and no scaling looks like:

1   0  
0   1  

You can think of this as two vectors, one representing the x-axis, and one representing the y-axis. The first column is [1, 0], indicating that the vector representing the x-axis will land at the point (1, 0) relative to the origin. The second colum is [0, 1], indicating that the y-axis vector will land on the point (0, 1). If we wanted to scale the transformation uniformly by 2, we would do the following:

2   0  
0   2  

We can use the functions applyMatrix() and resetMatrix() to manually assign transformations in matrix form. Otherwise, we can use rotate() and scale() to do the job for us.

The following example renders an image of a grid that represents the transformation plane. Try using the translate() function to center the grid on the canvas. Can you make it follow the mouse?

Next, let's modify the example below to rotate to the mouse position. We can calculate the amount to rotate using the atan2() function.

Try introducing scaling based on distance from the cursor to the center of the canvas.

Next, let's make this object-oriented, so that we can have an array of independent transformations. Solution below:

Now let's modify the previous example, introducing cumulative push() / pop() transformations in order to create a simple "solar system" where an array of "planets" rotate around their center, but also orbit around a central "star". Let's render the star as a static image at the center of the canvas. Each Planet should have an offset value, which determines its distance from the "sun". We can use the same angle property to define rotation around the sun as well as rotation about the center of the planet (although these should actually be different values). Solution below:

Try modifying the above code to add a tint() to the images to distinguish the planets.