🔬 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
🔬 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
🔬 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.
🔬 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