Classes
^ 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:
- a
class
definition, which describes the shape of the object, and - a
constructor ()
function, which returns an object of the shape described in theclass
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 ())
}