🔬 This nested loop checks every bullet against every enemy. What happens if you remove the break statement—can one bullet destroy multiple enemies in the same frame?
// Bullet–enemy collisions
for (let ei = enemies.length - 1; ei >= 0; ei--) {
const e = enemies[ei];
for (let bi = bullets.length - 1; bi >= 0; bi--) {
const b = bullets[bi];
if (circlePointCollision(e.x, e.y, e.size * 0.5, b.x, b.y)) {
spawnExplosion(e.x, e.y);
enemies.splice(ei, 1);
bullets.splice(bi, 1);
score++;
break;
function updateShooterGame() {
// Move ship with mouse and handle shooting (mouse only)
updateShooterPlayerWithMouse();
handleShootingShooter();
// Update bullets
for (let i = bullets.length - 1; i >= 0; i--) {
bullets[i].update();
if (bullets[i].offscreen()) {
bullets.splice(i, 1);
}
}
// Spawn new enemies based on time + score difficulty
const now = millis();
if (now - lastSpawnTime > spawnDelay) {
enemies.push(new Enemy());
lastSpawnTime = now;
const difficulty = constrain(score / 50, 0, 1);
spawnDelay = 900 - difficulty * 500; // faster spawns with score
}
// Update enemies; check if they pass bottom
for (let i = enemies.length - 1; i >= 0; i--) {
const e = enemies[i];
e.update();
if (e.y - e.size / 2 > height) {
spawnExplosion(e.x, height - 40);
enemies.splice(i, 1);
loseLife();
}
}
// Bullet–enemy collisions
for (let ei = enemies.length - 1; ei >= 0; ei--) {
const e = enemies[ei];
for (let bi = bullets.length - 1; bi >= 0; bi--) {
const b = bullets[bi];
if (circlePointCollision(e.x, e.y, e.size * 0.5, b.x, b.y)) {
spawnExplosion(e.x, e.y);
enemies.splice(ei, 1);
bullets.splice(bi, 1);
score++;
break; // enemy destroyed, move to next enemy
}
}
}
// Enemy–player collisions
if (player) {
for (let ei = enemies.length - 1; ei >= 0; ei--) {
const e = enemies[ei];
if (e.hitsPlayer(player)) {
spawnExplosion(e.x, e.y);
spawnExplosion(player.x, player.y);
enemies.splice(ei, 1);
loseLife();
}
}
}
// Update particles
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update();
if (particles[i].isDead()) {
particles.splice(i, 1);
}
}
drawShooterGame();
}
Line-by-line explanation (14 lines)
🔧 Subcomponents:
for-loop
Bullet Update Loop
for (let i = bullets.length - 1; i >= 0; i--) {
Iterates through all bullets, moves each one upward, and removes bullets that leave the screen
conditional
Enemy Spawn Timer
if (now - lastSpawnTime > spawnDelay) {
Checks if enough time has passed since the last enemy was spawned, and creates a new enemy if so
calculation
Difficulty Scaling
const difficulty = constrain(score / 50, 0, 1);
Converts the player's score into a 0–1 difficulty multiplier that speeds up enemy spawning as the game progresses
for-loop
Enemy Update and Escape Check
for (let i = enemies.length - 1; i >= 0; i--) {
Moves each enemy downward and removes it if it escapes off the bottom, causing a life loss
for-loop
Bullet–Enemy Collision Detection
for (let ei = enemies.length - 1; ei >= 0; ei--) {
Nested loop that checks every bullet against every enemy; if a hit is detected, both are removed and the score increases
for-loop
Enemy–Player Collision Detection
if (player) {
for (let ei = enemies.length - 1; ei >= 0; ei--) {
Checks if any enemy has collided with the player ship; if so, both are destroyed and a life is lost
updateShooterPlayerWithMouse();
- Moves the player ship so it follows the mouse's x position, keeping it near the bottom of the screen
handleShootingShooter();
- Checks if the mouse button is held, and if enough time has passed, fires a new bullet from the ship's nose
for (let i = bullets.length - 1; i >= 0; i--) {
- Loops backward through the bullets array so we can safely remove bullets during iteration
bullets[i].update();
- Moves each bullet upward by calling its update() method
if (bullets[i].offscreen()) {
- Checks if the bullet has moved above the canvas; if so, it's no longer useful and should be removed
bullets.splice(i, 1);
- Removes the offscreen bullet from the array to save memory and reduce calculation overhead
const now = millis();
- Records the current time in milliseconds for comparison against the last spawn time
if (now - lastSpawnTime > spawnDelay) {
- Checks if spawnDelay milliseconds have passed since the last enemy spawn; if so, it's time to spawn a new enemy
const difficulty = constrain(score / 50, 0, 1);
- Converts score to a 0–1 value: every 50 points increases difficulty toward 1.0, capping at 1.0 to avoid extreme speeds
spawnDelay = 900 - difficulty * 500;
- Uses the difficulty value to shorten the spawn delay: at difficulty 0, enemies spawn every 900 ms; at difficulty 1, every 400 ms
if (e.y - e.size / 2 > height) {
- Checks if the top of the enemy has passed the bottom of the screen; if so, the player lost the chance to shoot it
if (circlePointCollision(e.x, e.y, e.size * 0.5, b.x, b.y)) {
- Calls circlePointCollision() to test if the bullet point is inside the enemy circle; a hit triggers destruction and scoring
break;
- Stops checking this enemy against remaining bullets after a hit, since the enemy is already destroyed
if (e.hitsPlayer(player)) {
- Calls the enemy's collision method to test if it overlaps with the player ship; a hit causes both to explode
🔬 The difficulty calculation divides score by 20 instead of 50 (like in Shooter). Why does Target Range escalate faster? Try changing it to 50—do targets spawn much slower as you score?
// Spawn new targets
if (now - lastTargetSpawnTime > targetSpawnDelay) {
targets.push(new Target());
lastTargetSpawnTime = now;
const difficulty = constrain(score / 20, 0, 1);
targetSpawnDelay = 1000 - difficulty * 500;
function updateTargetsGame() {
const now = millis();
// Spawn new targets
if (now - lastTargetSpawnTime > targetSpawnDelay) {
targets.push(new Target());
lastTargetSpawnTime = now;
const difficulty = constrain(score / 20, 0, 1);
targetSpawnDelay = 1000 - difficulty * 500; // more targets as score rises
}
// Update targets; if they escape off bottom, lose a life
for (let i = targets.length - 1; i >= 0; i--) {
const t = targets[i];
t.update();
if (t.isOffscreen()) {
spawnExplosion(t.x, height - 40);
targets.splice(i, 1);
loseLife();
}
}
// Update particles
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update();
if (particles[i].isDead()) {
particles.splice(i, 1);
}
}
drawTargetsGame();
}
Line-by-line explanation (9 lines)
🔧 Subcomponents:
conditional
Target Spawn Timer
if (now - lastTargetSpawnTime > targetSpawnDelay) {
Checks elapsed time and spawns a new target if spawnDelay milliseconds have passed
calculation
Target Difficulty Scaling
const difficulty = constrain(score / 20, 0, 1);
Converts score into a 0–1 difficulty multiplier; score scales faster here (divide by 20) than in Shooter (divide by 50)
for-loop
Target Update and Escape Check
for (let i = targets.length - 1; i >= 0; i--) {
Moves each target downward and removes it if it escapes, triggering a life loss
for-loop
Particle Cleanup
for (let i = particles.length - 1; i >= 0; i--) {
Updates all active particles and removes those that have expired
const now = millis();
- Captures the current time for comparison against the last target spawn time
if (now - lastTargetSpawnTime > targetSpawnDelay) {
- If the elapsed time since the last spawn exceeds the delay, spawn a new target
targets.push(new Target());
- Creates a new Target object and adds it to the targets array
const difficulty = constrain(score / 20, 0, 1);
- Converts score to difficulty; every 20 points moves difficulty up (faster than Shooter's every 50 points)
targetSpawnDelay = 1000 - difficulty * 500;
- At difficulty 0, targets spawn every 1000 ms; at difficulty 1, every 500 ms—a tighter, more intense pace
for (let i = targets.length - 1; i >= 0; i--) {
- Iterates backward through targets so we can safely remove ones that escape
t.update();
- Moves the target downward and applies its horizontal wobble animation
if (t.isOffscreen()) {
- Checks if the target has scrolled off the bottom of the screen without being clicked
loseLife();
- Decrements lives and potentially ends the game if lives reach zero
function drawMainMenu() {
textAlign(CENTER, CENTER);
noStroke();
fill(255, 0, 0);
stroke(255, 0, 0);
strokeWeight(2);
textSize(min(width * 0.06, 56));
text("VIRTUAL VOID ARCADE", width / 2, height * 0.18);
const layout = getMenuLayout();
drawMenuCard(
layout.shooter,
"SPACE SHOOTER",
"Move the ship with your mouse\nHold the mouse button to fire"
);
drawMenuCard(
layout.targets,
"TARGET RANGE",
"Aim with your mouse\nClick targets before they escape"
);
noStroke();
textSize(min(width * 0.028, 22));
fill(255, 0, 0);
text(
"Choose a game with your mouse\nClick the X in the corner to return here",
width / 2,
height * 0.85
);
}
Line-by-line explanation (6 lines)
🔧 Subcomponents:
calculation
Main Title
text("VIRTUAL VOID ARCADE", width / 2, height * 0.18);
Renders the arcade's name in large red text at the top of the menu
function-call
Menu Card Rendering
drawMenuCard(
Calls drawMenuCard twice to render the two game selection buttons with descriptions
textAlign(CENTER, CENTER);
- Sets text alignment so text is centered horizontally and vertically at the given x,y coordinates
fill(255, 0, 0);
- Sets the text color to bright red (r=255, g=0, b=0), the signature neon color of the arcade
stroke(255, 0, 0);
- Sets the outline color for shapes to red as well
textSize(min(width * 0.06, 56));
- Sets the font size to 6% of the canvas width, but caps it at 56 pixels so it doesn't get unreasonably huge on wide screens
const layout = getMenuLayout();
- Calls getMenuLayout() to calculate the positions and sizes of the two menu cards based on the current canvas size
drawMenuCard(
- Renders the first menu card for Space Shooter with its title and instructions
🔬 This function returns early after the first hit. What happens if you remove the return statement—can one click destroy multiple targets if they overlap?
for (let i = targets.length - 1; i >= 0; i--) {
const t = targets[i];
if (circlePointCollision(t.x, t.y, t.r, mx, my)) {
spawnExplosion(t.x, t.y);
targets.splice(i, 1);
score++;
return;
function handleTargetClick(mx, my) {
for (let i = targets.length - 1; i >= 0; i--) {
const t = targets[i];
if (circlePointCollision(t.x, t.y, t.r, mx, my)) {
spawnExplosion(t.x, t.y);
targets.splice(i, 1);
score++;
return;
}
}
}
Line-by-line explanation (7 lines)
🔧 Subcomponents:
conditional
Click Collision Detection
if (circlePointCollision(t.x, t.y, t.r, mx, my)) {
Tests if the click point is inside any target; if so, destroys it and increments the score
for (let i = targets.length - 1; i >= 0; i--) {
- Loops backward through the targets array so we can safely remove a target during iteration
const t = targets[i];
- Gets the current target object for easier reference
if (circlePointCollision(t.x, t.y, t.r, mx, my)) {
- Calls circlePointCollision() to test if the click point (mx, my) is inside the target's circular boundary
spawnExplosion(t.x, t.y);
- Creates a burst of particles at the target's position to give visual feedback for a successful hit
targets.splice(i, 1);
- Removes the target from the array since it has been destroyed
score++;
- Increments the score by 1 for the successful hit
return;
- Exits the function early; since one click can only hit one target, we stop checking after the first hit
function drawCloseButton() {
const x1 = width - CLOSE_BTN_MARGIN - CLOSE_BTN_SIZE;
const y1 = CLOSE_BTN_MARGIN;
const x2 = x1 + CLOSE_BTN_SIZE;
const y2 = y1 + CLOSE_BTN_SIZE;
const hovered = isOverCloseButton(mouseX, mouseY);
stroke(hovered ? color(255, 0, 0) : color(120, 0, 0, 200));
strokeWeight(2);
noFill();
rect(x1, y1, CLOSE_BTN_SIZE, CLOSE_BTN_SIZE, 4);
if (hovered) {
fill(40, 0, 0, 180);
rect(x1, y1, CLOSE_BTN_SIZE, CLOSE_BTN_SIZE, 4);
}
stroke(255, 0, 0);
line(x1 + 4, y1 + 4, x2 - 4, y2 - 4);
line(x1 + 4, y2 - 4, x2 - 4, y1 + 4);
}
Line-by-line explanation (10 lines)
🔧 Subcomponents:
conditional
Hover Feedback
if (hovered) {
Fills the button with dark red when hovered, giving visual feedback that it's interactive
drawing
X Icon Drawing
line(x1 + 4, y1 + 4, x2 - 4, y2 - 4);
Draws the two diagonal lines that form an X symbol inside the button
const x1 = width - CLOSE_BTN_MARGIN - CLOSE_BTN_SIZE;
- Calculates the left edge of the button: start from the right edge of the canvas, subtract the margin, then subtract the button size
const y1 = CLOSE_BTN_MARGIN;
- Positions the button's top edge at a fixed distance from the top of the canvas
const x2 = x1 + CLOSE_BTN_SIZE;
- Calculates the right edge by adding the button size to the left edge
const y2 = y1 + CLOSE_BTN_SIZE;
- Calculates the bottom edge by adding the button size to the top edge
const hovered = isOverCloseButton(mouseX, mouseY);
- Calls isOverCloseButton() to test if the mouse is currently over the button
stroke(hovered ? color(255, 0, 0) : color(120, 0, 0, 200));
- Uses a ternary operator to set the border to bright red if hovered, or dark red if not
rect(x1, y1, CLOSE_BTN_SIZE, CLOSE_BTN_SIZE, 4);
- Draws the button's rectangular border with 4-pixel rounded corners
if (hovered) {
- If the button is hovered, fill it with a dark red semi-transparent color
line(x1 + 4, y1 + 4, x2 - 4, y2 - 4);
- Draws a diagonal line from the top-left corner (inset by 4 pixels) to the bottom-right, forming the first stroke of the X
line(x1 + 4, y2 - 4, x2 - 4, y1 + 4);
- Draws a diagonal line from the bottom-left (inset by 4 pixels) to the top-right, completing the X symbol
🔬 This draws a triangle with three vertices. What happens if you add a fourth vertex like vertex(0, this.h / 2 + 10)—does the ship shape change from a triangle to a quadrilateral?
beginShape();
vertex(-this.w / 2, this.h / 2);
vertex(0, -this.h / 2);
vertex(this.w / 2, this.h / 2);
endShape(CLOSE);
class Player {
constructor() {
this.w = min(width * 0.06, 80);
this.h = this.w * 0.5;
this.x = width / 2;
this.y = height * 0.8;
}
getNosePosition() {
return { x: this.x, y: this.y - this.h / 2 };
}
draw() {
push();
translate(this.x, this.y);
noFill();
stroke(255, 0, 0);
strokeWeight(2);
// Simple angular ship
beginShape();
vertex(-this.w / 2, this.h / 2);
vertex(0, -this.h / 2);
vertex(this.w / 2, this.h / 2);
endShape(CLOSE);
// Cockpit line
line(-this.w / 3, this.h / 4, this.w / 3, this.h / 4);
pop();
}
}
Line-by-line explanation (13 lines)
🔧 Subcomponents:
initialization
Constructor
constructor() {
Initializes a new Player object with starting position, size, and ship parameters
method
getNosePosition Method
getNosePosition() {
Returns the tip of the ship (for spawning bullets)
drawing
Ship Shape Drawing
beginShape();
vertex(-this.w / 2, this.h / 2);
vertex(0, -this.h / 2);
vertex(this.w / 2, this.h / 2);
endShape(CLOSE);
Draws a triangular ship pointing upward, creating a classic arcade spaceship silhouette
this.w = min(width * 0.06, 80);
- Sets the ship width to 6% of the canvas, but never wider than 80 pixels, making it responsive to screen size
this.h = this.w * 0.5;
- Sets height to half the width, creating a compact, tapered ship shape
this.x = width / 2;
- Positions the ship horizontally at the center of the canvas
this.y = height * 0.8;
- Positions the ship 80% down the canvas, near the bottom where the player can defend
getNosePosition() {
- Defines a method that returns an object with the x and y coordinates of the ship's tip
return { x: this.x, y: this.y - this.h / 2 };
- The nose is at the ship's x position but shifted up by half the height, pointing in the direction the ship faces
translate(this.x, this.y);
- Moves the coordinate origin to the ship's center, so all drawing is relative to the ship
beginShape();
- Starts defining a polygon shape for the ship's hull
vertex(-this.w / 2, this.h / 2);
- Defines the bottom-left corner of the ship
vertex(0, -this.h / 2);
- Defines the top point (nose) of the ship, creating the angular tip
vertex(this.w / 2, this.h / 2);
- Defines the bottom-right corner of the ship
endShape(CLOSE);
- Closes the shape by connecting the last vertex back to the first, completing the triangle
line(-this.w / 3, this.h / 4, this.w / 3, this.h / 4);
- Draws a horizontal line across the ship's upper third as a cockpit detail, adding visual depth
class Bullet {
constructor(x, y) {
this.x = x;
this.y = y;
this.speed = max(8, height * 0.015);
}
update() {
this.y -= this.speed;
}
offscreen() {
return this.y < -10;
}
draw() {
stroke(255, 0, 0);
strokeWeight(3);
line(this.x, this.y + 4, this.x, this.y - 8);
}
}
Line-by-line explanation (9 lines)
🔧 Subcomponents:
calculation
Responsive Speed
this.speed = max(8, height * 0.015);
Sets bullet speed based on canvas height, ensuring bullets move at a consistent apparent speed across different screen sizes
constructor(x, y) {
- Initializes a bullet at the given x, y position (spawned from the ship's nose)
this.speed = max(8, height * 0.015);
- Sets speed to 1.5% of canvas height, but at least 8 pixels per frame, ensuring bullets move fast enough on any screen
update() {
- Defines the update method, called every frame to move the bullet
this.y -= this.speed;
- Moves the bullet upward (negative y) by its speed each frame, creating upward motion
offscreen() {
- Defines a method that checks if the bullet has left the screen and should be removed
return this.y < -10;
- Returns true if the bullet's y position is above the top of the canvas (with a 10-pixel buffer), indicating it's gone
stroke(255, 0, 0);
- Sets the line color to bright red, matching the arcade's neon aesthetic
strokeWeight(3);
- Makes the bullet line thick (3 pixels) so it's visible and easy to track during gameplay
line(this.x, this.y + 4, this.x, this.y - 8);
- Draws a short vertical line centered at the bullet's position, creating a simple beam projectile shape
🔬 Enemies drift horizontally (this.vx) and bounce at edges. What happens if you remove the vx bouncing logic—do enemies just scroll off the sides?
this.y += this.speed;
this.x += this.vx;
if (this.x < this.size / 2 || this.x > width - this.size / 2) {
this.vx *= -1;
class Enemy {
constructor() {
this.size = random(30, 60);
this.x = random(this.size, width - this.size);
this.y = -this.size;
this.speed = random(1.5, 3);
this.vx = random(-1.2, 1.2);
}
update() {
this.y += this.speed;
this.x += this.vx;
if (this.x < this.size / 2 || this.x > width - this.size / 2) {
this.vx *= -1;
}
}
hitsPlayer(player) {
const dx = this.x - player.x;
const dy = this.y - player.y;
const distSq = dx * dx + dy * dy;
const rad = this.size / 2 + max(player.w, player.h) / 3;
return distSq < rad * rad;
}
draw() {
push();
translate(this.x, this.y);
noFill();
stroke(255, 0, 0);
strokeWeight(2);
// Saucer / ring
ellipse(0, 0, this.size, this.size * 0.6);
line(-this.size * 0.4, 0, this.size * 0.4, 0);
pop();
}
}
Line-by-line explanation (16 lines)
🔧 Subcomponents:
initialization
Randomized Spawning
this.size = random(30, 60);
this.x = random(this.size, width - this.size);
this.speed = random(1.5, 3);
this.vx = random(-1.2, 1.2);
Each enemy is spawned with random size, position, and speed, creating variety and preventing predictable patterns
conditional
Edge Bouncing
if (this.x < this.size / 2 || this.x > width - this.size / 2) {
When an enemy approaches the left or right edge, its horizontal velocity reverses, keeping it on screen
method
Player Collision
hitsPlayer(player) {
Checks if this enemy overlaps with the player ship using distance-based collision
this.size = random(30, 60);
- Assigns a random diameter between 30 and 60 pixels, making enemies visually varied
this.x = random(this.size, width - this.size);
- Spawns the enemy at a random x position, but constrained so it doesn't start partially off-screen
this.y = -this.size;
- Spawns the enemy just above the canvas (negative y), so it descends into view
this.speed = random(1.5, 3);
- Each enemy moves downward at a different speed, creating unpredictability
this.vx = random(-1.2, 1.2);
- Each enemy has a random horizontal velocity component, making it drift side-to-side as it descends
this.y += this.speed;
- Moves the enemy downward by its speed each frame
this.x += this.vx;
- Moves the enemy horizontally by its drift velocity
if (this.x < this.size / 2 || this.x > width - this.size / 2) {
- Checks if the enemy has hit the left or right edge; if so, reverses its horizontal direction
this.vx *= -1;
- Flips the enemy's horizontal velocity, making it bounce back toward the center
const dx = this.x - player.x;
- Calculates horizontal distance from enemy to player
const dy = this.y - player.y;
- Calculates vertical distance from enemy to player
const distSq = dx * dx + dy * dy;
- Calculates squared distance (avoiding expensive sqrt)
const rad = this.size / 2 + max(player.w, player.h) / 3;
- Calculates the collision radius by adding enemy radius to a portion of the player's dimensions
return distSq < rad * rad;
- Returns true if the squared distance is less than the squared radius, indicating a collision
ellipse(0, 0, this.size, this.size * 0.6);
- Draws an ellipse (wider than it is tall) to represent the enemy saucer shape
line(-this.size * 0.4, 0, this.size * 0.4, 0);
- Draws a horizontal line across the middle of the ellipse, adding detail to the saucer design
🔬 The sine wave creates smooth horizontal wobble. What happens if you remove the sin() calculation—do targets move in straight lines instead of weaving?
update() {
this.y += this.baseSpeed;
// slight horizontal wobble
this.x += this.vx + sin(millis() * 0.003 + this.phase) * 0.3;
this.x = constrain(this.x, this.r, width - this.r);
class Target {
constructor() {
this.r = random(18, 30);
this.x = random(this.r, width - this.r);
this.y = -this.r - random(20, height * 0.3);
this.baseSpeed = random(1.0, 2.2);
this.vx = random(-0.5, 0.5);
this.phase = random(TWO_PI);
}
update() {
this.y += this.baseSpeed;
// slight horizontal wobble
this.x += this.vx + sin(millis() * 0.003 + this.phase) * 0.3;
this.x = constrain(this.x, this.r, width - this.r);
}
isOffscreen() {
return this.y - this.r > height;
}
draw() {
push();
translate(this.x, this.y);
noFill();
stroke(255, 0, 0);
strokeWeight(2);
ellipse(0, 0, this.r * 2, this.r * 2);
line(-this.r * 0.6, 0, this.r * 0.6, 0);
line(0, -this.r * 0.6, 0, this.r * 0.6);
pop();
}
}
Line-by-line explanation (13 lines)
🔧 Subcomponents:
calculation
Animated Wobble
this.x += this.vx + sin(millis() * 0.003 + this.phase) * 0.3;
Adds sine-wave horizontal wobble to make targets more visually interesting and harder to predict
method
Offscreen Detection
isOffscreen() {
Checks if the target has scrolled below the bottom of the screen without being clicked
this.r = random(18, 30);
- Sets a random radius between 18 and 30 pixels, making targets visually varied in size
this.x = random(this.r, width - this.r);
- Spawns the target at a random x position, constrained so it doesn't start off-screen horizontally
this.y = -this.r - random(20, height * 0.3);
- Spawns the target above the screen at a random distance, so targets appear from varying heights
this.baseSpeed = random(1.0, 2.2);
- Each target scrolls down at a different base speed, creating unpredictability
this.vx = random(-0.5, 0.5);
- Sets a small drift velocity component, giving targets slight horizontal bias
this.phase = random(TWO_PI);
- Assigns a random phase offset for the sine wobble, so targets don't all wobble in sync
this.y += this.baseSpeed;
- Moves the target downward at its base speed
this.x += this.vx + sin(millis() * 0.003 + this.phase) * 0.3;
- Moves the target horizontally; the sine wave (millis() * 0.003) creates a smooth oscillation, and phase makes each target wobble independently
this.x = constrain(this.x, this.r, width - this.r);
- Clamps the target's x position so it never moves completely off-screen horizontally
return this.y - this.r > height;
- Returns true if the bottom of the target has passed below the canvas, indicating it escaped
ellipse(0, 0, this.r * 2, this.r * 2);
- Draws a circle centered at the origin, with diameter = 2 * radius
line(-this.r * 0.6, 0, this.r * 0.6, 0);
- Draws a horizontal crosshair line across the target
line(0, -this.r * 0.6, 0, this.r * 0.6);
- Draws a vertical crosshair line, completing the crosshair reticle design
🔬 This uses cos and sin to spread particles in all directions. What happens if you remove the angle randomness and just set this.vx = speed and this.vy = 0—do all particles move horizontally?
const angle = random(TWO_PI);
const speed = random(1, 5);
this.vx = cos(angle) * speed;
this.vy = sin(angle) * speed;
class Particle {
constructor(x, y) {
this.x = x;
this.y = y;
const angle = random(TWO_PI);
const speed = random(1, 5);
this.vx = cos(angle) * speed;
this.vy = sin(angle) * speed;
this.maxLife = random(15, 35);
this.life = this.maxLife;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.vy += 0.05; // slight gravity
this.life -= 1;
}
isDead() {
return this.life <= 0;
}
draw() {
const alpha = (this.life / this.maxLife) * 255;
stroke(255, 0, 0, alpha);
strokeWeight(2);
point(this.x, this.y);
}
}
Line-by-line explanation (14 lines)
🔧 Subcomponents:
calculation
Random Burst Direction
const angle = random(TWO_PI);
const speed = random(1, 5);
this.vx = cos(angle) * speed;
this.vy = sin(angle) * speed;
Each particle shoots off in a random direction at a random speed, creating a burst effect
calculation
Gravity Simulation
this.vy += 0.05; // slight gravity
Particles accelerate downward over time, creating a realistic arc trajectory
calculation
Fade Out
const alpha = (this.life / this.maxLife) * 255;
Particles become more transparent as they age, creating a smooth disappearance
constructor(x, y) {
- Creates a particle at the given x, y position (typically the location of a destroyed object)
const angle = random(TWO_PI);
- Picks a random angle in radians (0 to 2π), representing a direction from 0° to 360° around the center
const speed = random(1, 5);
- Assigns a random speed between 1 and 5 pixels per frame
this.vx = cos(angle) * speed;
- Calculates the horizontal velocity component using cosine; particles moving at 0° move right, 90° move up, etc.
this.vy = sin(angle) * speed;
- Calculates the vertical velocity component using sine
this.maxLife = random(15, 35);
- Sets a random lifetime between 15 and 35 frames, so particles live for varying durations
this.life = this.maxLife;
- Initializes current life to max life; it decrements each frame until reaching zero
this.x += this.vx;
- Moves the particle by its velocity each frame
this.y += this.vy;
- Moves the particle vertically by its velocity
this.vy += 0.05;
- Adds slight gravity: the vertical velocity accelerates downward each frame, causing particles to arc and fall
this.life -= 1;
- Decrements the particle's remaining life each frame
const alpha = (this.life / this.maxLife) * 255;
- Calculates the particle's opacity: at full life, alpha is 255 (opaque); at zero life, alpha is 0 (invisible)
stroke(255, 0, 0, alpha);
- Sets the color to red with the calculated alpha, making the particle fade out smoothly
point(this.x, this.y);
- Draws a 1-pixel red dot at the particle's current position