p5.js Beginner Article 10

Intro to Object-Oriented Programming

Use JavaScript classes to represent self-contained entities — the basis of every complex sketch.

⏱ 22 min read OOP classes objects constructor methods

The problem classes solve

Imagine 50 balls bouncing around the screen. Without classes, you need 50 separate variables for x, 50 for y, 50 for velocity… Managing that is impossible.

Arrays of plain objects help (we covered that in article 4), but they have a limitation: the behaviour (the update and draw code) lives separately from the data. As sketches grow, this separation becomes messy.

A class bundles data and behaviour into one reusable unit.

Defining a class

class Ball {
  constructor(x, y) {
    this.x  = x;
    this.y  = y;
    this.vx = random(-3, 3);
    this.vy = random(-3, 3);
    this.r  = random(8, 20);
    this.hue = random(360);
  }

  update() {
    this.x += this.vx;
    this.y += this.vy;

    if (this.x < this.r || this.x > width - this.r)  this.vx *= -1;
    if (this.y < this.r || this.y > height - this.r) this.vy *= -1;
  }

  draw() {
    colorMode(HSB, 360, 100, 100);
    fill(this.hue, 70, 90);
    noStroke();
    circle(this.x, this.y, this.r * 2);
  }
}

The constructor runs when you create a new instance. this refers to the specific instance being created or used.

Creating instances

let balls = [];

function setup() {
  createCanvas(700, 500);

  for (let i = 0; i < 40; i++) {
    balls.push(new Ball(random(width), random(height)));
  }
}

function draw() {
  background(20);

  for (let b of balls) {
    b.update();
    b.draw();
  }
}

new Ball(x, y) creates an instance. Each instance has its own independent copy of all properties.

Adding more methods

Methods can be anything — reactions to input, state checks, interactions with other objects:

class Ball {
  constructor(x, y) {
    this.x   = x;
    this.y   = y;
    this.vx  = random(-2, 2);
    this.vy  = random(-2, 2);
    this.r   = random(10, 25);
    this.hue = random(360);
    this.alive = true;
  }

  update() {
    this.x += this.vx;
    this.y += this.vy;

    if (this.x < this.r || this.x > width - this.r)  this.vx *= -1;
    if (this.y < this.r || this.y > height - this.r) this.vy *= -1;
  }

  isHovered() {
    return dist(mouseX, mouseY, this.x, this.y) < this.r;
  }

  onClick() {
    this.hue = (this.hue + 60) % 360;
  }

  draw() {
    colorMode(HSB, 360, 100, 100);
    let sat = this.isHovered() ? 100 : 70;
    fill(this.hue, sat, 90);
    stroke(this.hue, 50, 100, 50);
    strokeWeight(2);
    circle(this.x, this.y, this.r * 2);
  }
}

function mouseClicked() {
  for (let b of balls) {
    if (b.isHovered()) b.onClick();
  }
}

Inheritance — extending a class

One class can extend another, inheriting all its properties and methods:

class Particle extends Ball {
  constructor(x, y) {
    super(x, y);        // call the parent constructor
    this.life = 255;
    this.fade = random(2, 6);
  }

  update() {
    super.update();     // run the parent update
    this.life -= this.fade;
  }

  isDead() {
    return this.life <= 0;
  }

  draw() {
    colorMode(HSB, 360, 100, 100, 255);
    fill(this.hue, 70, 90, this.life);
    noStroke();
    circle(this.x, this.y, this.r * 2);
  }
}

Use super(args) to call the parent constructor, and super.method() to call a parent method.

A complete OOP sketch

class Bubble {
  constructor() {
    this.reset();
  }

  reset() {
    this.x    = random(width);
    this.y    = height + random(50);
    this.r    = random(6, 24);
    this.vy   = random(-0.5, -1.8);
    this.vx   = random(-0.3, 0.3);
    this.hue  = random(180, 260);
    this.life = 255;
  }

  update() {
    this.x    += this.vx;
    this.y    += this.vy;
    this.life -= 1.5;
  }

  isDead() {
    return this.life <= 0 || this.y < -this.r;
  }

  draw() {
    colorMode(HSB, 360, 100, 100, 255);
    noFill();
    stroke(this.hue, 60, 90, this.life);
    strokeWeight(1.5);
    circle(this.x, this.y, this.r * 2);

    // Highlight
    stroke(this.hue, 20, 100, this.life * 0.6);
    strokeWeight(1);
    arc(this.x - this.r * 0.3, this.y - this.r * 0.3,
        this.r * 0.8, this.r * 0.8, PI + 0.3, PI + 1.2);
  }
}

let bubbles = [];

function setup() {
  createCanvas(600, 700);
  for (let i = 0; i < 60; i++) {
    let b = new Bubble();
    b.y = random(height);  // start scattered, not just at the bottom
    bubbles.push(b);
  }
}

function draw() {
  background(10, 30, 60);

  for (let b of bubbles) {
    b.update();
    b.draw();
    if (b.isDead()) b.reset();
  }
}

When to use classes

Use a class when:

  • You need multiple instances of the same type of thing
  • Each instance has its own independent state
  • The update/draw logic is non-trivial

Don’t use a class just for grouping functions — a plain object or a module works better for that.


Key takeaways

  • A class bundles data (this.x, this.y) and behaviour (update(), draw()) into one unit
  • constructor() runs when new ClassName() is called; initialise all properties there
  • Each new call creates an independent instance with its own copy of all properties
  • Methods can check state, react to input, and interact with other objects
  • extends creates a subclass that inherits all parent properties and methods; super() calls the parent constructor or method
  • Arrays of class instances are the backbone of every complex p5.js sketch