Creating Patterns with Grids and Sine Waves in P5

03/03/2021

Continuously evolving your sketches

My very first post on this website explains how to create a arbitrarily sized grid of shapes, which I think teaches a very important concept, which is 'instancing'. Thinking in an object oriented manner can go a long way when creating sketches, and can lead to having code that follows a modular structure.

Once you code up a working sketch with such a structure, simply changing one parameter can lead to an entirely different behaviour. At that point you're not really 'coding' your sketch anymore, but you're 'playing' with it.

Sketch from Scratch

In this blog post I'd like to explain what's going on in this Sketch specifically:

It might not be very obvious, but the secret ingredient behind the motion in this sketch are trigonometric functions. I talk a little about trigonometric functions in this previous blog post.

Intuition behind the Sketch

Essentially, we're creating a grid of objects, each of which holds a number of parameters and display() function to draw itself to the canvas. The parameters include it's position and size, and optionally also it's color. The heavy lifting happens in the display() function. Since every rectangle is continuously redrawn, what if we were to change their size and color over time? We could simply do so by modulating the size attribute with a sine function. But first things first, let's create the grid.

The grid

For the sake of clarity I'll brush over the code for creating the grid, however a more detailed explanation can be found here. This snippet, that constitutes the largest part of the code, creates a 'Grid' object which holds an array of 'Square' objects. Invoking the display function of the Grid object will in turn call all the display functions of each Square object and draw them to the canvas, depending on it's position. The grid object takes care of positioning the Squares such that you can create grids with an arbitrary number of rows and columns:

class Square {
  constructor(px, py, s) {
    this.positionX = px;
    this.positionY = py;
    this.curvature = 1;
    this.size = s;
    this.c = 0;
  }
  
  display() {
    strokeWeight(3)
    rect(this.positionX - this.size / 2, this.positionY - this.size / 2,
          this.size, this.size, this.curvature)
  }
}

class SquareGrid {
  constructor() {
    this.squares = []
    this.gridWidth = 20;
    this.gridHeight = 5;
    this.squareSize = 20;
    this.spacing = 24;
    this.positionX = width / 2 - ((this.gridWidth - 1) * (this.spacing)) / 2;
    this.positionY = height / 2 - ((this.gridHeight - 1) * (this.spacing)) / 2;
    for (let i = 0; i < this.gridWidth; i++) {
      let row = []
      for (let j = 0; j < this.gridHeight; j++) {
        row.push(
          new Square((this.positionX + this.spacing * i), (this.positionY + this.spacing * j), this.squareSize)
        )
      }
      this.squares.push(row)
    }
  }

  display() {
    for (let i = 0; i < this.gridWidth; i++) {
      for (let j = 0; j < this.gridHeight; j++) {
        this.squares[i][j].display()
      }
    }
  }
}

function setup() {
  createCanvas(500, 200);
  grid = new SquareGrid()
}


function draw() {
  background(255);
  grid.display();  
}

Modulating the Rectangle's size

As I already mentioned, since the rectangles are continuously redrawn, it would be interesting to plug something more interesting into it's 'this.size' attribute, rather than just a fixed number. For example, a sine function! We also need the millis() function that p5js provides to get motion out of the sin() function. The sine function now start oscillating between it's two bounds, that are -1 and 1, and we need to scale it accordingly to obtain any visual change, in this case I just multiply it by 14, which is adequate for the canvas and rectangle size we are using. You might have to change this number if you change the canvas size and the overall size of you drawn objects. Additionally I divide millis() by a value, which essentially just slows down the osciallting movement of the sine function. Otherwise it would be nauseatingly fast.


class Square {
  constructor(px, py, s) {
    this.positionX = px;
    this.positionY = py;
    this.curvature = 1;
    this.size = s;
    this.c = 0;
  }
  
  display() {
    strokeWeight(3);
    
    // This Line is all that changed
    this.size = 20 + 14 * sin(millis() / 500);
    rect(this.positionX - this.size / 2, this.positionY - this.size / 2, this.size, this.size, this.curvature, this.curvature, this.curvature, this.curvature);
  }
}

Sine function with respect to x coordinate

Here's where things become interesting, what if we were to plug in another attribute into the sine function? For instance, the x coordinate of the drawn object? This would offset the sine function by some amount, and the size of each one of the rectangles wouldn't uniformly increasing and decreasing. Here you can see the effect:

display() {
    strokeWeight(3);
    
    // This Line is all that changed
    this.size = 20 + 14 * sin(this.positionX + millis() / 500);
    rect(this.positionX - this.size / 2, this.positionY - this.size / 2, this.size, this.size, this.curvature, this.curvature, this.curvature, this.curvature);
}

Sine function with respect to y coordinate

What about the Y coordinate?

this.size = 20 + 14 * sin(this.positionY + millis() / 500);

Sine function with respect to x and y coordinate

What if we were to use both X and Y coordinates?

this.size = 20 + 14 * sin(this.positionY +this.positionX + millis() / 500);

Adding Color

Now we can apply this same concept to the color of each object. And we're not just limited to the sine function, we could alternatively use a cosine function or a combination of the two. The possibilities really are endless!

fill(127.5 + 127.5 * cos(this.positionX + tan(millis() / 5000) + millis() / 500), 127.5 + 127.5 * sin(this.positionY + cos(millis() / 500) + millis() / 500), 120.5)

This is it for today's blog post, if you have any questions leave a comment, or shoot me a message directly on any of my social medias! If you enjoyed this article, consider subscribing to my mailing list, or if you'd like to support this website, consider sharing this article.