Moonlight

Under Construction

How would you draw Earth on a computer?

$\lambda$ Transformation

Drawing collections of objects can get tricky fast. The sketch below draws a simple representation of Orion’s Belt.

function setup() {
createCanvas(400, 400);
}

function draw() {
background(0);
stroke(255);
strokeWeight(5);
// alnitak
point(50, 100);
// alnilam
point(100, 75);
// mintaka
point(150, 50);
}


What if you wanted to shift everything to the left a little? Or how about representing the view from another location on Earth? It turns out realignments, or transformations, like these are easily handled by playing a little with our coordinate system.

Translate

How would you move the constellation $100$ pixels to the right?

One way to go about it would be to comb through all your calls to point(), adding $100$ to the first argument.

function setup() {
createCanvas(400, 400);
}

function draw() {
background(0);
stroke(255);
strokeWeight(5);
// alnitak
point(100, 250);
// alnilam
point(200, 175);
// mintaka
point(300, 100);
}


What if you wanted to move the constellation a smidge to the left? Or a little down?

Each adjustment would require you to update all your calls to point() again. As you can imagine, this would quickly become a hassle for sketches composed of many objects.

Shifting, or translating, is a transformation simplifies this sort of work.

Add a call to translate() with two arguments for the amount left/right and up/down you’d like to shift your constellation.

function setup() {
createCanvas(400, 400);
}

function draw() {
background(0);
stroke(255);
strokeWeight(5);
translate(75, 50);
// alnitak
point(100, 250);
// alnilam
point(200, 175);
// mintaka
point(300, 100);
}


Move your call to translate() to the end of draw(). What happened?

One way to think about transformations is as an adjustment to your coordinate system. Calling translate(1, 2) effectively shifts the origin from the top-left corner to $(1,2)$. Anything drawn afterward will appear relative to the new origin.

Translations really start to shine when combined with other transformations.

Rotate

The Moon is Earth’s natural satellite and a source of inspiration for numerous creative works. Two notable examples are Nick Drake’s album Pink Moon and the Lua programming language.

Let’s see if we can put the Moon in orbit around Earth. This sketch is a little more ambitious, so it will help to lay out some initial ideas.

function setup() {
createCanvas(400, 400);
}

function draw() {
background(220);
// drawEarth();
// drawMoon();
}


Breaking a big problem down into manageable pieces is known as decomposition in computer science. The practice will serve you well in many walks of life.

To keep things simple, draw Earth as a big, blue circle in the middle of your canvas. Remove the // when you’re ready to go.

function setup() {
createCanvas(400, 400);
}

function draw() {
background(0);
drawEarth();
// drawMoon();
}

function drawEarth() {
fill(0, 0, 255);
circle(200, 200, 200);
}


You could make the drawEarth() function a bit more flexible by adding parameters for Earth’s position. Let’s make use of translate() as well.

function setup() {
createCanvas(400, 400);
}

function draw() {
background(0);
drawEarth(200, 200);
// drawMoon();
}

function drawEarth(x, y) {
fill(0, 0, 255);
translate(x, y);
circle(0, 0, 200);
}


Pass mouseX and mouseY as arguments to drawEarth(). You’ll have the whole world in your hands.

Not bad. Now we’ll draw the Moon as another circle some distance away from Earth.

Transformations accumulate, so let’s take advantage of the fact that the origin is now at Earth’s center.

function setup() {
createCanvas(400, 400);
}

function draw() {
background(0);
drawEarth(200, 200);
drawMoon(50);
}

function drawEarth(x, y) {
fill(0, 0, 255);
translate(x, y);
circle(0, 0, 200);
}

fill(200);
circle(0, 0, 50);
}


Switch the order in which you call drawEarth() and drawMoon(). What happened?

The sketch works for the moment, but it’s worth getting ahead of some common sources of error. Transformations accumulate by default, but we have the ability to isolate groups of related transformations.

It’s easy to rewrite drawEarth() and drawMoon() so that their transformations no longer impact one another—just call push() before the first transformation and pop() after the last piece of the object.

We’ll rewrite drawMoon() so that it’s position is based on two transformations, first to Earth’s center and then to its place in orbit.

function setup() {
createCanvas(400, 400);
}

function draw() {
background(0);
drawEarth(200, 200);
drawMoon(200, 200, 150);
}

function drawEarth(x, y) {
push();
fill(0, 0, 255);
translate(x, y);
circle(0, 0, 200);
pop();
}

push();
fill(200);
translate(centerX, centerY);
circle(0, 0, 50);
pop();
}


Switch the order in which you call drawEarth() and drawMoon(). What happened this time?

You can think of rotations as an adjustment to your coordinate system, just like translations. Rotating your coordinate system $45$ degrees clockwise would leave it looking something like the following.

The $y$-axis label wound up off the canvas after rotating. Let’s try to get it back.

You can compose a sequence of transformations to generate many interesting effects. Here’s the same $45$ degree clockwise rotation followed by a translation along the (rotated) positive $x$-axis.

p5.js measures degrees in radians by default. They can be simpler to work with once you’ve gotten the hang of them, but we’ll stick to degrees for the time being.

The Moon’s place in orbit will change, and when things change, it’s probably time to use variables.

The following sketch adds the variable angle and uses it to control rotation. Note the call to angleMode() in setup().

let angle = 0;

function setup() {
createCanvas(400, 400);
angleMode(DEGREES);
}

function draw() {
background(0);
drawEarth(200, 200);
drawMoon(200, 200, 150, angle);
angle = angle + 0.25;
}

function drawEarth(x, y) {
push();
fill(0, 0, 255);
translate(x, y);
circle(0, 0, 200);
pop();
}

function drawMoon(centerX, centerY, radius, angle) {
push();
fill(200);
translate(centerX, centerY);
rotate(angle);
circle(0, 0, 50);
pop();
}


Rearrange the transformations in drawMoon() to see what happens. Breaking programs can provide great insight into how they work.

Scale

Earth and the Moon look good, but the sky is a little too empty, even by space standards. Let’s add our constellation to help fill out the sky.

let angle = 0;

function setup() {
createCanvas(400, 400);
angleMode(DEGREES);
}

function draw() {
background(0);
drawConstellation(5, 15);
drawEarth(200, 200);
drawMoon(200, 200, 150, angle);
angle = angle + 0.25;
}

function drawConstellation(x, y) {
push();
translate(x, y);
stroke(255);
strokeWeight(5);
// alnitak
point(100, 250);
// alnilam
point(200, 175);
// mintaka
point(300, 100);
pop();
}

function drawEarth(x, y) {
push();
translate(x, y);
fill(0, 0, 255);
circle(0, 0, 200);
pop();
}

function drawMoon(centerX, centerY, radius, angle) {
push();
translate(centerX, centerY);
rotate(angle);
fill(200);
circle(0, 0, 50);
pop();
}


Write your own drawConstellation() function with parameters x and y.

The stars are arranged just fine but they’re a little too big. We can adjust the overall size, or scale, of objects using the scale() function.

let angle = 0;

function setup() {
createCanvas(400, 400);
angleMode(DEGREES);
}

function draw() {
background(0);
drawConstellation(5, 15, 0.5);
drawEarth(200, 200);
drawMoon(200, 200, 150, angle);
angle = angle + 0.25;
}

function drawConstellation(x, y, theScale) {
push();
translate(x, y);
scale(theScale);
stroke(255);
strokeWeight(5);
// alnitak
point(50, 100);
// alnilam
point(100, 75);
// mintaka
point(150, 50);
pop();
}

function drawEarth(x, y) {
push();
translate(x, y);
fill(0, 0, 255);
circle(0, 0, 200);
pop();
}

function drawMoon(centerX, centerY, radius, angle) {
push();
translate(centerX, centerY);
rotate(angle);
fill(200);
circle(0, 0, 50);
pop();
}


Change the order you call functions in draw() to see what happens.

Like translate() and rotate(), you can think of scale() visually as a stretch or compression of your coordinate system. The graph paper below was compressed by a factor of $0.5$ in the $x$-direction and stretched by a factor of $2$ in the $y$-direction.

Transformation Review

How do translate(), rotate(), and scale() work?

Noise

Perlin Noise

let angle = 0;

function setup() {
createCanvas(400, 400);
angleMode(DEGREES);
}

function draw() {
background(0);
drawStars();
drawConstellation(5, 15, 0.5);
drawEarth(200, 200);
drawMoon(200, 200, 150, angle);
angle = angle + 0.25;
}

function drawStars() {
const noiseLevel = 0.5;
for (let i = 0; i < 250; i += 1) {
push();
translate(-width/2, -height/2);
stroke(255, 255*noise(noiseLevel*i + 1000));
strokeWeight(5*noise(noiseLevel*i + 2000));
point(2*width*noise(noiseLevel*i + 3000), 2*height*noise(noiseLevel*i + 4000));
pop();
}
}

function drawConstellation(x, y, theScale) {
push();
translate(x, y);
scale(theScale);
stroke(255);
strokeWeight(5);
// alnitak
point(50, 100);
// alnilam
point(100, 75);
// mintaka
point(150, 50);
pop();
}

function drawEarth(x, y) {
push();
translate(x, y);
fill(0, 0, 255);
circle(0, 0, 200);
pop();
}

function drawMoon(centerX, centerY, radius, angle) {
push();
translate(centerX, centerY);
rotate(angle);
fill(200);
circle(0, 0, 50);
pop();
}


Project Ideas

Interactivity Rotate the Moon using your mouse.

Animation Fill out the surfaces of Earth and the Moon using 2D Primitives.