a pipe shaped cookie cutter

^ ceci n’est pas un objet

Classes are like cookie cutters – they describe the shape of an object, and they are useful because they allow you to make objects of that shape very easily.

The way they do this is by having:

  1. a class definition, which describes the shape of the object, and
  2. a constructor () function, which returns an object of the shape described in the class definition.

Objects - a brief revision

Because they bundle information (properties) and behaviour (methods) together in the one place, using objects can be an invaluable way to keep your code organised and simple to understand.

Consider the following sketch:

const square_size = 100

let y_acc = 1
let y_vel = 0

let x_pos
let y_pos = 10

function setup () {
  createCanvas (window.innerWidth, window.innerHeight)
  x_pos = (width / 2) - (square_size / 2)
  noStroke ()
}

function draw () {
  background (`turquoise`)
  
  fill (`hotpink`)
  square (x_pos, y_pos, square_size)
  
  y_vel += y_acc
  y_pos += y_vel
  
  if (y_pos > height - square_size) {
    y_pos = height - square_size
    y_vel *= -1
  }
}

All of the data about the bouncing square, ie. its position, velocity, etc. we are holding in global variables.

Compare this with the following example, which uses the same data, but organises it in an object, which we are defining using object literal syntax, and assigning to the bouncer variable:

// declaring an object
// using object literal syntax
const bouncer = {
  
  // defining object properties
  // seperated by commas
  square_size : 100,
  y_acc : 2,
  y_vel : 0,
  y_pos : 10,
  x_pos : window.innerWidth / 2 - 50,
  
  // defining object methods
  // do_physics, check_collision, & draw
  // seperated by commas
  do_physics () {
    
    // properties of the object can be
    // referred to using 'this' keyword
    this.y_vel += this.y_acc
    this.y_pos += this.y_vel    
  },
  
  check_collision () {
    if (this.y_pos > height - this.square_size) {
      this.y_pos = height - this.square_size
      this.y_vel *= -1
    }
  },
  
  draw () {
    fill (`hotpink`)
    square (this.x_pos, this.y_pos, this.square_size)
  }
}

function setup () {
  createCanvas (window.innerWidth, window.innerHeight)  
  noStroke ()
}

function draw () {
  background (`turquoise`)
  
  // calling the various methods
  // on the bouncer object
  bouncer.do_physics ()
  bouncer.check_collision ()
  bouncer.draw ()
}

Note that when specifying properties inside the object literal, we are assigning values to property names (or keys), using a colon : rather than an equals sign, and seperating them using a comma ,.

Also note the use of mathematical assignment operators such as += and *=. These operators apply the mathematical operation to the two terms, and then assign the result to the variable on the left hand side of the operator. Ie.:

this.y_vel += this.y_acc

… is equivalent to:

this.y_vel = this.y_vel + this.y_acc

Similarly, this:

this.y_vel *= -1

… is equivalent to:

this.y_vel = this.y_vel * -1


Using an object in this way helps us organise our code, drastically neatening up what we need to include in the draw () function:

function draw () {
  background (`turquoise`)  
  bouncer.do_physics ()
  bouncer.check_collision ()
  bouncer.draw ()
}

In this sketch, which is quite simple, this reorganisation might not seem so important. However, when the code starts to become more complicated, and you are trying to keep track of several sets of data, things can become very messy very quickly, and the organisational affordances of javascript objects become highly valuable.

Classes

Using class definitions can be a powerful paradigm for creative coding, as it allows you to leverage the convenience of objects, en masse.

We can recreate the same sketch one more time, this time using classes.

Assuming our p5 sketch’s index.html looks something like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.1/p5.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css">
    <meta charset="utf-8" />

  </head>
  <body>
    <main>
    </main>
    <script src="bouncer.js"></script>
    <script src="sketch.js"></script>
  </body>
</html>

Note the reference to bouncer.js before the reference to sketch.js. This is where we put our class definition:

// class definitions look like
// function definitions
// but without the parentheses ()
// and with a capital first letter
class Bouncer {
  
  // this constructor method
  // takes 3 arguments:
  // x, y, and length
  constructor (x, y, length) {
    
    // the argument x, is assigned
    // to the property x_pos 
    this.x_pos = x
    
    // the argument y, is assigned
    // to the property y_pos
    this.y_pos = y
    
    // values are assigned to
    // the y_vel and y_acc properties
    this.y_vel = 0
    this.y_acc = 2
    
    // the argument length, is 
    // assigned to the size property
    this.size  = length
  }

  // the physics engine lives in
  // this do_physics method
  do_physics () {    
    this.y_vel += this.y_acc
    this.y_pos += this.y_vel    
  }

  // the collision detection behaviour
  // is defined in this method
  check_collision () {
    if (this.y_pos > height - this.size) {
      this.y_pos = height - this.size
      this.y_vel *= -1
    }
  }

  // this method draws the square
  draw () {
    fill (`hotpink`)
    square (this.x_pos, this.y_pos, this.size)
  }
}

Now we can use this Bouncer class in our sketch.js file:

const x_mid = window.innerWidth / 2

// we can call the class constructor
// by using the "new" keyword
// and then passing in the values we want
// the constructor to use in fashioning
// our Bouncer object
const bouncer = new Bouncer (x_mid - 50, 10, 100)

function setup () {
  createCanvas (window.innerWidth, window.innerHeight)  
  noStroke ()
}

function draw () {
  background (`turquoise`)  
  bouncer.do_physics ()
  bouncer.check_collision ()
  bouncer.draw ()
}

At this point, the amount of code we have used for each approach has been fairly similar. The difference is, when using classes, there is hardly any extra code needed to instantiate an arbitrary number of them.

Consider the following sketch, which reuses the Bouncer class definition from the previous example:

const bouncers = []

const w = window.innerWidth / 25
const h = window.innerHeight / 30
for (let i = 1; i < 25; i++) {
  bouncers.push (new Bouncer (i * w, i * h, 20))
  
}

function setup () {
  createCanvas (window.innerWidth, window.innerHeight) 
  noStroke ()
}

function draw () {
  background (`turquoise`)  
  bouncers.forEach (b => b.do_physics ())
  bouncers.forEach (b => b.check_collision ())
  bouncers.forEach (b => b.draw ())
}