System

This interactive sketch simulates an airplane flying across the screen that drops supply boxes with parachutes at regular intervals. Players click or tap falling supplies to collect them and increment a food counter, while particle trails and synthesized sound effects create an engaging, gamified experience.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make supplies drop twice as fast — Halving dropInterval makes supplies appear twice per second instead of every 4 seconds, making the game more challenging
  2. Paint the airplane blue — Change the RGB color values from gray to bright blue, instantly recoloring the airplane
  3. Make supplies fall in super slow motion — Reducing the gravity value makes supplies drift down gently instead of plummeting, giving you more time to collect them
  4. Emit more particle trail — Lowering the emission frequency from 5 to 2 creates a denser, more visible smoke trail behind the airplane
  5. Collect 10 supplies per click instead of 1 — Changing foodCount++ to foodCount += 10 makes each collection instantly award 10 food instead of 1
  6. Make the parachute bright yellow — Bright yellow parachutes are more visually striking and easier to spot
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a playable game where an airplane continuously flies across the screen, dropping supplies with parachutes that fall toward the ground. You collect supplies by clicking or tapping them, and each collection increments a food counter. The sketch demonstrates several core p5.js techniques: object-oriented design with custom classes, vector-based physics and collision detection, particle systems for visual effects, and procedural audio using p5.sound oscillators to create engine hums, drop sounds, and collection beeps.

The code is organized into a setup() and draw() loop that manage the overall game state, plus four ES6 classes—Airplane, SupplyDrop, Particle, and implicit user interaction—that encapsulate the logic for each game element. By studying this sketch, you will learn how to structure a multi-element interactive experience, manage arrays of dynamic objects, detect collisions between the mouse and falling items, and layer synthesized sounds on top of visuals to create a complete sensory experience.

⚙️ How It Works

  1. When the sketch loads, preload() initializes three p5.Oscillator objects (sine wave for engine sound, triangle for drop sound, square for collect sound) with different frequencies and amplitudes. setup() creates the canvas, spawns a single Airplane instance, and calls userStartAudio() to enable sound playback after the first user interaction.
  2. Every frame, draw() updates the airplane's position and draws it, then emits particles from its rear every 5 frames to create a trailing smoke effect. The particle system loops backward through the particles array, updating each one's position and fading its alpha value until it dies and is removed.
  3. Every 4 seconds (controlled by dropInterval), a new SupplyDrop is created at the airplane's position. Each drop has a parachute, string connections, and a box labeled 'FOOD' that falls downward under gravity, with velocity capped at 5 pixels per frame to simulate air resistance.
  4. When you click or touch the canvas, handleTouchOrClick() loops through all falling supplies and checks if your click was inside any drop's bounding box using the isCollected() method. If a hit is detected, the drop's state changes to 'collected', the food counter increments, and a collect sound plays with a fade-in and fade-out effect.
  5. Collected or off-screen supplies are removed from the supplyDrops array by looping backward and using splice(). This backward loop is essential to prevent index-skipping bugs when removing items during iteration.
  6. The screen displays the food counter in the top-left corner, updating in real-time as you collect supplies. Resizing the window calls windowResized() to maintain a responsive canvas and reset the airplane position.

🎓 Concepts You'll Learn

Object-oriented design with ES6 classesVector physics and velocityParticle systems and trailsCollision detection with bounding boxesArray management with backward loopsMouse and touch input handlingProcedural audio with p5.sound oscillatorsState management (falling vs. collected)Canvas resizing and responsive design

📝 Code Breakdown

preload()

preload() runs before setup() and is the ideal place to load or initialize resources like sounds, images, and fonts. p5.Oscillator lets you create procedural audio by specifying waveform shape, frequency, and amplitude. The three waveforms here (sine, triangle, square) each have a distinct sonic character.

function preload() {
  // Setup simple oscillators for sound effects
  // These will be replaced with actual sound files if you have them
  airplaneEngineSound = new p5.Oscillator('sine');
  airplaneEngineSound.freq(220); // Low hum
  airplaneEngineSound.amp(0.1);
  airplaneEngineSound.stop(); // Stop initially

  dropSound = new p5.Oscillator('triangle');
  dropSound.freq(440); // Beep
  dropSound.amp(0.2);
  dropSound.stop(); // Stop initially

  collectSound = new p5.Oscillator('square');
  collectSound.freq(880); // Higher beep
  collectSound.amp(0.3);
  collectSound.stop(); // Stop initially
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Engine sound oscillator setup airplaneEngineSound = new p5.Oscillator('sine');

Creates a sine wave oscillator for the airplane's engine sound

calculation Drop sound oscillator setup dropSound = new p5.Oscillator('triangle');

Creates a triangle wave oscillator for supply drop notification

calculation Collect sound oscillator setup collectSound = new p5.Oscillator('square');

Creates a square wave oscillator for successful collection feedback

airplaneEngineSound = new p5.Oscillator('sine');
Creates a new oscillator object using a sine wave shape, which produces a smooth, pure tone
airplaneEngineSound.freq(220);
Sets the oscillator frequency to 220 Hz, a low A note that sounds like an engine hum
airplaneEngineSound.amp(0.1);
Sets the amplitude (volume) to 0.1, keeping it quiet so it doesn't overwhelm other sounds
airplaneEngineSound.stop();
Stops the oscillator initially—it will be started later in setup() after user interaction
dropSound = new p5.Oscillator('triangle');
Triangle waves have a brighter, woodier tone than sine waves, creating a distinct drop notification sound
dropSound.freq(440);
Sets frequency to 440 Hz, the musical note A4, exactly one octave higher than the engine sound
collectSound = new p5.Oscillator('square');
Square waves are bright and buzzy, perfect for quick feedback beeps when you collect a supply
collectSound.freq(880);
Sets frequency to 880 Hz, two octaves above the engine—the highest pitch makes success feel rewarding

setup()

setup() runs once when the sketch starts. Use it to initialize the canvas, create objects, and set up global state. The windowWidth and windowHeight variables are built into p5.js and automatically reflect the current window dimensions.

function setup() {
  createCanvas(windowWidth, windowHeight);
  airplane = new Airplane();
  foodCount = 0;

  // Start audio on user gesture (mandatory for browsers)
  // Audio will start playing after the first click/touch on the canvas
  userStartAudio();
  airplaneEngineSound.start();
  // airplaneEngineSound.loop(); // REMOVED: Oscillators are continuous by default

  // Initialize particles array (particles will be emitted by the airplane)
  particles = [];
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Canvas initialization createCanvas(windowWidth, windowHeight);

Creates a full-window canvas that scales to fit the device screen

calculation Audio permission and startup userStartAudio();

Prompts the browser to enable audio playback on first user interaction (required by modern browsers)

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire window; this makes the game responsive across devices
airplane = new Airplane();
Instantiates the Airplane class, creating a single airplane object that will fly across the screen
foodCount = 0;
Initializes the food counter to zero at the start of the game
userStartAudio();
Calls p5.sound's function to request browser permission for audio playback—required before playing any oscillators
airplaneEngineSound.start();
Starts the engine oscillator, which will play continuously once audio is permitted
particles = [];
Initializes an empty array to hold particle objects that will be created and destroyed during gameplay

draw()

draw() runs 60 times per second and is where the game's main loop happens. Notice the three backward for-loops for safely removing items from arrays while iterating. This pattern is crucial in interactive sketches where objects spawn and die each frame. Also notice the semi-transparent background trick: instead of clearing completely with background(20, 20, 30, 255), using alpha 25 leaves fading trails—a simple but powerful visual technique.

🔬 This code emits a particle every 5 frames. What happens if you change the 5 to a 2 or a 10? What visual change do you see?

  if (frameCount % 5 === 0) { // Emit a particle every 5 frames
    let particlePos = createVector(airplane.pos.x - airplane.size * 0.8, airplane.pos.y + airplane.size * 0.1);
    particles.push(new Particle(particlePos.x, particlePos.y));
  }
function draw() {
  // Draw background with some transparency to create a trailing effect
  background(20, 20, 30, 25);

  // --- Airplane Logic ---
  airplane.update();
  airplane.display();

  // Emit particles from the airplane's rear as a trail
  if (frameCount % 5 === 0) { // Emit a particle every 5 frames
    let particlePos = createVector(airplane.pos.x - airplane.size * 0.8, airplane.pos.y + airplane.size * 0.1);
    particles.push(new Particle(particlePos.x, particlePos.y));
  }

  // Drop supply logic: Check if it's time to drop a new supply
  if (millis() - lastDropTime > dropInterval) {
    supplyDrops.push(new SupplyDrop(airplane.pos.x, airplane.pos.y));
    
    // Play drop sound with a quick fade-in and fade-out
    dropSound.start();
    dropSound.amp(0.2, 0.05); // Fade in to 0.2 amplitude in 0.05 seconds
    dropSound.amp(0, 0.5);   // Fade out to 0 amplitude in 0.5 seconds
    
    lastDropTime = millis();
  }

  // --- Supply Drops Logic ---
  // Loop through supply drops backwards to allow safe removal
  for (let i = supplyDrops.length - 1; i >= 0; i--) {
    let drop = supplyDrops[i];
    drop.update();
    drop.display();

    // Remove collected drops or drops that have fallen off-screen
    if (drop.state === 'collected' || drop.pos.y > height + drop.size) {
      supplyDrops.splice(i, 1);
    }
  }

  // --- Particle System Logic (Airplane Trail) ---
  // Loop through particles backwards to allow safe removal
  for (let i = particles.length - 1; i >= 0; i--) {
    let p = particles[i];
    p.update();
    p.display();
    if (!p.isAlive) {
      particles.splice(i, 1); // Remove dead particles
    }
  }

  // --- Food Counter Display ---
  fill(255); // White color for text
  textSize(24);
  textAlign(LEFT, TOP);
  text(`Food: ${foodCount}`, 10, 10);
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

calculation Semi-transparent background background(20, 20, 30, 25);

Clears most of the screen with a dark blue-gray but preserves fading trails from previous frames

conditional Particle emission timing if (frameCount % 5 === 0) { // Emit a particle every 5 frames

Emits a new particle every 5 frames instead of every frame to avoid overwhelming the particle count

conditional Supply drop spawn timing if (millis() - lastDropTime > dropInterval) {

Checks if enough milliseconds have passed since the last drop to spawn a new supply

for-loop Backward loop through supply drops for (let i = supplyDrops.length - 1; i >= 0; i--) {

Iterates through all supplies from end to start, enabling safe removal with splice()

for-loop Backward loop through particles for (let i = particles.length - 1; i >= 0; i--) {

Iterates through all particles from end to start, enabling safe removal when dead

calculation Food counter display text(`Food: ${foodCount}`, 10, 10);

Renders the current food count in the top-left corner of the canvas

background(20, 20, 30, 25);
Fills the background with a dark blue-gray color (RGB 20,20,30), and the fourth argument (25) is the alpha value—it's semi-transparent, so previous frames slightly show through, creating motion trails
airplane.update();
Calls the airplane's update method, which moves it based on its velocity and wraps it around the screen
airplane.display();
Calls the airplane's display method, which draws all of its visual parts (body, wings, cockpit, tail) at its current position
if (frameCount % 5 === 0) { // Emit a particle every 5 frames
frameCount is the number of frames drawn so far; this condition is true every 5 frames, limiting particle creation to avoid performance issues
let particlePos = createVector(airplane.pos.x - airplane.size * 0.8, airplane.pos.y + airplane.size * 0.1);
Calculates the position slightly behind and below the airplane's center where the particle trail should originate
particles.push(new Particle(particlePos.x, particlePos.y));
Creates a new Particle at the calculated position and adds it to the particles array
if (millis() - lastDropTime > dropInterval) {
millis() returns total milliseconds since the sketch started; this checks if enough time has passed since the last drop
supplyDrops.push(new SupplyDrop(airplane.pos.x, airplane.pos.y));
Creates a new SupplyDrop at the airplane's current position and adds it to the supplyDrops array
dropSound.start();
Starts playing the drop sound oscillator, triggering the notification beep
dropSound.amp(0.2, 0.05); // Fade in to 0.2 amplitude in 0.05 seconds
Gradually increases the volume from 0 to 0.2 over 0.05 seconds, creating a smooth fade-in effect
dropSound.amp(0, 0.5); // Fade out to 0 amplitude in 0.5 seconds
Gradually decreases the volume from 0.2 to 0 over 0.5 seconds, creating a smooth fade-out effect
lastDropTime = millis();
Records the current time in milliseconds, resetting the timer for the next supply drop
for (let i = supplyDrops.length - 1; i >= 0; i--) {
Loops backward through the supplyDrops array (from last to first), essential for safely removing items with splice()
if (drop.state === 'collected' || drop.pos.y > height + drop.size) {
Removes a supply if it has been collected by the player OR if it has fallen below the bottom of the canvas
for (let i = particles.length - 1; i >= 0; i--) {
Loops backward through the particles array, again enabling safe removal of dead particles
fill(255); // White color for text
Sets the fill color to white for all subsequent text drawn
text(`Food: ${foodCount}`, 10, 10);
Displays the current food count as a string at position (10, 10), using template literal syntax to insert the foodCount variable

mousePressed()

mousePressed() is a built-in p5.js callback that fires on every mouse click. By delegating to handleTouchOrClick(), we avoid duplicating code for both mouse and touch input.

function mousePressed() {
  handleTouchOrClick();
}
Line-by-line explanation (2 lines)
function mousePressed() {
This is a p5.js built-in function that is called automatically whenever the mouse is clicked on the canvas
handleTouchOrClick();
Delegates to a shared helper function so mouse clicks and touch events use the same collection logic

touchStarted()

touchStarted() is the mobile/tablet equivalent of mousePressed(). Returning false prevents the browser's default touch behaviors (scrolling, zooming) so the touch only affects your sketch. This is standard practice for touch-based canvas games.

function touchStarted() {
  handleTouchOrClick();
  return false; // Prevent default touch behavior (like scrolling)
}
Line-by-line explanation (3 lines)
function touchStarted() {
This is a p5.js built-in function that is called automatically whenever a touch begins on the canvas (mobile/tablet)
handleTouchOrClick();
Calls the same helper function used by mousePressed(), unifying desktop and mobile input
return false; // Prevent default touch behavior (like scrolling)
Returning false stops the browser from treating the touch as a scroll or page swipe, keeping focus on the game

handleTouchOrClick()

This function is the core of the game mechanic: it handles collision detection between the player's click/touch and falling supplies. The isCollected() method in the SupplyDrop class performs axis-aligned bounding-box (AABB) collision—a simple but fast way to check if a point (the mouse) is inside a rectangle (the drop's box). The break statement ensures only one drop is collected per interaction, keeping gameplay balanced.

function handleTouchOrClick() {
  for (let drop of supplyDrops) {
    // Check if the drop is currently falling and if the mouse/touch is over it
    if (drop.state === 'falling' && drop.isCollected(mouseX, mouseY)) {
      drop.state = 'collected'; // Mark as collected
      foodCount++;             // Increment food count
      
      // Play collect sound with a quick fade-in and fade-out
      collectSound.start();
      collectSound.amp(0.3, 0.05); // Fade in to 0.3 amplitude in 0.05 seconds
      collectSound.amp(0, 0.5);    // Fade out to 0 amplitude in 0.5 seconds
      
      break; // Only collect one drop per click/touch
    }
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Loop through supply drops for (let drop of supplyDrops) {

Iterates through each supply drop to check if it was clicked

conditional Collision and state check if (drop.state === 'falling' && drop.isCollected(mouseX, mouseY)) {

Ensures the drop is falling and the click/touch is inside its bounding box before collecting

calculation Collect sound with fade collectSound.start();

Provides auditory feedback when the player successfully collects a supply

for (let drop of supplyDrops) {
Loops through each SupplyDrop object in the supplyDrops array (using for-of syntax, which is simpler than indexed loops)
if (drop.state === 'falling' && drop.isCollected(mouseX, mouseY)) {
Checks two conditions: (1) the drop is still falling (not already collected), and (2) the mouse/touch position is inside the drop's bounding box
drop.state = 'collected'; // Mark as collected
Changes the drop's state property to 'collected', which signals the draw loop to stop drawing it and eventually remove it from the array
foodCount++; // Increment food count
Adds 1 to the foodCount variable, which increments the counter displayed on screen
collectSound.start();
Starts playing the collect oscillator, giving immediate audio feedback to the player
collectSound.amp(0.3, 0.05); // Fade in to 0.3 amplitude in 0.05 seconds
Quickly fades in the collect sound to full volume over 0.05 seconds, making it punchy and responsive
collectSound.amp(0, 0.5); // Fade out to 0 amplitude in 0.5 seconds
Fades out the collect sound over 0.5 seconds, creating a satisfying tail-off effect
break; // Only collect one drop per click/touch
Exits the loop immediately after collecting one drop, preventing a single click from collecting multiple supplies

windowResized()

windowResized() is a p5.js callback that fires automatically whenever the browser window is resized. It's essential for responsive sketches. Here, we not only resize the canvas but also reset the airplane position to ensure continuity when the screen dimensions change—a small detail that prevents visual glitches.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Reset airplane position on resize to ensure it starts properly
  airplane.pos.x = -airplane.size;
  airplane.pos.y = height * 0.2;
}
Line-by-line explanation (3 lines)
resizeCanvas(windowWidth, windowHeight);
Automatically adjusts the canvas size to match the new window dimensions whenever the browser window is resized
airplane.pos.x = -airplane.size;
Resets the airplane's x position to off-screen left so it re-enters the screen smoothly after a resize
airplane.pos.y = height * 0.2;
Resets the airplane's y position to 20% of the new window height, keeping it in the upper portion of the canvas

Airplane class

The Airplane class demonstrates the core pattern of game objects in p5.js: a constructor initializes properties, update() changes state (position), and display() renders visuals. The use of push/pop and translate is crucial—it lets you draw the airplane relative to its own position without affecting other objects on screen. The wrapping logic (checking if x > width and resetting) is a classic pattern for continuously looping objects.

🔬 These three shapes make up the airplane: body (ellipse), tail (triangle), wings (rect). What happens if you swap the order—draw the wings first, then the tail, then the body? Can you predict which shape will be drawn on top?

    // Body
    fill(this.color);
    noStroke();
    ellipse(0, 0, this.size, this.size * 0.4);

    // Tail fin
    triangle(-this.size * 0.4, 0, -this.size * 0.6, -this.size * 0.3, -this.size * 0.8, 0);

    // Wings
    rectMode(CENTER);
    rect(0, 0, this.size * 1.5, this.size * 0.2);
class Airplane {
  constructor() {
    this.size = 60; // Size reference for drawing
    // Start off-screen left, flying at 20% of screen height
    this.pos = createVector(-this.size, height * 0.2);
    this.vel = createVector(3, 0); // Constant speed flying right
    this.color = color(150, 150, 150); // Gray plane
  }

  update() {
    this.pos.add(this.vel);
    // Wrap around screen: if plane goes off right, reset to off-screen left
    if (this.pos.x > width + this.size) {
      this.pos.x = -this.size;
      this.pos.y = height * random(0.1, 0.4); // Vary flight height slightly
    }
  }

  display() {
    push();
    translate(this.pos.x, this.pos.y);

    // Body
    fill(this.color);
    noStroke();
    ellipse(0, 0, this.size, this.size * 0.4);

    // Tail fin
    triangle(-this.size * 0.4, 0, -this.size * 0.6, -this.size * 0.3, -this.size * 0.8, 0);

    // Wings
    rectMode(CENTER);
    rect(0, 0, this.size * 1.5, this.size * 0.2);

    // Cockpit
    fill(100, 100, 100);
    ellipse(this.size * 0.3, -this.size * 0.1, this.size * 0.3, this.size * 0.2);

    pop();
  }
}
Line-by-line explanation (19 lines)

🔧 Subcomponents:

calculation Constructor initialization constructor() {

Sets up all airplane properties: size, starting position, velocity, and color

calculation Update method with screen wrapping update() {

Moves the airplane and wraps it back to the left side when it exits the right edge

calculation Display method with transform stack display() {

Draws all airplane components (body, wings, tail, cockpit) using push/pop and translate

constructor() {
The constructor method runs once when you create a new Airplane() and initializes all instance properties
this.size = 60; // Size reference for drawing
Stores a size value that all other parts (wings, cockpit, tail) scale from, making the airplane easy to resize
this.pos = createVector(-this.size, height * 0.2);
Places the airplane off-screen to the left (negative x) and at 20% of the screen height (y), ready to fly in from the left
this.vel = createVector(3, 0); // Constant speed flying right
Sets a constant velocity of 3 pixels per frame horizontally (x direction) with no vertical movement (y = 0)
this.color = color(150, 150, 150); // Gray plane
Pre-creates a gray color value and stores it, so we don't have to recreate it every frame in display()
update() {
This method is called every frame to update the airplane's position and handle wrapping
this.pos.add(this.vel);
Adds the velocity vector to the position vector, moving the airplane 3 pixels to the right each frame
if (this.pos.x > width + this.size) {
Checks if the airplane has completely exited the right side of the canvas (position greater than width)
this.pos.x = -this.size;
Resets the airplane's x position to off-screen left, making it re-enter smoothly from the opposite side
this.pos.y = height * random(0.1, 0.4); // Vary flight height slightly
Randomly adjusts the airplane's y position to between 10% and 40% of screen height, adding variety to each pass
display() {
This method draws all the visual parts of the airplane at its current position
push();
Saves the current transform state (translation, rotation, scale) onto a stack, so changes only affect this airplane
translate(this.pos.x, this.pos.y);
Moves the origin (0, 0) to the airplane's position, so all subsequent drawing happens relative to the airplane
ellipse(0, 0, this.size, this.size * 0.4);
Draws the airplane's body as an ellipse: full width is this.size, height is 40% of that (a horizontal oval)
triangle(-this.size * 0.4, 0, -this.size * 0.6, -this.size * 0.3, -this.size * 0.8, 0);
Draws a triangular tail fin pointing backward (negative x direction) and upward from the airplane
rectMode(CENTER);
Switches rect() to CENTER mode so the rectangle is drawn from its center point instead of the top-left
rect(0, 0, this.size * 1.5, this.size * 0.2);
Draws the wings as a rectangle centered on the airplane, 1.5x wider and 20% as tall as the body
ellipse(this.size * 0.3, -this.size * 0.1, this.size * 0.3, this.size * 0.2);
Draws a small ellipse (cockpit/window) at the front of the airplane, in a darker gray
pop();
Restores the previous transform state, undoing the translate() so other objects draw normally

SupplyDrop class

The SupplyDrop class combines physics (gravity and velocity limiting), rendering (parachute, strings, box, text), and collision detection (isCollected). The state property demonstrates a common game pattern: objects can be in different states ('falling' or 'collected') and behave differently in each. The bounding-box collision detection is simple but effective—you could enhance it with circle collision or pixel-perfect detection for more complex shapes.

🔬 These three lines simulate gravity and air resistance. What happens if you remove the vel.limit(5) line so velocity keeps growing forever? How does the drop behave?

      this.vel.add(this.gravity);
      this.pos.add(this.vel);

      // Limit velocity to prevent drops from falling too fast (simulating air resistance)
      this.vel.limit(5);

🔬 This code draws the parachute and two strings. What if you added a third or fourth string down the middle, or changed the lines to diagonal? Try adding another line.

      // Parachute
      fill(this.parachuteColor);
      noStroke();
      arc(0, -this.size * 0.8, this.size * 2, this.size * 1.5, PI, TWO_PI); // Semi-circle

      // Strings connecting parachute to box
      stroke(0);
      strokeWeight(1);
      line(-this.size * 0.5, -this.size * 0.8, -this.size * 0.5, 0);
      line(this.size * 0.5, -this.size * 0.8, this.size * 0.5, 0);
class SupplyDrop {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.vel = createVector(0, 0);
    this.gravity = createVector(0, 0.1); // Simple downward gravity
    this.size = 30; // Size of the box
    this.state = 'falling'; // States: 'falling', 'collected'
    this.parachuteColor = color(255, 100, 100); // Red parachute
    this.boxColor = color(150, 100, 50);   // Brown box
  }

  update() {
    if (this.state === 'falling') {
      this.vel.add(this.gravity);
      this.pos.add(this.vel);

      // Limit velocity to prevent drops from falling too fast (simulating air resistance)
      this.vel.limit(5);
    }
  }

  display() {
    if (this.state === 'falling') {
      push();
      translate(this.pos.x, this.pos.y);

      // Parachute
      fill(this.parachuteColor);
      noStroke();
      arc(0, -this.size * 0.8, this.size * 2, this.size * 1.5, PI, TWO_PI); // Semi-circle

      // Strings connecting parachute to box
      stroke(0);
      strokeWeight(1);
      line(-this.size * 0.5, -this.size * 0.8, -this.size * 0.5, 0);
      line(this.size * 0.5, -this.size * 0.8, this.size * 0.5, 0);

      // Box
      fill(this.boxColor);
      noStroke();
      rectMode(CENTER);
      rect(0, 0, this.size, this.size);

      // "FOOD" label on the box
      fill(255);
      textSize(this.size * 0.4);
      textAlign(CENTER, CENTER);
      text("FOOD", 0, 0);

      pop();
    }
  }

  isCollected(mx, my) {
    // Check if the given mouse/touch coordinates are within the box
    return mx > this.pos.x - this.size / 2 &&
           mx < this.pos.x + this.size / 2 &&
           my > this.pos.y - this.size / 2 &&
           my < this.pos.y + this.size / 2;
  }
}
Line-by-line explanation (28 lines)

🔧 Subcomponents:

calculation Constructor setup constructor(x, y) {

Initializes all drop properties: position, velocity, gravity, size, colors, and state

conditional Physics update with velocity limiting update() {

Applies gravity to velocity, updates position, and caps speed to simulate air resistance

conditional Display with transform stack display() {

Draws parachute, strings, box, and FOOD label only when state is 'falling'

calculation Bounding box collision detection isCollected(mx, my) {

Returns true if the mouse/touch point (mx, my) is inside the drop's square bounding box

constructor(x, y) {
Constructor takes x and y parameters from the airplane's position when the supply is dropped
this.pos = createVector(x, y);
Stores the drop's position as a vector, starting at the airplane's coordinates
this.vel = createVector(0, 0);
Initializes velocity to zero; it will accumulate due to gravity as the drop falls
this.gravity = createVector(0, 0.1); // Simple downward gravity
Creates a constant downward acceleration of 0.1 pixels per frame per frame, simulating Earth's gravity
this.size = 30; // Size of the box
Stores the size of the supply box, which scales all the visual and collision components
this.state = 'falling'; // States: 'falling', 'collected'
Initializes state to 'falling'; when collected, it changes to 'collected' and stops rendering
this.parachuteColor = color(255, 100, 100); // Red parachute
Pre-creates a red color object for the parachute, avoiding unnecessary color() calls every frame
this.boxColor = color(150, 100, 50); // Brown box
Pre-creates a brown color object for the box body
update() {
This method updates the drop's physics every frame when it's in the 'falling' state
if (this.state === 'falling') {
Only update physics if the drop is currently falling (skip if already collected)
this.vel.add(this.gravity);
Adds the gravity vector to velocity each frame, making the drop accelerate downward over time
this.pos.add(this.vel);
Updates position by adding the velocity vector, moving the drop based on accumulated acceleration
this.vel.limit(5);
Caps the velocity magnitude at 5 pixels per frame, preventing the drop from falling impossibly fast (terminal velocity simulation)
display() {
This method draws the supply drop if it's still falling
if (this.state === 'falling') {
Only draw the drop if state is 'falling'; once collected, it stops rendering and is soon removed from the array
push();
Saves the current transform state so translate() only affects this drop
translate(this.pos.x, this.pos.y);
Moves the origin to the drop's position, so all drawing is relative to the drop
arc(0, -this.size * 0.8, this.size * 2, this.size * 1.5, PI, TWO_PI); // Semi-circle
Draws a red semi-circle (the parachute) centered above the drop; arc() with PI to TWO_PI draws the bottom half of a circle
line(-this.size * 0.5, -this.size * 0.8, -this.size * 0.5, 0);
Draws the left string connecting the parachute to the box
line(this.size * 0.5, -this.size * 0.8, this.size * 0.5, 0);
Draws the right string connecting the parachute to the box
rect(0, 0, this.size, this.size);
Draws the brown supply box as a square centered on the drop's position
text("FOOD", 0, 0);
Writes 'FOOD' in white text at the center of the box, making it clear what the supply is
pop();
Restores the previous transform state, undoing the translate()
isCollected(mx, my) {
This collision detection method takes mouse/touch coordinates and returns true if they're inside the box
return mx > this.pos.x - this.size / 2 &&
First condition: mouse x is to the right of the box's left edge
mx < this.pos.x + this.size / 2 &&
Second condition: mouse x is to the left of the box's right edge
my > this.pos.y - this.size / 2 &&
Third condition: mouse y is below the box's top edge
my < this.pos.y + this.size / 2;
Fourth condition: mouse y is above the box's bottom edge. All four must be true for a hit.

Particle class

The Particle class is a simple but powerful particle system. Each particle lives independently—it moves, fades, and dies—but the particles collectively create the airplane trail effect. By creating hundreds of particles over time and using the lifespan property as an alpha channel, we achieve a smooth, natural-looking motion trail. This is a foundational pattern used in games, VFX, and interactive art.

🔬 These two lines randomize each particle's velocity and size. What happens if you remove the random() calls and use fixed values instead? Try this.vel = createVector(-1, 0) and this.size = 3 for identical particles everywhere.

    this.vel = createVector(random(-1, 0), random(-1, 1));
    this.size = random(2, 5);
class Particle {
  constructor(x, y) {
    this.pos = createVector(x, y);
    // Particles move slightly backward and downward relative to the plane
    this.vel = createVector(random(-1, 0), random(-1, 1));
    this.size = random(2, 5);
    this.color = color(200, 200, 200); // Light gray for smoke/trail
    this.lifespan = 255; // Initial alpha value, decreases over time
    this.isAlive = true;
  }

  update() {
    this.pos.add(this.vel);
    this.lifespan -= random(3, 7); // Fade faster
    if (this.lifespan <= 0) {
      this.isAlive = false; // Mark particle for removal
    }
  }

  display() {
    if (this.isAlive) {
      noStroke();
      // Apply lifespan as alpha value
      fill(red(this.color), green(this.color), blue(this.color), this.lifespan);
      ellipse(this.pos.x, this.pos.y, this.size);
    }
  }
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

calculation Constructor with randomization constructor(x, y) {

Initializes each particle with random velocity, size, and full lifespan

calculation Update with fade and death update() {

Moves the particle, decreases its lifespan, and marks it dead when fully faded

conditional Display with alpha fade display() {

constructor(x, y) {
Constructor takes the airplane's position and creates a particle at that location
this.pos = createVector(x, y);
Stores the particle's starting position
this.vel = createVector(random(-1, 0), random(-1, 1));
Gives each particle a random velocity: x is between -1 and 0 (moving leftward), y is between -1 and 1 (any vertical direction)
this.size = random(2, 5);
Each particle has a random diameter between 2 and 5 pixels, creating visual variety in the trail
this.color = color(200, 200, 200); // Light gray for smoke/trail
Pre-creates a light gray color; the same gray is reused for all particles (only the alpha changes per particle)
this.lifespan = 255; // Initial alpha value, decreases over time
Starts at 255 (fully opaque) and decreases each frame until it reaches 0 (invisible), creating a fade effect
this.isAlive = true;
Marks the particle as alive; when lifespan reaches 0, isAlive is set to false for removal
update() {
This method updates the particle's position and lifespan every frame
this.pos.add(this.vel);
Moves the particle by its velocity vector
this.lifespan -= random(3, 7); // Fade faster
Decreases lifespan by a random amount between 3 and 7 each frame, creating variable fade rates
if (this.lifespan <= 0) {
Checks if the particle has faded completely (lifespan <= 0)
this.isAlive = false; // Mark particle for removal
Sets isAlive to false, signaling the draw loop to remove this particle from the particles array on the next iteration
display() {
This method renders the particle if it's still alive
if (this.isAlive) {
Only draw if alive; skip drawing for dead particles
noStroke();
Disables stroke, so particles are filled circles with no outline
fill(red(this.color), green(this.color), blue(this.color), this.lifespan);
Extracts the R, G, B values from the stored color and uses the particle's lifespan as the alpha channel for fading
ellipse(this.pos.x, this.pos.y, this.size);
Draws a circle at the particle's current position with its random size

📦 Key Variables

airplane object

Stores a single Airplane instance that flies across the screen and drops supplies

let airplane;
supplyDrops array

An array of SupplyDrop objects representing all currently falling supplies on screen

let supplyDrops = [];
foodCount number

Tracks the total number of supplies the player has collected, displayed as a counter

let foodCount = 0;
dropInterval number

The time in milliseconds between each supply drop (e.g., 4000 = one drop every 4 seconds)

let dropInterval = 4000;
lastDropTime number

Records the timestamp (in milliseconds) of the last supply drop, used to calculate when the next drop should occur

let lastDropTime = 0;
airplaneEngineSound object

A p5.Oscillator object that generates the airplane's engine hum sound effect

let airplaneEngineSound;
dropSound object

A p5.Oscillator object that generates the beep sound when a supply is dropped

let dropSound;
collectSound object

A p5.Oscillator object that generates the higher beep sound when a supply is collected

let collectSound;
particles array

An array of Particle objects representing the airplane trail effect. Particles are created each frame and fade away over time.

let particles = [];

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG draw() — background transparency

Using alpha 25 in background() creates motion trails, but if the canvas is paused or unfocused, trails never fully clear, leaving visual artifacts

💡 Consider adding a fade-to-black option or using full alpha (255) during gameplay. Alternatively, add a reset button that calls background(20, 20, 30, 255) once to clear trails.

PERFORMANCE draw() — particle creation

Particles are created every 5 frames indefinitely, and even though they die and are removed, the array can grow large in long gameplay sessions

💡 Consider capping the particles array at a maximum length (e.g., 200), or spawning fewer particles in bursts rather than continuously

STYLE preload() and setup()

Oscillators are started and stopped manually, but there's no error handling if userStartAudio() fails or if the browser doesn't support Web Audio API

💡 Wrap audio initialization in a try-catch block and provide fallback visuals or console messages if audio is unavailable

FEATURE Game mechanics

The game has no end condition, score multiplier, or difficulty progression—it plays the same forever

💡 Add a timer or lives system, increase dropInterval over time to make it harder, or add a UI showing current level/score

FEATURE Supply drops

All supplies are identical—same size, speed, parachute color. Variety could increase engagement

💡 Randomize supply size or color, or add rare 'bonus' supplies worth more food, spawned less frequently

STYLE Airplane and SupplyDrop classes

Colors are hardcoded into constructors, making global recoloring difficult

💡 Define a color palette object at the top of the sketch and pass colors as constructor parameters, allowing easy theme changes

🔄 Code Flow

Code flow showing preload, setup, draw, mousePressed, touchStarted, handleTouchOrClick, windowResized, airplane, supplydrop, particle

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] setup --> canvas-creation[Canvas Initialization] setup --> audio-start[Audio Permission and Startup] setup --> engine-oscillator[Engine Sound Oscillator Setup] setup --> drop-oscillator[Drop Sound Oscillator Setup] setup --> collect-oscillator[Collect Sound Oscillator Setup] draw --> background-render[Semi-transparent Background] draw --> particle-emission[Particle Emission Timing] draw --> drop-spawn-timing[Supply Drop Spawn Timing] draw --> supply-drops-loop[Supply Drops Loop] draw --> particles-loop[Particles Loop] draw --> hud-render[Food Counter Display] supply-drops-loop --> drop-loop[Drop Loop] drop-loop --> collision-check[Collision and State Check] collision-check --> sound-feedback[Collect Sound with Fade] particles-loop --> particle-update[Particle Update with Fade and Death] particle-update --> particle-display[Particle Display with Alpha Fade] draw --> airplane[Airplane] airplane --> airplane-constructor[Constructor Initialization] airplane --> airplane-update[Update Method with Screen Wrapping] airplane --> airplane-display[Display Method with Transform Stack] drop-loop --> drop-constructor[Constructor Setup] drop-constructor --> drop-update[Physics Update with Velocity Limiting] drop-update --> drop-display[Display with Transform Stack] drop-display --> drop-collision[Bounding Box Collision Detection] click setup href "#fn-setup" click draw href "#fn-draw" click canvas-creation href "#sub-canvas-creation" click audio-start href "#sub-audio-start" click engine-oscillator href "#sub-engine-oscillator" click drop-oscillator href "#sub-drop-oscillator" click collect-oscillator href "#sub-collect-oscillator" click background-render href "#sub-background-render" click particle-emission href "#sub-particle-emission" click drop-spawn-timing href "#sub-drop-spawn-timing" click supply-drops-loop href "#sub-supply-drops-loop" click particles-loop href "#sub-particles-loop" click hud-render href "#sub-hud-render" click drop-loop href "#sub-drop-loop" click collision-check href "#sub-collision-check" click sound-feedback href "#sub-sound-feedback" click airplane href "#fn-airplane" click airplane-constructor href "#sub-airplane-constructor" click airplane-update href "#sub-airplane-update" click airplane-display href "#sub-airplane-display" click drop-constructor href "#sub-drop-constructor" click drop-update href "#sub-drop-update" click drop-display href "#sub-drop-display" click drop-collision href "#sub-drop-collision" click particle-update href "#sub-particle-update" click particle-display href "#sub-particle-display"

❓ Frequently Asked Questions

What visual elements are created in the p5.js System sketch?

The sketch visually features an airplane that emits a trail of particles, along with supply drops that occur periodically, set against a dynamic background.

How can users interact with the System sketch?

Users can interact with the sketch by clicking or touching the canvas, which triggers the airplane's engine sounds and allows for a more immersive experience.

What creative coding concepts does the System sketch demonstrate?

The sketch showcases concepts like particle systems, sound synthesis with oscillators, and the use of time-based events for creating dynamic interactions.

Preview

System - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of System - Code flow showing preload, setup, draw, mousePressed, touchStarted, handleTouchOrClick, windowResized, airplane, supplydrop, particle
Code Flow Diagram