One of the best ways to start learning to code is by finding something you like, and recreating it. It’s not about making a perfect match, but learning how to take a complex system, break it down into parts you can handle, and then rebuild it. In this article we’ll be recreating this piece by Saskia Freeke, a creative coder and lecturer at Goldsmiths:

Saskia has been posting awesome daily work on Twitter, Instagram, and Tumblr for almost three years straight. If you’re not following her yet, do so now.

Setup

You won’t need any special libraries for this, so last week’s post on getting yourself a p5 environment should be enough to start out. If you’ve already got a setup you’re comfortable with, great. We’ll be using a lot of built-in p5 functions, as well as some basic JavaScript, like for loops. If you’re unfamiliar with either, follow along using the p5 reference: they have really great documentation on everything we’ll use here.

Let’s start out with a look at an empty sketch.js:

function setup() { } function draw() { } 1 2 3 4 5 6 7 function setup ( ) { } function draw ( ) { }

If this is your first time using p5.js, these are the two most important functions to know: whatever you put in setup runs one time, and then the draw loop begins. The draw loop will rerun over and over (up to 60 times a second) until the sketch stops—that means we can use it to animate!

We’ll fill out this empty sketch piece by piece until we’ve got something “close enough” to the animation above.

First: Break things down

This piece has a few things going on, so we’ll start with the simplest one: a circle that moves up and down, and changes size at a smooth rate.

1. In the setup function, create a canvas with width and height 500px.

function setup() { createCanvas(500, 500); } 1 2 3 function setup ( ) { createCanvas ( 500 , 500 ) ; }

2. In the draw loop, set the background color to something so we can see the canvas against the browser.

function draw() { background(127); } 1 2 3 function draw ( ) { background ( 127 ) ; }

3. Time to draw! Still in the draw loop, create x and y variables in the center of the canvas and draw an ellipse at x:y with width and height 10px. p5 knows what “width” and “height” are automatically.

function draw() { background(127); var x = width/2, y = height/2; ellipse(x, y, 10, 10); } 1 2 3 4 5 function draw ( ) { background ( 127 ) ; var x = width / 2 , y = height / 2 ; ellipse ( x , y , 10 , 10 ) ; }

4. Declare phase and speed variables at the top of the sketch to control the system. Set the speed to some small multiplier, and we’ll start the phase at 0. Now we’re going to start using some sine and cosine functions, but you won’t need to review your trigonometry to get the hang of them. Just know that we can use sine and cosine to make smooth oscillations between -1 and 1.

In the draw loop set phase to frameCount * speed , and add sin(phase) to the ellipse’s y position. Multiply by 50 to make the effect more visible. frameCount counts the number of frames that have passed, so you can use it to effect changes over time. You’ll find yourself using it pretty much whenever you make animations in p5.

var phase = 0, speed = 0.03; function setup() { createCanvas(500, 500); } function draw() { background(127); var x = width/2; var y = height/2 + sin(phase) * 50; phase = frameCount * speed; ellipse(x, y, 10, 10); } 1 2 3 4 5 6 7 8 9 10 11 12 13 var phase = 0 , speed = 0.03 ; function setup ( ) { createCanvas ( 500 , 500 ) ; } function draw ( ) { background ( 127 ) ; var x = width / 2 ; var y = height / 2 + sin ( phase ) * 50 ; phase = frameCount * speed ; ellipse ( x , y , 10 , 10 ) ; }

5. Declare a maxCircleSize variable and set it equal to 20. Back in the draw loop declare a sizeOffset variable of (cos(phase) + 1) * 0.5 . This math gives us a cosine wave based off our initial phase, moving between 0 and 1 (rather than -1 and 1). Finally, declare a circleSize variable, set to sizeOffset * maxCircleSize .

var maxCircleSize = 20 var phase = 0, speed = 0.03; function setup() { createCanvas(500, 500); } function draw() { background(127); var x = width/2; var y = height/2 + sin(phase) * 50; phase = frameCount * speed; var sizeOffset = (cos(phase) + 1) * 0.5; var circleSize = sizeOffset * maxCircleSize; ellipse(x, y, circleSize, circleSize); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var maxCircleSize = 20 var phase = 0 , speed = 0.03 ; function setup ( ) { createCanvas ( 500 , 500 ) ; } function draw ( ) { background ( 127 ) ; var x = width / 2 ; var y = height / 2 + sin ( phase ) * 50 ; phase = frameCount * speed ; var sizeOffset = ( cos ( phase ) + 1 ) * 0.5 ; var circleSize = sizeOffset * maxCircleSize ; ellipse ( x , y , circleSize , circleSize ) ; }

Click ‘play’ below to look at what we’ve got so far: [Ed. note: If the widget below isn’t working in your browser, head to the alpha p5 editor and paste the above code in to try it out.]

Pretty good! We’ve got one circle that moves up and down, changing size in the same way as the original. (You can continue coding in the widget above if you prefer.)

Next: Rebuild it

Everything in the original animation is built from circles like the one above, so now we can construct the system that makes it interesting.

6. Declare a numRows variable and set it equal to 10. In the draw loop, enclose the ellipse code in a for loop counting from 0 to numRows and add row * 10 to the y position.

var numRows = 10; function draw() { // ... for(var row = 0; row < numRows; row += 1) { var y = height/2 + row * 10 + sin(phase) * 50; ellipse(x, y, circleSize, circleSize); } } 1 2 3 4 5 6 7 8 9 10 var numRows = 10 ; function draw ( ) { // ... for ( var row = 0 ; row < numRows ; row += 1 ) { var y = height / 2 + row * 10 + sin ( phase ) * 50 ; ellipse ( x , y , circleSize , circleSize ) ; } }

7. Move the sizeOffset variable into the for loop as well. Subtract row / numRows from the phase in the cosine calculation of sizeOffset .

for(var row = 0; row < numRows; row += 1) { var y = height/2 + row * 10 + sin(phase) * 50; var sizeOffset = (cos(phase - row / numRows) + 1) * 0.5; ellipse(x, y, circleSize, circleSize); } 1 2 3 4 5 for ( var row = 0 ; row < numRows ; row += 1 ) { var y = height / 2 + row * 10 + sin ( phase ) * 50 ; var sizeOffset = ( cos ( phase - row / numRows ) + 1 ) * 0.5 ; ellipse ( x , y , circleSize , circleSize ) ; }

8. Declare a numCols variable and set it equal to 10. In the draw loop, wrap the rows for loop in another for loop counting from 0 to numCols . Set x to spread out columns across the canvas with a 50px margin on either side. These two loops in combination give us a “grid” of circles to work with. Later on we’ll refer to it as a “strand,” since we’ll need multiple to complete the sketch.

var numCols = 10; function draw() { // ... for(var col = 0; col < numCols; col += 1) { for(var row = 0; row < numRows; row += 1) { var x = map(col, 0, numCols, 50, width - 50); // ... } }; 1 2 3 4 5 6 7 8 9 10 11 var numCols = 10 ; function draw ( ) { // ... for ( var col = 0 ; col < numCols ; col += 1 ) { for ( var row = 0 ; row < numRows ; row += 1 ) { var x = map ( col , 0 , numCols , 50 , width - 50 ) ; // ... } } ;

9. In the draw loop, declare a colOffset variable and set it using a map , so it can offset our phase calculations based on column. Add colOffset to phase in the y and sizeOffset cosine calculations.

function draw() { // ... for(var col = 0; col < numCols; col += 1) { for(var row = 0; row < numRows; row += 1) { var colOffset = map(col, 0, numCols, 0, TWO_PI); var x = map(col, 0, numCols, 50, width - 50); var y = height/2 + row * 10 + sin(phase + colOffset) * 50; var sizeOffset = (cos(phase - (row 0.1) + colOffset) + 1) 0.5; ellipse(x, y, circleSize, circleSize); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 function draw ( ) { // ... for ( var col = 0 ; col < numCols ; col += 1 ) { for ( var row = 0 ; row < numRows ; row += 1 ) { var colOffset = map ( col , 0 , numCols , 0 , TWO_PI ) ; var x = map ( col , 0 , numCols , 50 , width - 50 ) ; var y = height / 2 + row * 10 + sin ( phase + colOffset ) * 50 ; var sizeOffset = ( cos ( phase - ( row 0.1 ) + colOffset ) + 1 ) 0.5 ; ellipse ( x , y , circleSize , circleSize ) ; } } }

10. Declare 2 color variables and set them equal to the colors in the original GIF with the color function. Call noStroke() in setup to remove to annoying outlines. In the draw loop, set the background to the background color in the gif, and set the fill with a lerpColor to smoothly interpolate a color for each row.

var colorA, colorB; function setup() { // ... noStroke(); colorA = color(253, 174, 120); colorB = color(226, 129, 161); } function draw() { background(4, 58, 74); // ... for(var col = 0; col < numCols; col += 1) { for(var row = 0; row < numRows; row += 1) { fill(lerpColor(colorA, colorB, row / numRows)); // ... } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var colorA , colorB ; function setup ( ) { // ... noStroke ( ) ; colorA = color ( 253 , 174 , 120 ) ; colorB = color ( 226 , 129 , 161 ) ; } function draw ( ) { background ( 4 , 58 , 74 ) ; // ... for ( var col = 0 ; col < numCols ; col += 1 ) { for ( var row = 0 ; row < numRows ; row += 1 ) { fill ( lerpColor ( colorA , colorB , row / numRows ) ) ; // ... } } }

11. One last for loop! Declare a numStrands variable and set it equal to 2. In the draw loop, enclose the columns loop in another for loop counting from 0 to numStrands . Inside the loop declare a strandPhase variable and set it equal to phase + map(strand, 0, numStrands, 0, TWO_PI) , then replace the phase variable with strandPhase in the y and sizeOffset cosine calculations.

var numStrands = 2; for(var strand = 0; strand < numStrands; strand += 1) { for(var col = 0; col < numCols; col += 1) { for(var row = 0; row < numRows; row += 1) { var strandPhase = phase + map(strand, 0, numStrands, 0, TWO_PI); var y = height/2 + row * 10 + sin(strandPhase + colOffset) * 50; var sizeOffset = (cos(strandPhase - (row / numRows) + colOffset) + 1) * 0.5; // ... } } } 1 2 3 4 5 6 7 8 9 10 11 12 var numStrands = 2 ; for ( var strand = 0 ; strand < numStrands ; strand += 1 ) { for ( var col = 0 ; col < numCols ; col += 1 ) { for ( var row = 0 ; row < numRows ; row += 1 ) { var strandPhase = phase + map ( strand , 0 , numStrands , 0 , TWO_PI ) ; var y = height / 2 + row * 10 + sin ( strandPhase + colOffset ) * 50 ; var sizeOffset = ( cos ( strandPhase - ( row / numRows ) + colOffset ) + 1 ) * 0.5 ; // ... } } }

“Final” Code

Below, we cleaned things up a little, and most importantly, moved most of the variable assignments to the setup function. You can play around with them and see how changing each one affects the whole system (you can also view it on OpenProcessing).

You’ll notice that it’s a little different from the original, but we did what we set out to do: figure out how each component works, and make it ourselves. Now we have a bigger toolset that we can apply to our own creative work. [Ed. note: If the widget below isn’t working in your browser, view the full source code on OpenProcessing.]

Many thanks to Saskia Freeke, both for making great art and letting us use her work here, and also to toolness on GitHub, who wrote the code that made these p5 widgets possible.

Once you're comfortable with what we've used above, these free online courses will help bring you to the next level:

Introduction to p5.js

LEARN MORE

The Nature of Code

LEARN MORE