The Creative Coder’s Field Guide
Everything You Need to Survive and Thrive with p5.js
Creative Coding · Week 2
Week 2 is one idea wearing many outfits: a variable is a labeled container for a piece of data. Name it, fill it, change it, and the canvas starts to move.
Last week you borrowed p5's ready-made variables (width, mouseX) without making any of your own. This week you make your own.
A variable is a container with a name on the label and a piece of data inside. The data can be:
200, 3.5, -40"hello"Despite the name, variables don't have to vary. Some hold a value that changes every frame; others are set once and never touched again. What they all give you is a name, so instead of the mystery number 347 scattered through your code, you have sunX, and everyone (including you, next week) knows what it means.
let and =A thirty-second reminder from last week: syntax is JavaScript's grammar, the rules for how words, symbols, and punctuation have to be arranged, as opposed to vocabulary like p5's function names. And the grammar isn't a suggestion: get it wrong and the computer just stops. Last week you only had to recognize it on sight; from here on you're writing it, so the examples get more exact.
Two pieces of that grammar do all the work this week. let creates a variable; = puts a value into one.
let makes the container, x names it, and = fills it. The right side always runs first: width / 2 becomes 200, and the result is what lands in x.let x = 200;: create x and fill it, in one line.let x; then later x = 200;: the same thing, split in two. (You'll see why the split matters in section 06.)= must be a variable name. The right side can be anything that produces a value: a number, some math, another variable, mouseX… Code like that has a name: an expression (section 04).sunx and sunX are two different variables.Assigning again replaces what was inside, and JavaScript has shorthands for the most common replacements.
| Write | Say it as | The long way |
|---|---|---|
x = 5 | set x to 5 | n/a |
x += 3 | grow x by 3 | x = x + 3 |
x -= 3 | shrink x by 3 | x = x - 3 |
x *= 2 | double x | x = x * 2 |
x /= 2 | halve x | x = x / 2 |
x++ | add 1 to x | x = x + 1 |
x-- | subtract 1 from x | x = x - 1 |
x++, x += 1, and x = x + 1 do exactly the same thing. The long form shows what's really happening; the other two are abbreviations of it. Read all three fluently, then write whichever you like. You'll meet all of them in other people's code.
= is not “equals.” In algebra, x = x + 1 is impossible: nothing equals itself plus one. In JavaScript it's not a claim, it's a command: take what's in x, add 1, put the result back in x. Right side first, then store. Once that clicks, the whole line reads naturally.
That “right side of =” deserves its proper name. An expression is any piece of code the computer can work out (evaluate) down to a single value. This week, every expression we write evaluates to a number.
Numerical expressions are built from three kinds of ingredients:
200, 1.5, -40. (Already an expression, just a very short one.)width, x, mouseX.+ - * /), each sitting between two ingredients: width / 2, x + 70.Two grammar rules complete the picture. Parentheses group: (width + 10) / 2 says “add first, then divide.” And any expression can be an ingredient in a bigger one: mouseX - pmouseX is an expression, so (mouseX - pmouseX) * 2 is too. That nesting is how small pieces grow into exactly the value you need.
Order of operations works like math class: * and / before + and -. When in doubt, add parentheses: they cost nothing and say exactly what you mean.
x = (width + 10) / 2;, the 205 is what = actually stores.Anywhere the code needs a single number, a whole expression can stand in its place:
=: x = width / 2;ellipse(width / 2, height / 2, 40, 40); needs no in-between variable.(width + height) / 2.One thing an expression is not: a complete command. width / 2 on a line by itself does nothing: it's a value with nowhere to go, a phrase without a sentence. It only matters once something uses it: an assignment stores it, a function call receives it.
Expressions come in other flavors too: ones that evaluate to text, or to true/false. Those arrive in later weeks. This week: numbers.
Every variable you'll write this semester goes through some or all of these, in this order:
let x;): make the container.x = width / 2;): put a first value in.x = x + 1;): change it. Optional: variables don't have to vary.ellipse(x, y, 40, 40);): read what's inside. Technically optional too, since nothing breaks if you never use it, but then what was the point?draw(), so they repeat about 60 times a second, which is where motion will come from (section 09).Where you declare a variable decides who can see it: that's called its scope.
setup() and draw() can see it, and it survives from frame to frame.That survival is the whole game. A variable declared inside draw() is rebuilt from scratch 60 times a second, so it can never accumulate change. For anything that should move, the variable has to live outside, where the frames can't reset it.
x is visible everywhere and keeps its value between frames. Local secret belongs to setup() alone: asking for it in draw() is an error.Tempting: let x = width / 2; on line one. But line one runs before createCanvas() has made the canvas, so at that moment width doesn't hold anything useful yet. The value has to be assigned after the canvas exists, which means inside setup().
The Week 2 house pattern: declare at the top (global), initialize in setup(), update in draw(). Almost every sketch this week has that shape.
print()A variable's contents are invisible while the sketch runs, and half of debugging is answering one question: what is actually in this container right now? print() answers it. Whatever you hand it appears in the console, the panel at the bottom of the editor.
print(x); // 200
print("x is", x); // x is 200 (label it so you know which print this is)
print(mouseX); // wherever the mouse is this frame
print() is p5's name for it. Plain JavaScript calls it console.log(): same job, works everywhere, and it's what you'll see outside p5.print() inside draw() fires ~60 times a second. That's exactly what you want when watching a variable change, and a firehose when you don't.print() the variable you're suspicious of and look.Here's the deeper reason variables matter, beyond tidy names: once a value has a name, other code can be written in terms of it.
let x = 100;
ellipse(x, 200, 50, 50); // the anchor
ellipse(x + 70, 200, 50, 50); // always 70 to the right of it
ellipse(x, 140, 30, 30); // always directly above it
Change x once and all three move together, still in formation. The positions aren't three separate facts anymore: they're one fact plus two relationships.
A set of rules like this (“compute this from that”) is an algorithm. You've already been using them: (width / 2, height / 2) doesn't say where the center is, it says how to find the center of any canvas. And ellipse(mouseX, mouseY, 40, 40) is a relationship with the mouse itself, re-evaluated every frame, which is why the circle follows the cursor.
Hard-coded numbers describe one picture. Relationships describe every picture the rule allows: that's the shift from drawing to programming.
Nothing on the canvas ever actually moves.
What p5 gives you is about 60 still pictures every second, the frames from last week. Make each frame slightly different from the one before, and your eye does the rest. It's a flip-book; it's every movie ever made.
So each run of draw() follows the same three-beat recipe:
background(...): clear the previous frame away.let x;
function setup() {
createCanvas(400, 300);
x = 0;
}
function draw() {
background(220); // 1. clear
ellipse(x, 150, 40, 40); // 2. draw at the current x
x = x + 1; // 3. nudge: next frame is 1px further
}
background() before the new one is drawn, just a sliver behind. That clear-and-redraw is the entire secret of animation.One more piece: p5 keeps a variable called frameCount, which counts how many frames have been drawn so far. It follows the full lifecycle without you lifting a finger, so ellipse(frameCount, 150, 40, 40) slides right all on its own. A free clock, ticking 60 times a second.
The computer will run almost anything. Style is for the other readers of your code: your classmates, your teacher, and above all you, two weeks from now.
Indentation makes structure visible: every line inside a pair of braces steps in one level, so a glance tells you what belongs to setup() and what belongs to draw(). The computer ignores indentation completely, which is exactly why keeping it right is your job, not JavaScript's.
} usually becomes obvious the moment the indentation snaps into place.camelCase: eyeSize, ballSpeed, skyBlue. First word lowercase, every following word capitalized.x is fine for a position; thing2 is a note to your future self that says nothing.x and y, or ballX and ballY, not ballX and height2.A comment is a note the computer skips over entirely: it exists only for humans. JavaScript gives you two kinds:
//): everything from the slashes to the end of that line is ignored. Toggle one on any line with Cmd/Ctrl - /./* … */): everything between the markers is ignored, even across many lines. The natural home for the paragraph at the top of a sketch saying what it is./*
Drifting dot: Week 2 exercise.
Starts at the left edge and drifts right
2 pixels every frame.
*/
let x; // horizontal position, in pixels
function setup() {
createCanvas(400, 300);
x = 0;
}
function draw() {
background(220);
// ellipse(x, 75, 90, 90); switched off while I test
ellipse(x, 150, 40, 40);
x += 2;
}
x += 2; // add 2 to x restates the code and helps no one; // drift right, 2px per frame records a decision.// instead of deleting it, run, and switch it back when you've learned what you needed.Style isn't decoration. Most “bugs” this week turn out to be a stray brace or a mystery number, and tidy indentation, honest names, and a comment or two are how you find them fast. Tidy before you ask anyone (including yourself) to read your code.
p5 maintains a whole shelf of pre-filled containers, declared, initialized, and updated for you, fresh every frame. Your only job is step four of the lifecycle: use them.
| Variable | Holds |
|---|---|
width, height | The canvas's size in pixels, set by createCanvas() |
mouseX, mouseY | The cursor's position, right now (this frame) |
pmouseX, pmouseY | The cursor's position one frame ago (the p is for previous) |
frameCount | How many frames have been drawn since the sketch started |
mouseIsPressed | true while a mouse button is held down; false otherwise |
keyIsPressed | true while any key is held down |
key | The most recent key pressed, as text: "a", "G", " " |
mouseX - pmouseX is how far the mouse traveled in one frame: in other words, its speed. Two variables and a minus sign, and you've measured motion. (That's a relationship, section 08.)mouseIsPressed and keyIsPressed hold booleans, the true/false data type from section 01. For now, try print(mouseIsPressed) and watch it flip; making the sketch decide things with them arrives with if in a coming week.One exercise, worth doing slowly: move an ellipse from the center of the canvas to each of the four corners, on a canvas that is not a square. The not-a-square part looks like a footnote; it's what keeps you honest about width and height.
The house pattern from section 06: declare at the top, initialize in setup().
let x;
let y;
function setup() {
createCanvas(400, 300); // not a square, on purpose
x = width / 2; // 200
y = height / 2; // 150
}
For each corner, decide two things before typing: does x need to grow or shrink to get there? Does y? (Remember: y grows downward.)
Bottom-right first, since both values grow:
function draw() {
background(220);
ellipse(x, y, 40, 40);
x++; // or x += 1, or x = x + 1: your pick
y++;
}
Run it: the ellipse drifts steadily down and to the right, and off the canvas. That's one corner done.
Same sketch, only the signs on the two update lines change:
| Corner | Update lines in draw() |
|---|---|
Top-left (0, 0) | x--; y--; |
Top-right (400, 0) | x++; y--; |
Bottom-left (0, 300) | x--; y++; |
Bottom-right (400, 300) | x++; y++; |
Two honest notes: the ellipse won't stop at the corner. It sails right off the canvas. Stopping needs if, which is coming. And if anything looks wrong along the way, print(x, y) in draw() (section 07) and watch the numbers travel.
Watch a journey closely and you'll notice the ellipse actually leaves through an edge near its corner, not the corner itself. That's the non-square canvas at work: from the center, x has 200 pixels to travel but y only has 150, so at equal speeds, y runs out of canvas first and the ellipse slips out early.
The fix: give the speeds the same ratio as the distances.
x += 2;
y += 1.5; // 2 : 1.5, the same ratio as 200 : 150
Now both arrive at the same moment, and the path runs straight through the corner. Notice what you just did: the speeds are a relationship (section 08), and they encode the shape of the canvas.