Learn Creative Coding (#17) - State Machines for Animations
Last episode we got smooth, polished motion with lerp and easing functions. Everything glides, settles, bounces -- motion that feels alive instead of robotic. But here's the thing: all of our sketches so far have been one continuous stream of behavior. Particles drift, shapes pulse, the galaxy spins. There's no concept of "what's happening right now" versus "what happens next." No intro screen that dissolves into a main scene. No modes the user can switch between. No sequence of events that plays out over time.
Think about any game you've played, any interactive installation you've seen, any kiosk at a museum. There's always a structure underneath: an idle state, an active state, maybe a transition between them. Press a button and something changes. Wait too long and it resets. That structure is a state machine. And despite the intimidating computer-science name, it's one of the simplest patterns in programming.
A state machine is just a variable that tracks "what mode are we in right now" plus rules for when to change modes. That's it. Your phone's lock screen is a state machine. A traffic light is a state machine. A vending machine (literally in the name!) is a state machine. Different inputs do different things depending on the current state. When your phone is locked, pressing the home button unlocks it. When it's unlocked, the same button goes to the home screen. Same input, different response, because the state is different.
In creative coding, state machines let us build sketches with narrative structure -- progression, chapters, modes, transitions. Things that feel designed and intentional. This episode is about giving your sketches memory :-)
The simplest state machine
You already know everything you need. A state machine is a variable:
let state = 'intro';
Done. You have a state machine. Now you route behavior based on it:
function draw() {
if (state === 'intro') {
drawIntro();
} else if (state === 'main') {
drawMain();
} else if (state === 'outro') {
drawOutro();
}
}
Three different visual behaviors, cleanly separated. The variable state is the only thing deciding which one runs. Changing it swaps the entire sketch's behavior in a single frame. The concept isn't complex -- the art is in managing transitions cleanly.
A two-state sketch with cross-fade
Let's build something real: a title screen that fades into a particle field when you click. We need two states (title and particles) plus a transition variable that blends between them:
let state = 'title';
let transition = 0;
let transitioning = false;
let particles = [];
function setup() {
createCanvas(600, 400);
textAlign(CENTER, CENTER);
textFont('monospace');
for (let i = 0; i < 200; i++) {
particles.push({
x: random(width),
y: random(height),
vx: random(-1, 1),
vy: random(-1, 1),
size: random(2, 6)
});
}
}
function drawTitle() {
background(15);
fill(255, 255 * (1 - transition));
textSize(32);
text('particle field', width/2, height/2 - 20);
textSize(14);
fill(150, 150 * (1 - transition));
text('click to enter', width/2, height/2 + 30);
}
function drawParticles() {
background(15, 15, 15, 20);
for (let p of particles) {
p.x += p.vx;
p.y += p.vy;
if (p.x < 0 || p.x > width) p.vx *= -1;
if (p.y < 0 || p.y > height) p.vy *= -1;
fill(100, 200, 255, 200 * transition);
noStroke();
ellipse(p.x, p.y, p.size);
}
}
function draw() {
if (transitioning) {
transition += 0.02;
if (transition >= 1) {
transition = 1;
transitioning = false;
state = 'particles';
}
}
if (state === 'title' || transitioning) {
drawTitle();
if (transitioning) drawParticles();
} else {
drawParticles();
}
}
function mousePressed() {
if (state === 'title' && !transitioning) {
transitioning = true;
}
}
During the transition, BOTH scenes draw simultaneously. The title text fades out via 255 * (1 - transition), the particles fade in via 200 * transition. Cross-fade. The transition variable goes from 0 to 1 at 0.02 per frame, so the whole thing takes about 50 frames -- roughly a second at 60fps. Smooth, cinematic, and the code is completely readable.
Notice the transition speed (0.02) controls the feel. Remember episode 16 where we talked about how different easing speeds change the character of motion? Same principle here. A faster transition (0.05) feels snappy and decisive. A slower one (0.008) feels dreamy and gradual. You could even apply an easing function to the transition variable instead of incrementing it linearly -- ease-in-out would make the cross-fade accelerate in the middle and decelerate at the edges. Try it. The difference is subtle but noticeable.
Organized state objects
That if/else approach works for two states. But creative coding projects grow. You start with idle and active. Then you add a loading state. Then transition-in, transition-out. A menu. A pause screen. Without structure, your draw loop becomes a nest of conditionals that's impossible to read.
Here's a cleaner pattern -- each state is an object with enter(), update(), and draw() methods:
const states = {
intro: {
enter() {
this.startTime = millis();
this.alpha = 0;
},
update() {
let elapsed = millis() - this.startTime;
this.alpha = constrain(elapsed / 1000, 0, 1);
if (elapsed > 3000) {
switchState('scene1');
}
},
draw() {
background(10);
fill(255, this.alpha * 255);
textSize(40);
textAlign(CENTER, CENTER);
text('Chapter One', width/2, height/2);
}
},
scene1: {
enter() {
this.hue = 0;
},
update() {
this.hue = (this.hue + 0.5) % 360;
},
draw() {
colorMode(HSB, 360, 100, 100);
background(this.hue, 40, 15);
for (let i = 0; i < 20; i++) {
let x = width/2 + cos(frameCount * 0.02 + i) * (100 + i * 10);
let y = height/2 + sin(frameCount * 0.03 + i) * (80 + i * 8);
fill((this.hue + i * 15) % 360, 80, 90);
noStroke();
ellipse(x, y, 10 + i, 10 + i);
}
colorMode(RGB, 255);
}
},
scene2: {
enter() {
this.grid = [];
let cols = 20, rows = 15;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
this.grid.push({
x: (c + 0.5) * (width / cols),
y: (r + 0.5) * (height / rows),
size: 0,
targetSize: random(5, 25)
});
}
}
},
update() {
for (let cell of this.grid) {
cell.size = lerp(cell.size, cell.targetSize, 0.05);
}
},
draw() {
background(10);
for (let cell of this.grid) {
fill(200, 100, 50);
noStroke();
ellipse(cell.x, cell.y, cell.size);
}
}
}
};
let currentState = null;
function switchState(name) {
currentState = states[name];
if (currentState.enter) currentState.enter();
}
function setup() {
createCanvas(600, 400);
switchState('intro');
}
function draw() {
if (currentState) {
currentState.update();
currentState.draw();
}
}
function keyPressed() {
if (key === '1') switchState('scene1');
if (key === '2') switchState('scene2');
}
Each state is completely self-contained. enter() initializes whatever that state needs (timers, grids, counters). update() runs the logic. draw() handles rendering. The switchState function calls enter() on the new state, so everything gets initialized fresh.
See how scene2 uses lerp on the grid cells? That's the lerp-toward-target pattern from episode 16 -- the grid "grows in" when you enter the state because every cell starts at size 0 and lerps toward its target. The state's enter() function sets up those initial conditions. Clean separation between initialization and animation.
Adding a new state means adding a new object to the states dictionary. You never touch existing state code. That's the real power -- states are independent modules that don't know about each other. The state machine just swaps between them.
Transition effects
The cross-fade from the two-state example is just one way to move between states. Here are more transition effects, each with a completely different character.
Wipe transition
A horizontal wipe reveals the new state from left to right, like pulling a curtain:
let wipeProgress = 0;
function drawWipe(fromState, toState) {
toState.draw();
// clip the "from" state to the remaining area
ctx.save();
ctx.beginPath();
ctx.rect(wipeProgress * canvas.width, 0, canvas.width, canvas.height);
ctx.clip();
fromState.draw();
ctx.restore();
wipeProgress += 0.02;
}
The trick: draw the new state first (full canvas), then draw the old state on top but clipped to a shrinking rectangle. As wipeProgress goes from 0 to 1, the clip moves from left edge to right edge, progressively revealing what's underneath. Same save()/restore() pattern we used back when we were doing canvas transforms.
Pixelate dissolve
The current scene dissolves into blocky pixels before the new scene appears. This one reads the current frame's pixel data and redraws it at lower resolution:
function drawPixelateOut(state, progress) {
state.draw();
let blockSize = Math.max(1, Math.floor(progress * 40));
let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
for (let y = 0; y < canvas.height; y += blockSize) {
for (let x = 0; x < canvas.width; x += blockSize) {
let i = (y * canvas.width + x) * 4;
ctx.fillStyle = `rgb(${imgData.data[i]}, ${imgData.data[i+1]}, ${imgData.data[i+2]})`;
ctx.fillRect(x, y, blockSize, blockSize);
}
}
}
Remember pixel manipulation from episode 10? Same getImageData call, same (y * width + x) * 4 indexing into the pixel array. But instead of reading individual pixels, we're sampling one pixel per block and filling the whole block with that color. As progress increases, the blocks get bigger and the image gets chunkier. It has that retro-game-over feel that's weirdly satisfying.
Circle reveal
My personal favorite. An expanding circle reveals the new state from the center outward:
function drawCircleReveal(fromState, toState, progress) {
fromState.draw();
let prevFrame = ctx.getImageData(0, 0, canvas.width, canvas.height);
toState.draw();
ctx.save();
ctx.globalCompositeOperation = 'destination-in';
ctx.beginPath();
let maxRadius = Math.sqrt(
canvas.width * canvas.width + canvas.height * canvas.height
) / 2;
ctx.arc(canvas.width/2, canvas.height/2,
progress * maxRadius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
The globalCompositeOperation = 'destination-in' is doing the heavy lifting here. It tells the canvas to only keep pixels where the new drawing (our circle) overlaps with existing content. So we draw the new state, then stamp a circle mask over it -- only the area inside the circle survives. The expanding circle gradually reveals more of the new state. There's something cinematic about it, like an iris wipe in old films.
The maxRadius calculation uses the Pythagorean theorem to find the distance from center to corner -- that way the circle fully covers the canvas at progress=1, even in the corners. Trig doing the boring math so we can focus on the creative part :-)
Timing and sequencing
For presentations, narrative art pieces, or installations that loop, you often want things to happen at specific times. A timeline class handles this elegantly:
class Timeline {
constructor() {
this.events = [];
this.startTime = millis();
}
at(ms, callback) {
this.events.push({ time: ms, callback, fired: false });
return this;
}
update() {
let elapsed = millis() - this.startTime;
for (let event of this.events) {
if (!event.fired && elapsed >= event.time) {
event.callback();
event.fired = true;
}
}
}
reset() {
this.startTime = millis();
for (let event of this.events) {
event.fired = false;
}
}
}
let timeline = new Timeline();
timeline
.at(0, () => switchState('intro'))
.at(3000, () => switchState('scene1'))
.at(8000, () => switchState('scene2'))
.at(13000, () => switchState('outro'));
function draw() {
timeline.update();
if (currentState) {
currentState.update();
currentState.draw();
}
}
Chain events, reset the timeline, replay from the start. The return this in the at() method enables that fluent chaining syntax. Each event fires exactly once when its timestamp passes, and reset() marks them all as unfired and restarts the clock.
This is great for generative videos, installation loops, or any piece with a fixed temporal structure. You define the choreography once, and the timeline plays it. Combine this with eased transitions between states and you've got a proper performance system. Some creative coders use exactly this pattern for live shows -- triggering state transitions with a keyboard while the timeline handles the sequencing within each section.
A transition table approach
When your sketch has many states, keeping track of which transitions are valid gets messy. A transition table puts all the rules in one place:
const machine = {
current: 'idle',
transitions: {
idle: { click: 'loading' },
loading: { loaded: 'active', error: 'idle' },
active: { click: 'detail', escape: 'idle' },
detail: { escape: 'active', click: 'idle' }
},
send(event) {
let next = this.transitions[this.current]?.[event];
if (next) {
console.log(`${this.current} -> ${next}`);
this.current = next;
}
}
};
function draw() {
switch (machine.current) {
case 'idle': drawIdle(); break;
case 'loading': drawLoading(); break;
case 'active': drawActive(); break;
case 'detail': drawDetail(); break;
}
}
function mousePressed() { machine.send('click'); }
function keyPressed() { if (key === 'Escape') machine.send('escape'); }
All the valid transitions in one lookup table. The send() method checks if the event is valid for the current state. If it is, transition. If not, silently ignore it. Impossible to accidentally end up in an invalid state. You can literally read the transition table and trace every possible path through your application.
This pattern comes from formal automata theory in computer science, but you don't need to know any of that. Just think of it as a map: "when I'm in state X and event Y happens, go to state Z." At work I use this constantly for interactive data visualizations -- click, hover, drag, key press all route through a single transition table, and the draw function just renders based on the current state. Makes sense, right?
Practical state machine: interactive particle art
Allez, let's build something you can actually play with. An interactive sketch with three states: IDLE (particles drift calmly using noise), ATTRACT (particles rush toward the mouse), and EXPLODE (particles burst outward, then slowly return to idle). The state determines which forces apply to the particles -- everything else (drag, rendering) stays the same regardless of state.
let particles = [];
let state = 'idle';
let explodeFrame = 0;
function setup() {
createCanvas(600, 400);
for (let i = 0; i < 300; i++) {
particles.push({
x: random(width),
y: random(height),
vx: 0, vy: 0
});
}
}
function draw() {
background(20, 30);
switch (state) {
case 'idle':
particles.forEach(p => {
p.vx += (noise(p.x * 0.01, frameCount * 0.01) - 0.5) * 0.3;
p.vy += (noise(p.y * 0.01, frameCount * 0.01 + 100) - 0.5) * 0.3;
});
break;
case 'attract':
particles.forEach(p => {
let dx = mouseX - p.x;
let dy = mouseY - p.y;
let d = max(dist(p.x, p.y, mouseX, mouseY), 20);
p.vx += (dx / d) * 2;
p.vy += (dy / d) * 2;
});
break;
case 'explode':
particles.forEach(p => {
let dx = p.x - mouseX;
let dy = p.y - mouseY;
let d = max(dist(p.x, p.y, mouseX, mouseY), 20);
p.vx += (dx / d) * 8;
p.vy += (dy / d) * 8;
});
if (frameCount - explodeFrame > 60) state = 'idle';
break;
}
// shared physics -- drag + position update + rendering
particles.forEach(p => {
p.vx *= 0.98;
p.vy *= 0.98;
p.x += p.vx;
p.y += p.vy;
// wrap around edges
if (p.x < 0) p.x = width;
if (p.x > width) p.x = 0;
if (p.y < 0) p.y = height;
if (p.y > height) p.y = 0;
fill(255, 200);
noStroke();
ellipse(p.x, p.y, 3);
});
}
function mousePressed() {
if (state === 'idle') {
state = 'attract';
} else if (state === 'attract') {
state = 'explode';
explodeFrame = frameCount;
}
}
function mouseReleased() {
if (state === 'attract') {
state = 'idle';
}
}
Three completely different particle behaviors, cleanly separated in the switch statement. But the physics -- drag (*= 0.98), position update, edge wrapping, and rendering -- happen regardless of state. This is a clean pattern to remember: shared physics, state-specific forces. The state decides what forces to apply, the physics engine handles everything else uniformly.
The drag coefficient (0.98) means particles lose 2% of their velocity each frame. Without it, particles in the attract state would accelerate forever and shoot off screen. With it, they reach a natural terminal velocity where the attraction force balances the drag. This is the same friction concept we'll dig deeper into when we explore physics simulation properly.
The EXPLODE state automatically transitions back to IDLE after 60 frames (about a second). The state machine handles this timeout -- no external timer needed, just a frame counter check inside the state's update logic. Click to attract, click again to explode, wait a second and you're back to calm noise drift. The flow between states gives the piece a rhythm that pure generative work doesn't have.
State machines and narrative
This is the part I want you to think about creatively, not just technically. A pure generative noise field has no beginning, middle, or end -- it just runs. A state machine gives your piece temporal structure. Tension builds in one state, something changes, things settle into a new equilibrium. That's the difference between a screensaver and a performance piece.
Think about music. A song doesn't stay at one intensity. It has verses, choruses, bridges, breakdowns. Each section has different energy, different instruments, different dynamics. The transitions between sections are where the emotional impact lives -- the drop after a build-up, the quiet moment after the chorus. State machines let you do the same thing with visuals.
Professional creative coding performances almost always use state machines under the hood. The performer triggers state transitions (with a MIDI controller, keyboard, or even gesture detection), and each state has different visual rules. The machine handles the rendering, the human handles the dramaturgy. It's a partnership between code and creative intent.
For generative art pieces that run unattended (installations, screen art, NFT editions), you can use the Timeline class to choreograph transitions automatically. State A runs for 30 seconds, dissolves into state B, which runs for 20 seconds, transitions into state C, which loops back to A. The piece has rhythm even without interaction.
Adding a state stack
What if you want to overlay states instead of replacing them? A pause menu drawn on top of the running animation. A settings panel that doesn't destroy the current scene. A notification that appears briefly and goes away. For this, use a state stack instead of a single state variable:
let stateStack = [];
function pushState(name) {
let s = states[name];
if (s.enter) s.enter();
stateStack.push(s);
}
function popState() {
let s = stateStack.pop();
if (s && s.exit) s.exit();
}
function draw() {
// update and draw all states in the stack, bottom to top
for (let s of stateStack) {
s.update();
s.draw();
}
}
function keyPressed() {
if (key === 'p') {
if (stateStack.length > 1 &&
stateStack[stateStack.length - 1] === states.pause) {
popState();
} else {
pushState('pause');
}
}
}
The bottom of the stack is your main animation. Press P and a pause state gets pushed on top -- it draws over the main animation (maybe a semi-transparent overlay with "PAUSED" text). Press P again and it pops off, revealing the animation underneath which has been running the whole time.
Games use this pattern extensively. The pause menu sits on top of the game state. An inventory screen sits on top of the pause. A confirmation dialog sits on top of the inventory. Each layer renders in order, and popping returns you to the previous context without losing anything. For creative coding installations that need multiple layers of interaction, it's perfect.
Combining states with easing
Here's where last episode and this one snap together. State transitions are the when and what. Easing is the how. A state machine decides "we're moving from intro to scene1." Easing decides whether that transition feels snappy, smooth, bouncy, or dramatic.
let transitionProgress = 0;
let transitionActive = false;
let fromState = null;
let toState = null;
function beginTransition(from, to, duration) {
fromState = states[from];
toState = states[to];
transitionProgress = 0;
transitionActive = true;
if (toState.enter) toState.enter();
}
function easeInOutCubic(t) {
return t < 0.5
? 4 * t * t * t
: 1 - Math.pow(-2 * t + 2, 3) / 2;
}
function draw() {
if (transitionActive) {
transitionProgress += 0.02;
if (transitionProgress >= 1) {
transitionProgress = 1;
transitionActive = false;
currentState = toState;
}
let eased = easeInOutCubic(transitionProgress);
// cross-fade using global alpha
fromState.update();
toState.update();
fromState.draw();
ctx.save();
ctx.globalAlpha = eased;
toState.draw();
ctx.restore();
} else if (currentState) {
currentState.update();
currentState.draw();
}
}
The linear transitionProgress gets shaped by easeInOutCubic before being used as the alpha blend. The transition starts slow, accelerates through the middle, and decelerates at the end. Polished and cinematic. Swap the easing function for easeOutElastic from episode 16 and the transition wobbles into place. Swap it for easeOutBounce and it bounces. Same state machine, same transition logic, completly different feel -- just by changing one function.
Why bother with all this structure?
You might think: can't I just use a bunch of if/else statements? For two states, sure. But creative projects grow. I've worked on interactive installations at work that started with idle and active, then grew to need loading, error, tutorial, active, paused, and outro states. Without a state machine, the draw loop was 200 lines of nested conditionals and nobody could follow it. We refactored into a state machine in about an hour and suddenly the code was readable, extensible, and bug-free.
State machines give you separation of concerns. Each state's behavior is self-contained -- you can develop and test states independently. Adding a new state doesn't require touching existing state code. Transitions are explicit and traceable. And you can build reusable transition effects (fade, wipe, dissolve, circle reveal) that work between any pair of states.
The overhead of setting up a state machine is five minutes. The time it saves you in debugging and extending is hours. It's one of those patterns that feels like overkill until you use it, and then you can never go back.
Thinking ahead
State machines are the skeleton that holds complex creative coding pieces together. Now that you have them, you can start thinking about sketches with real structure -- installations with phases, interactive pieces with multiple modes, generative art that evolves through distinct visual chapters.
And once you have states, you start wanting richer behavior within each state. Particles that bounce off walls with real physics. Objects connected by springs that stretch and snap back. Swarms that flock together and scatter apart. The forces inside each state get more interesting when you don't have to worry about managing the state transitions -- the state machine handles the macro structure so you can focus on the micro behavior.
There's also the question of making your visuals respond to external input in real time -- audio frequencies driving particle behavior, microphone input shaping the canvas. State machines give you the scaffolding to switch between different audio-reactive modes: bass-heavy visuals during the drop, ethereal noise during the ambient section, explosive particles during the breakdown.
But I'm getting ahead of myself. For now, practice building sketches with states. Start simple -- two states with a click to switch. Then add transitions. Then add a timeline. Then try the state stack pattern. Each layer of complexity pays for itself when your sketches start telling stories instead of just running :-)
't Komt erop neer...
- A state machine is just a variable tracking the current mode, plus rules for when to switch
- Use
enter(),update(),draw()per state for clean self-contained behavior - Transitions can cross-fade, wipe, pixelate, circle-reveal -- the transition IS the performance
- Apply easing functions to transition progress for polished, cinematic state changes
- The
Timelineclass sequences events based on elapsed time -- great for installations and loops - A transition table prevents impossible states by defining every valid path explicitly
- Shared physics + state-specific forces is a clean pattern for interactive particle sketches
- State stacks let you overlay states (pause menus, notifications) without destroying the scene underneath
- State machines turn screensavers into performances -- they give your code narrative structure
Phase 3 keeps rolling. We've got smooth motion (lerp and easing) and structured behavior (state machines). But the forces inside our states are still pretty basic -- constant velocities, simple attraction. What happens when particles are connected by elastic springs? When friction depends on velocity? When objects follow each other in flocking patterns? That's where things start to feel really physical and alive, and that's what we're digging into next.
Sallukes! Thanks for reading.
X