Inheritance

Inheritance is a key concept in object-oriented programming that allows one class to derive or “inherit” properties and methods from another class. This promotes abstraction, enabling developers to model complex systems by focusing on shared characteristics. Combined with polymorphism, inheritance lets you extend or override behavior in subclasses, making your code more flexible and reusable.

In the example below, the NoteCell class inherits from the SoundCell class—originally implemented in the Classes chapter—adding its own unique property (color) and overriding the play and display methods. This approach is further explored in the Celtic handpan demo, showcasing how subclasses can build upon and customize base class functionality while retaining shared behavior.

(mouse press plays the note sound; key press picks a random note sound)

code
// Initialize an array to store NoteCell instances
let notes = [];
let pickedNote;

// Base class representing a generic sound object
class SoundCell {
  constructor(name, sound) {
    this.name = name; // Name of the sound object
    this.sound = sound; // Associated sound file
  }

  // Retrieve the name of the sound object
  getName() {
    return this.name;
  }

  // Play the associated sound file
  play() {
    this.sound.play();
  }

  // Stop the sound file if it is playing
  stop() {
    this.sound.stop();
  }

  // Toggle sound playback (play if stopped, stop if playing)
  toggle() {
    this.sound.isPlaying() ? this.sound.stop() : this.sound.play();
  }

  // Display a visual representation of the sound object
  display(cellLength, outline, outlineWeight) {
    push();
    noFill();
    stroke(outline);
    strokeWeight(outlineWeight);
    ellipseMode(CORNER);
    circle(0, 0, cellLength); // Draw a circle with the given dimensions
    pop();
  }
}

// Subclass representing a note with additional visual properties
class NoteCell extends SoundCell {
  constructor(note, sound, color) {
    super(note, sound); // Initialize base class properties
    this.color = color; // Color of the note
    this.alpha = 0; // Transparency level for visual effects
  }

  // Override the play method to include visual feedback
  play() {
    super.play(); // Call the parent class's play method
    this.alpha = 255; // Set alpha to maximum for the fade-out effect
  }

  // Override the display method to include color and manual fade-out effect
  display(cellLength, outline, outlineWeight) {
    push();
    // Reduce alpha manually each frame for a smooth fade-out effect
    this.alpha = max(0, this.alpha - 5); // Fade from 255 to 0 over time
    const hue = color(this.color);
    hue.setAlpha(this.alpha); // Apply alpha transparency to the color
    noStroke();
    fill(hue);
    ellipseMode(CORNER);
    circle(0, 0, cellLength); // Draw the note with its color and alpha effect
    // Call the parent class's display method
    super.display(cellLength, outline, outlineWeight);
    pop();
  }
}

// Load sound files and create NoteCell objects
async function setup() {
  createCanvas(400, 400); // Setup the canvas
  // Note names
  const names = ['A3', 'A4', 'B2', 'B3', 'B4', 
                 'CSharp4', 'D4', 'E4', 'FSharp3', 'FSharp4'];
  // Corresponding colors
  const colors = ['red', 'orange', 'yellow', 'lime', 'green',
                  'aqua', 'cyan', 'skyblue', 'blue', 'purple'];
  for (let i = 0; i < names.length; i++) {
    // Load the sound file for the note
    const sound = await loadSound('/sounds/' + names[i] + '.wav');
    const cellColor = colors[i]; // Get the color by index
    // Create and add a NoteCell instance
    notes.push(new NoteCell(names[i], sound, cellColor));
  }
  pickedNote = random(notes); // Select a random NoteCell from the notes array
}

// Render the currently picked note on the canvas
function draw() {
  background(0); // Clear the canvas with a black background
  pickedNote.display(width, 'red', 2); // Display the picked note
}

// Handle mouse press to play the currently picked note
function mousePressed() {
  pickedNote.play(); // Play the sound associated with the picked note
}

// Handle key press to select a new random note
function keyPressed() {
  pickedNote = random(notes); // Pick a new random note
}

Class Definition

Inheritance in JavaScript is implemented using the extends keyword, which creates a parent-child relationship between two classes. In this example:

  • SoundCell: The base class, defining shared properties (name, sound) and methods (play, stop, toggle, and display).
  • NoteCell: The subclass, which extends SoundCell by adding the color property and overriding the play and display methods.

The following class diagram highlights the relationship between SoundCell and NoteCell:

  classDiagram
direction BT
    class SoundCell {
        +String name
        +p5.SoundFile sound
        +String getName()
        +play()
        +stop()
        +toggle()
        +display(cellLength: Number, outline: String, outlineWeight: Number)
    }

    class NoteCell {
        +String color
        +Number alpha
        +play()
        +display(cellLength: Number, outline: String, outlineWeight: Number)
    }

    NoteCell --|> SoundCell

Constructor and Object Instantiation

The SoundCell class includes a constructor that initializes the name and sound properties. This ensures that each instance of SoundCell or its subclasses has these properties defined.

class SoundCell {
  constructor(name, sound) {
    this.name = name; // Set the name of the sound
    this.sound = sound; // Associate the sound file with this object
  }
  // Other methods omitted for brevity
}

The NoteCell subclass extends this functionality by introducing a color property and an alpha property for visual feedback. Its constructor uses super to call the SoundCell constructor, ensuring that the shared properties (name and sound) are properly initialized before adding its unique color property.

class NoteCell extends SoundCell {
  constructor(note, sound, color) {
    super(note, sound); // Call parent constructor to initialize name and sound
    this.color = color; // Assign the color property for this NoteCell
    this.alpha = 0; // Initialize alpha property for fade-out effect
  }
  // Other methods omitted for brevity
}

The setup function demonstrates how NoteCell objects are instantiated. It loads a list of note sounds and associates each with a corresponding color, creating a distinct NoteCell for each one. These objects are then stored in the global notes array for later use in the sketch.

async function setup() {
  createCanvas(400, 400); // Setup the canvas
  // Array of note names
  const names = ['A3', 'A4', 'B2', 'B3', 'B4', 
                 'CSharp4', 'D4', 'E4', 'FSharp3', 'FSharp4'];
  // Array of corresponding colors                 
  const colors = ['red', 'orange', 'yellow', 'lime', 'green',
                  'aqua', 'cyan', 'skyblue', 'blue', 'purple'];
  for (let i = 0; i < names.length; i++) {
    // Load the sound file for the current note
    const sound = await loadSound('/sounds/' + names[i] + '.wav');
    // Create and store a NoteCell instance
    notes.push(new NoteCell(names[i], sound, colors[i]));
  }
  // Select a random NoteCell instance for display and interaction
  pickedNote = random(notes);
}

This approach demonstrates how inheritance enables the creation of multiple NoteCell instances that share common functionality from the SoundCell class while adding unique features like color and fade-out visual feedback.

Method Overriding and Polymorphism

The NoteCell class demonstrates method overriding, allowing it to redefine behavior from the parent class:

  • The play method is extended to include visual feedback by updating the alpha property.
  • The display method is customized to incorporate the color and alpha properties, creating a fade-out effect as the sound plays.

These overrides enable polymorphism, allowing all SoundCell objects (including NoteCell instances) to be treated uniformly while invoking subclass-specific behavior at runtime.

Drawing

The draw function leverages the display method to visually represent the pickedNote instance on the canvas.
As with classes, it calls pickedNote.display, which dynamically updates the canvas based on the current state of the pickedNote object.

Here, the overridden display method in NoteCell adds color and transparency effects to the visual representation, creating a dynamic and interactive experience.

Interaction

Interaction remains similar to the implementation in classes, with mouse and keyboard events driving user input:

  • Mouse Interaction: Clicking the mouse triggers the play method, which starts the sound and sets the visual feedback (alpha).
  • Keyboard Interaction: Pressing any key selects a new random note from the notes array, allowing seamless interaction across multiple NoteCell instances.

This showcases polymorphism by invoking methods dynamically on the current pickedNote object, regardless of whether it’s a SoundCell or a NoteCell.

Further Exploration

  • Add a Toggle to Display Name: Add a feature to toggle the visibility of the name display, similar to how it’s implemented in the object literals and classes posts. This helps practice interacting with inherited methods and managing visibility of the name display dynamically.
  • Implement Additional Subclasses: Create new subclasses that inherit from SoundCell or NoteCell, each with unique properties and behaviors (e.g., VolumeCell to adjust volume dynamically).
  • Explore Method Overloading: Extend the play method in NoteCell to optionally take parameters (e.g., play(loop = false)), showcasing method overloading and runtime flexibility.
  • Experiment with Visual Effects: Modify the display method in NoteCell to include animations or visual feedback based on the sound amplitude or playback speed.

References

p5 API

  • createCanvas — Creates a drawing canvas on which all the 2D and 3D visuals are rendered in p5.js.
  • push — Saves the current drawing state (e.g., styles, transformations) to the stack.
  • pop — Restores the most recently saved drawing state from the stack.
  • map — Re-maps a number from one range to another, often used to scale values for visual or interactive elements.
  • noFill — Disables filling geometry shapes with color.
  • fill — Sets the fill color for shapes drawn after this function is called.
  • stroke — Sets the outline (stroke) color for shapes drawn after this function is called.
  • strokeWeight — Sets the width of the stroke used for lines and shape outlines.
  • ellipseMode — Changes the way the parameters of ellipse shapes are interpreted (e.g., center, corner).
  • circle — Draws a circle at a specified location and diameter.
  • loadSound — Loads an external sound file for playback in the sketch.

Further Reading

  1. JavaScript Inheritance — Learn how inheritance works in JavaScript.
  2. p5.js Sound Library — Official documentation for the sound library.