Rotate and Translate

Rotation and translation behave unintuitively in p5.js. In particular, the order in which you call rotate and translate has a significant impact. This sketch demonstrates the effect of different combinations of rotation and translation.

Click or tap around the circle to change the degrees of rotation.

Blue and Red are first rotated, then translated, resulting in a sweeping arc around their radius of translation. By contrast, Green and Orange are translated first, then rotated, producing the effect of pivoting in place.

Observe that Blue appears to fly around the perimeter while Red points to the outside like the hand of a clock. They have the same rotation but different translation. If you want Blue to occupy the 12 o'clock position when the input is 0°, you can achieve this by adding 90° to its starting rotation (try the checkbox).

Calling rotate(x+90) is equivalent to calling rotate(x); rotate(90) — the second rotate call applies cumulatively with the first one.

Use the "second-order rotations" checkbox to see the effects of using multiple rotate and translate calls. Use "purple fixed at 90" to produce a similar "flying" trajectory that blue demonstrated earlier.

You will need to vary calls to translate and rotate depending on your inputs (mouse position or time, for example) and the initial position of your shapes.

Source Code for this Sketch

let degrees = 0
let blueOffset = 0
let secondOrderRotations = false
let purpleFixed = false

function drawArrows() {
	// Center position
	c("grey")
	arrow()
	ring()

	// Translate X, then rotate
	if (!secondOrderRotations) {
	push()
		c("green")
		translate(-100, 0)
		rotate(degrees)
		arrow()
	pop()
	}

	// Translate Y, then rotate
	push()
		c("orange")
		translate(0, -100)
		rotate(degrees)
		arrow()
	pop()

	// Rotate, then translate X
	if (!secondOrderRotations) {
		push()
			c("blue")
			rotate(degrees+blueOffset)
			line(0, 0, -100, 0)
			translate(-100, 0)
			arrow()
		pop()
	}

	// Rotate, then translate Y
	push()
		c("red")
		rotate(degrees)
		line(0, 0, 0, -100)
		translate(0, -100)
		arrow()

		if (secondOrderRotations) {
			ring(50)

			// Translate Y, then rotate
			push()
				c("purple")
				line(0, 0, 0, -50)
				translate(0, -50)
				rotate(purpleFixed ? 90 : degrees)
				arrow()
			pop()

			// Rotate, then translate Y
			push()
				c("teal")
				rotate(degrees)
				line(0, 0, 0, -50)
				translate(0, -50)
				arrow()
			pop()
		}
	pop()
}

function c(color) {
	fill(color)
	stroke(color)
}

function arrow() {
	push()
		noStroke()
		rect(-5, 0, 10, 20)
		triangle(-10, 0, 10, 0, 0, -20)
	pop()
}

function ring(radius) {
	radius = radius ? radius : 100
	push()
		noFill()
		circle(0, 0, radius*2)
	pop()
}

function grid(spacing) {
	strokeWeight(1)
	stroke(197, 184, 147)
	fill(197, 184, 147)
	textSize(10)

	for (let i = spacing; i < width; i += spacing) {
		text(i-height/2, i + 3, 10)
		line(i, 0, i, height);
	}
	for (let i = spacing; i < height; i += spacing) {
		text(i-width/2, 3, i + 10)
		line(0, i, width, i);
	}
}

function inRect(x, y, w, h) {
	return (mouseX >= x && mouseX < x+w && mouseY >= y && mouseY < y+h)
}

function setup() {
	createCanvas(400, 400, P2D, canvas)
	angleMode(DEGREES)
}

function draw() {
	if (mouseIsPressed && inRect(0, 0, width, height)) {
		degrees = round(abs(atan2(mouseX-200, mouseY-200)-180))
	}

	// Setup canvas
	background(232, 224, 201)
	grid(50)
	translate(width/2, height/2)
	strokeWeight(1)

	// Draw the arrows
	drawArrows()

	// Degrees label
	stroke(197, 184, 147)
	fill(197, 184, 147)
	textSize(14)
	text(degrees+"°", 115, 120)

	if (blueOffset !== 0 && !secondOrderRotations) {
		stroke("blue")
		fill("blue")
		text(degrees+blueOffset+"°", 115, 140)
	}
	if (purpleFixed && secondOrderRotations) {
		stroke("purple")
		fill("purple")
		text("90°", 115, 140)
	}
}

document.addEventListener("DOMContentLoaded", (e) => {
	let p5ui = document.getElementById("p5ui")
	p5ui.innerHTML = `
	<div style="text-align: left; width: 400px; margin: auto; user-select: none;">
		<label for="blueInput">Start blue at +90°</label><input type="checkbox" id="blueInput"><br>
		<label for="secondaryInput">Show second-order rotations</label><input type="checkbox" id="secondaryInput"><br>
		<label for="purpleInput">Purple fixed at 90°</label><input type="checkbox" id="purpleInput">
	</div>
	`

	let blueInput = document.getElementById("blueInput")
	let secondaryInput = document.getElementById("secondaryInput")
	let purpleInput = document.getElementById("purpleInput")

	blueInput.addEventListener("input", (e) => {
		blueOffset = e.target.checked ? 90 : 0
	})
	secondaryInput.addEventListener("input", (e) => {
		secondOrderRotations = e.target.checked
	})
	purpleInput.addEventListener("input", (e) => {
		purpleFixed = e.target.checked
	})
})

You may use all or part of the p5 code seen on this page for your own creations, without restriction. Attribution is appreciated but not required.

This sketch also includes the following sources: p5common.js . Sources are available under their respective licenses.

See all p5.js sketches