Sketch 2026-03-02 23:21

This interactive Spider-Man swing game challenges players to catch falling energy orbs while swinging on a web across a pixelated city skyline. The player controls the anchor point of the web to catch orbs before they drop off screen, with increasing difficulty, a scoring system, and three lives.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make orbs larger — Increase the orb size range so they're bigger and easier to catch, making the game less frustrating
  2. Slow down the difficulty ramp — Orbs spawn less frequently early on, giving you more time to adapt before the game gets hard
  3. Give yourself more lives — Start the game with 5 lives instead of 3, making it more forgiving
  4. Earn more points per catch — Each caught orb gives 25 points instead of 10, making high scores feel more rewarding
  5. Make the sky more purple — Change the sky gradient from blue-to-red to a more purple tone
  6. Faster swing speed — Spider-Man swings much quicker, requiring faster reflexes but feeling more energetic
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a fully playable Spider-Man swing game where you control a character swinging from a web anchor point to catch falling energy orbs. The game features a stunning pixel-art city skyline with flickering windows, a gradient night sky with twinkling stars, and smooth physics-based animation. It combines multiple p5.js techniques: pendulum animation using sine/cosine math, collision detection with distance calculations, particle bursts for visual feedback, and keyboard input handling.

The code is organized into several layers: game logic (orb spawning, collision, scoring), visual rendering (cityscape, sky, stars, Spider-Man, effects), and state management (lives, game over, reset). By studying it, you'll learn how to build a complete interactive game with increasing difficulty, particle effects, and responsive controls—skills that transfer directly to any game or interactive experience you want to create.

⚙️ How It Works

  1. When the sketch loads, setup() creates the canvas, initializes the cityscape with random buildings and stars, and sets the game to its starting state with 3 lives and a score of 0.
  2. Every frame, draw() renders the background (sky gradient and stars), buildings, and Spider-Man swinging from an anchor point whose position is controlled by arrow keys or A/D.
  3. The swinging motion is calculated using sin() and cos() functions applied to frameCount, creating a pendulum arc—the anchor stays at the top while Spider-Man swings back and forth below it.
  4. Energy orbs spawn at random x positions near the top and fall downward, wobbling slightly side-to-side using sin() with a unique phase offset for each orb.
  5. On every frame, the code checks if Spider-Man's position collides with any orb using the dist() function; if the distance is close enough, the orb is caught, score increases by 10, and a burst particle effect expands outward.
  6. If an orb reaches the bottom of the screen without being caught, the player loses a life; when lives reach zero, gameOver is set to true and a semi-transparent overlay displays the final score.
  7. Pressing R resets the game state, allowing the player to swing again with difficulty gradually increasing as orbInterval decreases over time.

🎓 Concepts You'll Learn

Animation loop and frameCountPendulum motion with trigonometry (sin/cos)Collision detection using distanceGame state managementParticle effects and burstsKeyboard input and constrainProcedural generation (buildings)Color gradients and lerp

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch loads. It prepares the canvas, initializes all variables, and builds the static city background. Anything you want ready before gameplay starts goes here.

function setup() {
  createCanvas(windowWidth, windowHeight);
  angleMode(RADIANS);

  anchorX = width * 0.5;
  anchorY = height * 0.08;

  createCityscape();
  createStars();
  resetGame();
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

initialization Canvas Setup createCanvas(windowWidth, windowHeight);

Creates a full-window canvas that responds to screen size

initialization Angle Mode angleMode(RADIANS);

Sets angle measurements to radians (required for sin/cos trigonometry)

calculation Anchor Position anchorX = width * 0.5; anchorY = height * 0.08;

Places the web anchor point at the top center of the canvas (8% down from top)

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire window; windowWidth and windowHeight make it responsive to screen size
angleMode(RADIANS);
Tells p5.js to measure angles in radians (0 to 2π) instead of degrees—required for sin() and cos() math
anchorX = width * 0.5;
Sets the anchor point's horizontal position to the center of the canvas (50% of width)
anchorY = height * 0.08;
Sets the anchor point's vertical position to 8% down from the top of the canvas
createCityscape();
Generates the random buildings that form the city skyline at the bottom
createStars();
Creates 120 random stars scattered across the sky for the twinkling background
resetGame();
Initializes game variables: score = 0, lives = 3, clears any existing orbs, and sets gameOver to false

draw()

draw() is the heartbeat of the game—it runs 60 times per second. The order of operations matters: background first (so it's always on bottom), then entities, then UI on top. The gameOver flag gates whether input processing and orb updates happen, which is how we freeze gameplay when you lose.

🔬 These three lines render the static background each frame. What happens if you comment out drawBuildings() to remove the city? How does that change what you focus on?

  drawSkyGradient();
  drawStars();
  drawBuildings();

🔬 This block processes input and then calculates where Spider-Man should be. What if you moved computeSwingState() BEFORE the if-statement? Would Spider-Man still swing while the game is over?

  if (!gameOver) {
    handleAnchorInput();
  }

  const swingState = computeSwingState();
function draw() {
  drawSkyGradient();
  drawStars();
  drawBuildings();

  if (!gameOver) {
    handleAnchorInput();
  }

  const swingState = computeSwingState();

  if (!gameOver) {
    updateOrbs(swingState);
  }

  drawOrbs();
  drawBursts();
  renderSpiderMan(swingState);

  drawHUD();
  drawInstructions();

  if (gameOver) {
    drawGameOverOverlay();
  }
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

function-call-sequence Background Rendering drawSkyGradient(); drawStars(); drawBuildings();

Draws the static background layers: night sky gradient, twinkling stars, and city buildings

conditional Input Processing if (!gameOver) { handleAnchorInput(); }

Only processes keyboard arrow key/A/D input if the game is still active

calculation Swing State Computation const swingState = computeSwingState();

Calculates Spider-Man's position and angle for this frame based on the pendulum math

conditional Orb Update Phase if (!gameOver) { updateOrbs(swingState); }

Falls falling orbs, checks collisions with Spider-Man, handles scoring and life loss—only when game is active

function-call-sequence Entity Rendering drawOrbs(); drawBursts(); renderSpiderMan(swingState);

Draws all interactive elements: energy orbs, particle burst effects, and the Spider-Man character with web

function-call-sequence UI Layer drawHUD(); drawInstructions(); if (gameOver) { drawGameOverOverlay(); }

Renders score/lives display, control instructions at the bottom, and the game over screen when needed

drawSkyGradient();
Draws a color gradient from dark blue at the top to reddish at the bottom—the backdrop for the entire scene
drawStars();
Renders all 120 stars with a twinkling pulse effect that makes them appear to breathe
drawBuildings();
Draws each building block with randomly flickering windows to simulate city lights
if (!gameOver) {
Only process player input when the game is active—no controls work after game over
handleAnchorInput();
Checks if arrow keys or A/D are being held and moves the anchor left or right
const swingState = computeSwingState();
Calculates where Spider-Man should be this frame using pendulum math (sine wave); returns position and angle
updateOrbs(swingState);
Moves each orb down, checks if Spider-Man caught it (collision detection), spawns new orbs when needed, and ends game if orbs drop
drawOrbs();
Renders each energy orb as a glowing three-layer circle with a pulsing glow effect
drawBursts();
Renders expanding ring particles from caught orbs; shrinks their alpha each frame until they disappear
renderSpiderMan(swingState);
Draws the web line from anchor to Spider-Man and the character itself, rotated to match the swing angle
drawHUD();
Displays the current score on the top left and remaining lives on the top right
drawInstructions();
Shows control instructions at the bottom: arrow keys, goal, and restart hint
if (gameOver) {
When the game ends, draw a semi-transparent overlay with game over message and final score

handleAnchorInput()

This function is called every frame (inside draw()) to make the anchor follow the player's input in real time. keyIsDown() is used instead of keyPressed() because we want the anchor to move continuously while a key is held, not just once per key press. The constrain() at the end is a safety guardrail—without it, the player could move the anchor off-screen.

🔬 These two if-statements check for four different keys (two per direction). What happens if you change 65 to 87 (the 'W' key) or 68 to 83 (the 'S' key)? Can you remap the controls?

  if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) {
    anchorX -= move;
  }
  if (keyIsDown(RIGHT_ARROW) || keyIsDown(68)) {
    anchorX += move;
  }
function handleAnchorInput() {
  const move = 6;
  if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) {
    anchorX -= move;
  }
  if (keyIsDown(RIGHT_ARROW) || keyIsDown(68)) {
    anchorX += move;
  }
  anchorX = constrain(anchorX, width * anchorBounds, width * (1 - anchorBounds));
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Left Movement if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) { anchorX -= move; }

If left arrow or 'A' key is held, move the anchor 6 pixels to the left

conditional Right Movement if (keyIsDown(RIGHT_ARROW) || keyIsDown(68)) { anchorX += move; }

If right arrow or 'D' key is held, move the anchor 6 pixels to the right

calculation Boundary Enforcement anchorX = constrain(anchorX, width * anchorBounds, width * (1 - anchorBounds));

Prevents the anchor from moving outside a safe zone (15%–85% of canvas width) so Spider-Man can't swing off-screen

const move = 6;
Defines how many pixels the anchor moves per frame when a key is held—higher values make it respond faster
if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) {
keyIsDown() checks if a key is currently held (not just pressed once); 65 is the ASCII code for 'A'
anchorX -= move;
Subtracts the move distance from anchorX, shifting the anchor (and Spider-Man) to the left
if (keyIsDown(RIGHT_ARROW) || keyIsDown(68)) {
Checks if the right arrow or 'D' key (ASCII 68) is being held
anchorX += move;
Adds the move distance to anchorX, shifting the anchor to the right
anchorX = constrain(anchorX, width * anchorBounds, width * (1 - anchorBounds));
constrain() clamps anchorX between the min (15% of width) and max (85% of width), preventing it from moving too far

computeSwingState()

This function is the mathematical heart of the game—it's pure trigonometry. The pendulum arc is created by converting an angle (swingAngle) into x,y coordinates using sine and cosine. Every frame, the angle changes slightly (driven by frameCount), so the position traces a smooth arc. This is the same math used for clock hands, rotating objects, circular motion, and anything that moves in an arc.

🔬 These two lines create the pendulum motion using a sine wave. What happens if you change swingSpeed from 0.025 to something much higher like 0.1? What if you change it to 0.001?

  const t = frameCount * swingSpeed;
  const swingAngle = sin(t) * 0.85;

🔬 These lines convert the swing angle into an x,y position using trig. What if you swap sin and cos? Or remove the webLength multiplier? How would that change the swing shape?

  const spideyX = anchorX + webLength * sin(swingAngle);
  const spideyY = anchorY + webLength * cos(swingAngle);
function computeSwingState() {
  anchorY = height * 0.08;

  const t = frameCount * swingSpeed;
  const swingAngle = sin(t) * 0.85;

  const spideyX = anchorX + webLength * sin(swingAngle);
  const spideyY = anchorY + webLength * cos(swingAngle);

  const trailingAngle = swingAngle - 0.08;
  const trailingX = anchorX + webLength * sin(trailingAngle);
  const trailingY = anchorY + webLength * cos(trailingAngle);

  return { swingAngle, spideyX, spideyY, trailingX, trailingY };
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Pendulum Angle const t = frameCount * swingSpeed; const swingAngle = sin(t) * 0.85;

Uses a sine wave to calculate the swing angle oscillating back and forth; multiplied by 0.85 to limit the swing's range

calculation Spider-Man Position const spideyX = anchorX + webLength * sin(swingAngle); const spideyY = anchorY + webLength * cos(swingAngle);

Uses trigonometry to convert the swing angle into x,y coordinates; webLength acts as the radius of the pendulum arc

calculation Trailing Web Point const trailingAngle = swingAngle - 0.08; const trailingX = anchorX + webLength * sin(trailingAngle); const trailingY = anchorY + webLength * cos(trailingAngle);

Calculates a second point slightly behind the main web to create a visual trailing effect

anchorY = height * 0.08;
Resets the anchor's y position every frame to ensure it stays at the top (8% down) even if something else moved it
const t = frameCount * swingSpeed;
Multiplies frameCount (which increases by 1 every frame) by swingSpeed to create a continuously increasing time value that drives the sine wave
const swingAngle = sin(t) * 0.85;
sin(t) oscillates between -1 and 1; multiplying by 0.85 limits the angle to roughly ±0.85 radians (~50 degrees), controlling swing width
const spideyX = anchorX + webLength * sin(swingAngle);
sin(swingAngle) determines left/right displacement; webLength acts as the lever arm—Spider-Man's horizontal position is the anchor plus this offset
const spideyY = anchorY + webLength * cos(swingAngle);
cos(swingAngle) determines how far below the anchor Spider-Man hangs; at small angles, cos is close to 1, so he hangs at nearly webLength distance
const trailingAngle = swingAngle - 0.08;
Slightly lags the swing angle (0.08 radians behind) to create a second point that trails behind the main web visually
const trailingX = anchorX + webLength * sin(trailingAngle);
Calculates the trailing point's x position using the lagged angle
const trailingY = anchorY + webLength * cos(trailingAngle);
Calculates the trailing point's y position; this creates the dimmer web line seen in renderSpiderMan()
return { swingAngle, spideyX, spideyY, trailingX, trailingY };
Returns an object containing all five values so draw() can use them for rendering and collision detection

updateOrbs(state)

This function drives the game loop. It handles three critical tasks: spawning orbs at increasing frequency (difficulty ramp), moving them downward and wobbling, and detecting collisions using the dist() function. The backward loop (i-- instead of i++) is a critical pattern when removing items—forward loops would skip elements after a splice. The 'continue' statement skips the rest of the loop after catching an orb to prevent double-processing.

🔬 This is the collision detection. The 24 is Spider-Man's radius. What happens if you change 24 to 50? To 5? How does the collision range change?

    const d = dist(orb.x, orb.y, state.spideyX, state.spideyY);
    if (d < orb.size * 0.5 + 24) {

🔬 The first line makes orbs fall, the second adds a wobble. What happens if you remove the wobble line? Or double the wobble amplitude from 0.5 to 1.0?

    orb.y += orb.speed;
    orb.x += sin(frameCount * 0.03 + orb.phase) * 0.5;
function updateOrbs(state) {
  const now = millis();
  if (now - lastOrbSpawn > orbInterval) {
    spawnOrb();
    lastOrbSpawn = now;
    orbInterval = max(900, orbInterval - 20); // ramp difficulty
  }

  for (let i = orbs.length - 1; i >= 0; i--) {
    const orb = orbs[i];
    orb.y += orb.speed;
    orb.x += sin(frameCount * 0.03 + orb.phase) * 0.5;

    const d = dist(orb.x, orb.y, state.spideyX, state.spideyY);
    if (d < orb.size * 0.5 + 24) {
      score += 10;
      bursts.push({ x: orb.x, y: orb.y, r: orb.size, alpha: 220 });
      orbs.splice(i, 1);
      continue;
    }

    if (orb.y - orb.size * 0.5 > height) {
      lives--;
      orbs.splice(i, 1);
      if (lives <= 0) {
        gameOver = true;
      }
    }
  }
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

conditional Orb Spawn Timer const now = millis(); if (now - lastOrbSpawn > orbInterval) { spawnOrb(); lastOrbSpawn = now; orbInterval = max(900, orbInterval - 20); }

Checks elapsed time since last orb spawn; if enough time has passed, spawns a new orb and reduces the interval to increase difficulty

for-loop Orb Physics Update for (let i = orbs.length - 1; i >= 0; i--) { const orb = orbs[i]; orb.y += orb.speed; orb.x += sin(frameCount * 0.03 + orb.phase) * 0.5;

For each orb, moves it downward at its speed and adds a side-to-side wobble using a sine wave

conditional Spider-Man Collision const d = dist(orb.x, orb.y, state.spideyX, state.spideyY); if (d < orb.size * 0.5 + 24) { score += 10; bursts.push({ x: orb.x, y: orb.y, r: orb.size, alpha: 220 }); orbs.splice(i, 1); continue; }

Uses distance formula to check if Spider-Man is close enough to catch the orb; if so, awards points, creates a burst, and removes the orb

conditional Off-Screen Check if (orb.y - orb.size * 0.5 > height) { lives--; orbs.splice(i, 1); if (lives <= 0) { gameOver = true; } }

If an orb reaches the bottom without being caught, deducts a life and ends the game if lives reach zero

const now = millis();
millis() returns the number of milliseconds since the sketch started—used to track spawn timing
if (now - lastOrbSpawn > orbInterval) {
Checks if the elapsed time since the last orb spawn exceeds the current interval; if true, it's time to spawn a new orb
spawnOrb();
Creates a new orb at a random x position near the top with a random speed and phase
lastOrbSpawn = now;
Updates the spawn timer by recording the current time, resetting the countdown for the next orb
orbInterval = max(900, orbInterval - 20);
Decreases the spawn interval by 20ms each spawn, making orbs appear more frequently; max(900, ...) ensures it never goes below 900ms
for (let i = orbs.length - 1; i >= 0; i--) {
Loops through orbs backward (from last to first) so we can safely remove items with splice() without skipping any
orb.y += orb.speed;
Moves each orb downward by its speed each frame, simulating gravity-like falling
orb.x += sin(frameCount * 0.03 + orb.phase) * 0.5;
Adds a wobbling side-to-side motion using sin(); each orb's phase offset makes them wobble independently
const d = dist(orb.x, orb.y, state.spideyX, state.spideyY);
dist() calculates the distance between the orb and Spider-Man—the foundation of collision detection
if (d < orb.size * 0.5 + 24) {
If the distance is less than the collision radius (orb radius + Spider-Man radius of 24), a catch happens
score += 10;
Awards 10 points for catching an orb
bursts.push({ x: orb.x, y: orb.y, r: orb.size, alpha: 220 });
Creates a particle burst effect at the orb's location that will expand and fade out
orbs.splice(i, 1);
Removes the caught orb from the array so it no longer exists
continue;
Skips the rest of the loop iteration; important to avoid checking collision and off-screen conditions on a caught orb
if (orb.y - orb.size * 0.5 > height) {
Checks if the orb's bottom edge (y - radius) has passed the bottom of the canvas
lives--;
Deducts one life when an orb is missed
if (lives <= 0) {
If the player has no lives left, ends the game by setting gameOver = true

spawnOrb()

This helper function is called by updateOrbs() whenever it's time to spawn a new orb. It creates a JavaScript object bundling all the properties an orb needs (position, size, speed, phase). Using an object is cleaner than tracking separate arrays for each property. The phase offset ensures each orb wobbles at its own rhythm instead of all wobbling in sync.

function spawnOrb() {
  const size = random(26, 40);
  orbs.push({
    x: random(width * anchorBounds, width * (1 - anchorBounds)),
    y: -size,
    size,
    speed: random(2.2, 3.4),
    phase: random(TWO_PI)
  });
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Random Orb Size const size = random(26, 40);

Picks a random diameter between 26 and 40 pixels, adding visual variety to orbs

calculation Orb Property Bundle orbs.push({ x: random(width * anchorBounds, width * (1 - anchorBounds)), y: -size, size, speed: random(2.2, 3.4), phase: random(TWO_PI) });

Creates and adds a new orb object to the array with randomized position, speed, and wobble phase

const size = random(26, 40);
random() picks a random decimal between 26 and 40; this becomes the orb's diameter
orbs.push({
push() adds a new object to the orbs array; this object bundles all the data needed to track one orb
x: random(width * anchorBounds, width * (1 - anchorBounds)),
Spawns the orb at a random x position within the safe swing zone (15%–85% of canvas width)
y: -size,
Spawns the orb just above the top of the canvas (negative y) so it falls in from above
size,
Shorthand for 'size: size'—stores the random size in the object
speed: random(2.2, 3.4),
Each orb gets a random falling speed between 2.2 and 3.4 pixels per frame
phase: random(TWO_PI)
random(TWO_PI) picks a random phase offset between 0 and 2π; this makes each orb wobble independently

drawOrbs()

This function demonstrates a powerful rendering technique: layering multiple shapes with different colors, sizes, and transparency to create depth and visual interest. The pulsing glow uses the same sin/map pattern seen elsewhere in the code—it's a standard technique for making things feel alive. The white highlight at the end is a 2D rendering trick that simulates light reflection, making flat circles feel three-dimensional.

🔬 This draws three concentric circles to build a layered glow effect. What if you remove the first circle (the outer glow)? Or remove the last one (the white core)? How does the orb's appearance change?

    fill(255, 120, 40, glow * 0.6);
    circle(orb.x, orb.y, orb.size * 1.4);
    fill(255, 200, 80, glow);
    circle(orb.x, orb.y, orb.size);
    fill(255, 255, 255, 220);
    circle(orb.x, orb.y, orb.size * 0.35);
function drawOrbs() {
  noStroke();
  for (const orb of orbs) {
    const glow = map(sin(frameCount * 0.1 + orb.phase), -1, 1, 120, 255);
    fill(255, 120, 40, glow * 0.6);
    circle(orb.x, orb.y, orb.size * 1.4);
    fill(255, 200, 80, glow);
    circle(orb.x, orb.y, orb.size);
    fill(255, 255, 255, 220);
    circle(orb.x, orb.y, orb.size * 0.35);
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Glow Pulsing const glow = map(sin(frameCount * 0.1 + orb.phase), -1, 1, 120, 255);

Calculates a pulsing glow intensity using a sine wave, mapped to a range of 120–255 for subtle breathing effect

function-call-sequence Three-Layer Orb fill(255, 120, 40, glow * 0.6); circle(orb.x, orb.y, orb.size * 1.4); fill(255, 200, 80, glow); circle(orb.x, orb.y, orb.size); fill(255, 255, 255, 220); circle(orb.x, orb.y, orb.size * 0.35);

Draws three concentric circles: a dimmer outer glow, a bright middle body, and a white core highlight

noStroke();
Disables stroke so the circles are pure fills without outlines
for (const orb of orbs) {
forEach-style loop that iterates over every orb in the orbs array
const glow = map(sin(frameCount * 0.1 + orb.phase), -1, 1, 120, 255);
sin(frameCount * 0.1 + orb.phase) oscillates between -1 and 1; map() converts that range to 120–255, creating a pulsing glow value
fill(255, 120, 40, glow * 0.6);
Sets an orange color (red 255, green 120, blue 40) with alpha at glow * 0.6—subtle and semi-transparent
circle(orb.x, orb.y, orb.size * 1.4);
Draws the outer glow circle at 1.4× the orb's size
fill(255, 200, 80, glow);
Sets a bright orange (255, 200, 80) with full glow intensity—the main orb body
circle(orb.x, orb.y, orb.size);
Draws the main orb circle at the standard size
fill(255, 255, 255, 220);
Sets white (255, 255, 255) with fixed high alpha for a bright core
circle(orb.x, orb.y, orb.size * 0.35);
Draws a small white highlight circle at 35% of the orb's size, giving it a glossy appearance

drawBursts()

Particle effects like this burst are a staple of game design—they provide visual feedback that something happened. This one is simple but effective: expand and fade. The backward loop and removal pattern are critical; without them, old bursts would accumulate in memory forever. Notice how the burst is pushed to the bursts array inside updateOrbs() and drawn here—a clean separation of concerns.

🔬 This animates an expanding, fading ring. What happens if you change b.alpha -= 8 to b.alpha -= 2? Or change b.r += 4 to b.r += 1? How does the burst feel different?

    stroke(255, 200, 80, b.alpha);
    circle(b.x, b.y, b.r);
    b.r += 4;
    b.alpha -= 8;
function drawBursts() {
  noFill();
  strokeWeight(2);
  for (let i = bursts.length - 1; i >= 0; i--) {
    const b = bursts[i];
    stroke(255, 200, 80, b.alpha);
    circle(b.x, b.y, b.r);
    b.r += 4;
    b.alpha -= 8;
    if (b.alpha <= 0) {
      bursts.splice(i, 1);
    }
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

for-loop Expanding Rings for (let i = bursts.length - 1; i >= 0; i--) { const b = bursts[i]; stroke(255, 200, 80, b.alpha); circle(b.x, b.y, b.r); b.r += 4; b.alpha -= 8;

Draws expanding ring particles at catch locations, growing each frame and fading out

conditional Burst Removal if (b.alpha <= 0) { bursts.splice(i, 1); }

Removes the burst from the array once it has fully faded out, preventing memory buildup

noFill();
Disables fill so only the stroke (outline) is visible—creates rings instead of solid circles
strokeWeight(2);
Sets the ring stroke thickness to 2 pixels
for (let i = bursts.length - 1; i >= 0; i--) {
Loops backward through the bursts array so we can safely remove items
const b = bursts[i];
Shorthand to reference the current burst object
stroke(255, 200, 80, b.alpha);
Sets the ring color to orange (255, 200, 80) with alpha from the burst object—fades as alpha decreases
circle(b.x, b.y, b.r);
Draws the ring circle at the burst's position with radius b.r
b.r += 4;
Expands the radius by 4 pixels each frame, making the ring grow outward
b.alpha -= 8;
Decreases transparency by 8 each frame, making the ring fade out
if (b.alpha <= 0) {
Checks if the burst has become fully invisible
bursts.splice(i, 1);
Removes the burst from the array so it's no longer drawn or processed

renderSpiderMan(state)

This function acts as a coordinator: it receives the swing state (position and angle) and uses it to draw both the web lines and the character. The trailing web line uses the lagged trailingAngle calculated in computeSwingState(), creating a visual trail that follows behind the main motion—a subtle but effective depth cue.

function renderSpiderMan(state) {
  stroke(230, 240);
  strokeWeight(3);
  line(anchorX, anchorY, state.spideyX, state.spideyY);

  stroke(255, 255, 255, 80);
  strokeWeight(1);
  line(state.spideyX, state.spideyY, state.trailingX, state.trailingY);

  drawSpiderMan(state.spideyX, state.spideyY, state.swingAngle);
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Main Web Line stroke(230, 240); strokeWeight(3); line(anchorX, anchorY, state.spideyX, state.spideyY);

Draws the thick, bright web from the anchor point down to Spider-Man

calculation Trailing Web stroke(255, 255, 255, 80); strokeWeight(1); line(state.spideyX, state.spideyY, state.trailingX, state.trailingY);

Draws a thin, semi-transparent trailing line from Spider-Man to a lagged point, creating a motion blur effect

stroke(230, 240);
Sets the web stroke color to nearly white (230, 240 with implicit 255 for alpha)—bright and visible
strokeWeight(3);
Makes the main web thick (3 pixels) so it's prominent
line(anchorX, anchorY, state.spideyX, state.spideyY);
Draws a line from the anchor point at the top to Spider-Man's current position, representing the swinging web
stroke(255, 255, 255, 80);
Sets the trailing web to white with 80 alpha, making it semi-transparent and subtle
strokeWeight(1);
Makes the trailing web thin (1 pixel) so it doesn't overpower the main web
line(state.spideyX, state.spideyY, state.trailingX, state.trailingY);
Draws a subtle trailing line from Spider-Man to a point slightly behind, enhancing the sense of motion
drawSpiderMan(state.spideyX, state.spideyY, state.swingAngle);
Calls drawSpiderMan() to render the character itself at the calculated position and angle

drawSpiderMan(x, y, angle)

This function demonstrates two crucial p5.js patterns: push/pop for local transformations, and translate/rotate for drawing rotated objects. By translating to Spider-Man's position and rotating to his swing angle, we avoid complex math in every shape call—instead, we draw simple shapes around the origin (0,0) and let the transforms handle positioning. This is how all rotated game characters and objects work in p5.js.

🔬 This draws three shapes: torso, legs, shoulders. What happens if you change the torso size from 50, 70 to 70, 90? Or remove the legs rectangle entirely?

  ellipse(0, 30, 50, 70); // torso
  fill(30, 30, 140);
  rectMode(CENTER);
  rect(0, 70, 26, 40, 8); // legs group
  rect(0, 15, 38, 25, 10); // shoulders
function drawSpiderMan(x, y, angle) {
  push();
  translate(x, y);
  rotate(swingAngleOffset(angle));

  noStroke();
  fill(200, 0, 20);
  ellipse(0, 30, 50, 70); // torso
  fill(30, 30, 140);
  rectMode(CENTER);
  rect(0, 70, 26, 40, 8); // legs group
  rect(0, 15, 38, 25, 10); // shoulders

  fill(200, 0, 20);
  ellipse(0, -10, 36, 40); // mask

  fill(255);
  const eyeOffset = 10;
  const eyeWidth = 16;
  const eyeHeight = 12;
  ellipse(-eyeOffset, -10, eyeWidth, eyeHeight);
  ellipse(eyeOffset, -10, eyeWidth, eyeHeight);

  stroke(0);
  strokeWeight(2);
  noFill();
  arc(-eyeOffset, -10, eyeWidth, eyeHeight, PI + QUARTER_PI, TWO_PI - QUARTER_PI);
  arc(eyeOffset, -10, eyeWidth, eyeHeight, PI + QUARTER_PI, TWO_PI - QUARTER_PI);

  drawMaskWebbing();
  pop();
}
Line-by-line explanation (22 lines)

🔧 Subcomponents:

calculation Local Coordinate System push(); translate(x, y); rotate(swingAngleOffset(angle));

Moves and rotates the drawing origin to Spider-Man's position and swing angle, so all subsequent shapes are drawn relative to him

function-call-sequence Body Assembly noStroke(); fill(200, 0, 20); ellipse(0, 30, 50, 70); // torso fill(30, 30, 140); rectMode(CENTER); rect(0, 70, 26, 40, 8); // legs group rect(0, 15, 38, 25, 10); // shoulders

Draws the torso (red ellipse), legs (blue rectangle), and shoulders (blue rectangle)

function-call-sequence Mask & Eyes fill(200, 0, 20); ellipse(0, -10, 36, 40); // mask fill(255); const eyeOffset = 10; const eyeWidth = 16; const eyeHeight = 12; ellipse(-eyeOffset, -10, eyeWidth, eyeHeight); ellipse(eyeOffset, -10, eyeWidth, eyeHeight);

Draws the red mask shape and two white eye ovals

function-call-sequence Eye Pupils stroke(0); strokeWeight(2); noFill(); arc(-eyeOffset, -10, eyeWidth, eyeHeight, PI + QUARTER_PI, TWO_PI - QUARTER_PI); arc(eyeOffset, -10, eyeWidth, eyeHeight, PI + QUARTER_PI, TWO_PI - QUARTER_PI);

Draws black arcs inside the white eyes, creating the pupil outlines

push();
Saves the current transformation state (position, rotation, scale) so changes don't affect other drawings
translate(x, y);
Moves the origin (0,0) to Spider-Man's x,y position—all subsequent shapes are drawn relative to this point
rotate(swingAngleOffset(angle));
Rotates the entire coordinate system by the swing angle, so Spider-Man leans into the swing
noStroke();
Disables strokes for the body, so shapes are pure fills
fill(200, 0, 20);
Sets the fill color to Spider-Man red
ellipse(0, 30, 50, 70); // torso
Draws the torso as a vertical ellipse, positioned 30 pixels below the origin
fill(30, 30, 140);
Switches to dark blue for the legs and shoulders
rectMode(CENTER);
Changes rect drawing mode so rectangles are drawn from their center instead of top-left corner
rect(0, 70, 26, 40, 8); // legs group
Draws the legs rectangle 70 pixels below the origin, width 26, height 40, with rounded corners (8px radius)
rect(0, 15, 38, 25, 10); // shoulders
Draws the shoulders rectangle 15 pixels below the origin, width 38, height 25, with slight rounding
fill(200, 0, 20);
Switches back to red for the mask
ellipse(0, -10, 36, 40); // mask
Draws the mask as a vertical ellipse, positioned 10 pixels above the origin
fill(255);
Switches to white for the eyes
const eyeOffset = 10;
Stores the horizontal distance between the eye centers (makes eyes symmetric)
ellipse(-eyeOffset, -10, eyeWidth, eyeHeight);
Draws the left white eye, positioned 10 pixels to the left of center
ellipse(eyeOffset, -10, eyeWidth, eyeHeight);
Draws the right white eye, positioned 10 pixels to the right of center
stroke(0);
Switches to black stroke for the eye pupils
strokeWeight(2);
Makes the pupil outlines 2 pixels thick
arc(-eyeOffset, -10, eyeWidth, eyeHeight, PI + QUARTER_PI, TWO_PI - QUARTER_PI);
Draws a black arc in the left eye from about 5 o'clock to 11 o'clock, creating a 'looking down' pupil
arc(eyeOffset, -10, eyeWidth, eyeHeight, PI + QUARTER_PI, TWO_PI - QUARTER_PI);
Draws the matching arc in the right eye
drawMaskWebbing();
Calls a helper function to draw the vertical and curved webbing pattern on the mask
pop();
Restores the previous transformation state, undoing the translate and rotate so subsequent drawings aren't affected

swingAngleOffset(angle)

This tiny helper converts the swing angle into a body rotation angle. The map() function is the key tool here—it's a universal translator between any two numeric ranges. The negative correlation (higher angle → lower rotation) makes Spider-Man lean into the swing for a more natural pose.

function swingAngleOffset(angle) {
  return map(angle, -0.9, 0.9, 0.4, -0.4);
}
Line-by-line explanation (1 lines)
return map(angle, -0.9, 0.9, 0.4, -0.4);
map() converts the swing angle range (-0.9 to 0.9 radians) into a rotation range (0.4 to -0.4 radians). When Spider-Man swings left, he rotates to lean right, and vice versa—this makes him tilt into the swing naturally.

drawMaskWebbing()

This function adds the iconic Spider-Man mask webbing detail. It demonstrates two techniques: a for loop for evenly spaced parallel lines, and layered arcs for curved webbing. It's purely visual and doesn't affect gameplay, but it's the kind of polish that makes a sketch feel finished.

function drawMaskWebbing() {
  stroke(40);
  strokeWeight(1);
  for (let i = -15; i <= 15; i += 7) {
    line(i, -25, i, 5);
  }
  arc(0, -10, 30, 42, 0, PI);
  arc(0, -6, 24, 32, 0, PI);
  arc(0, -2, 18, 22, 0, PI);
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Vertical Web Lines for (let i = -15; i <= 15; i += 7) { line(i, -25, i, 5); }

Draws vertical lines across the mask to create the pattern of web lines

function-call-sequence Curved Web Arcs arc(0, -10, 30, 42, 0, PI); arc(0, -6, 24, 32, 0, PI); arc(0, -2, 18, 22, 0, PI);

Draws concentric arc curves across the mask, creating the web pattern's horizontal structure

stroke(40);
Sets the webbing color to dark gray (40, 40, 40)
strokeWeight(1);
Makes the webbing lines thin (1 pixel)
for (let i = -15; i <= 15; i += 7) {
Loops from -15 to 15 in steps of 7, drawing vertical lines evenly spaced across the mask
line(i, -25, i, 5);
Draws a vertical line at x position i from y = -25 to y = 5, covering the height of the mask
arc(0, -10, 30, 42, 0, PI);
Draws a dark arc from 0 to PI radians (a half circle) to create curved webbing; 0 to PI draws the top half
arc(0, -6, 24, 32, 0, PI);
Draws a smaller concentric arc, adding more web pattern layers
arc(0, -2, 18, 22, 0, PI);
Draws the innermost arc, completing the web pattern

drawHUD()

This simple function demonstrates text rendering and alignment in p5.js. The key idea is textAlign()—it controls which corner of the text anchors to the x,y position, just like rectMode() for rectangles. Template literals (backticks with ${variable}) are a clean way to embed variables in strings.

function drawHUD() {
  fill(255);
  textSize(28);
  textAlign(LEFT, TOP);
  text(`Score: ${score}`, 24, 20);

  textAlign(RIGHT, TOP);
  text(`Lives: ${lives}`, width - 24, 20);
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

text-render Score Text fill(255); textSize(28); textAlign(LEFT, TOP); text(`Score: ${score}`, 24, 20);

Displays the current score in large white text at the top left

text-render Lives Text textAlign(RIGHT, TOP); text(`Lives: ${lives}`, width - 24, 20);

Displays the remaining lives in large white text at the top right

fill(255);
Sets text color to white
textSize(28);
Sets text size to 28 pixels for readability
textAlign(LEFT, TOP);
Aligns text to the left horizontally and top vertically, so the text grows rightward and downward from its anchor point
text(`Score: ${score}`, 24, 20);
Draws the score string (using template literals to insert the score variable) at position (24, 20)
textAlign(RIGHT, TOP);
Aligns text to the right horizontally, so the next text draws leftward from its anchor point
text(`Lives: ${lives}`, width - 24, 20);
Draws the lives string at the top right (width - 24 positions it near the right edge)

drawInstructions()

This function displays helpful on-screen instructions. The semi-transparent white (alpha 200) makes it visible but not distracting. It only updates once per draw call because it's static text, not animated.

function drawInstructions() {
  fill(255, 200);
  textSize(16);
  textAlign(CENTER, BOTTOM);
  text(
    'Move anchor with ← / → or A / D • Catch energy orbs before they drop • Press R to restart',
    width / 2,
    height - 18
  );
}
Line-by-line explanation (4 lines)
fill(255, 200);
Sets text color to white with alpha 200 (slightly transparent for subtlety)
textSize(16);
Sets text size to 16 pixels for smaller, less intrusive display
textAlign(CENTER, BOTTOM);
Centers text horizontally and anchors it from the bottom, so it sits at the bottom of the screen
text(..., width / 2, height - 18);
Displays the instructions string centered horizontally (width / 2) and 18 pixels from the bottom

drawGameOverOverlay()

This overlay is a classic UI pattern: a semi-transparent dark layer that dims the game, plus prominent text for important messages. The semi-transparent rectangle (alpha 200) keeps the game visible behind it, reinforcing that you've failed but the game state is still there. The centered text and large sizes make the game over message unmissable.

function drawGameOverOverlay() {
  fill(0, 200);
  rect(0, 0, width, height);

  fill(255);
  textAlign(CENTER, CENTER);
  textSize(60);
  text('GAME OVER', width / 2, height / 2 - 40);
  textSize(28);
  text(`Final Score: ${score}`, width / 2, height / 2 + 10);
  text('Press R to swing again', width / 2, height / 2 + 60);
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Semi-Transparent Overlay fill(0, 200); rect(0, 0, width, height);

Draws a semi-transparent black rectangle covering the entire canvas to dim the game behind

function-call-sequence Game Over Text fill(255); textAlign(CENTER, CENTER); textSize(60); text('GAME OVER', width / 2, height / 2 - 40); textSize(28); text(`Final Score: ${score}`, width / 2, height / 2 + 10); text('Press R to swing again', width / 2, height / 2 + 60);

Displays the game over title, final score, and restart instruction in the center of the screen

fill(0, 200);
Sets fill to semi-transparent black (alpha 200)
rect(0, 0, width, height);
Draws a full-screen rectangle that dims the game behind it
fill(255);
Switches fill to white for the text
textAlign(CENTER, CENTER);
Centers text both horizontally and vertically
textSize(60);
Sets large text size (60 pixels) for the 'GAME OVER' title
text('GAME OVER', width / 2, height / 2 - 40);
Displays 'GAME OVER' centered horizontally and slightly above the middle of the screen
textSize(28);
Reduces text size to 28 pixels for the secondary text
text(`Final Score: ${score}`, width / 2, height / 2 + 10);
Displays the final score below the title
text('Press R to swing again', width / 2, height / 2 + 60);
Displays the restart instruction below the score

keyPressed()

keyPressed() is a p5.js event function that runs once per key press (different from keyIsDown(), which runs every frame a key is held). It's used here to detect the restart command. The key variable is automatically set by p5.js to the most recently pressed key.

function keyPressed() {
  if (key === 'r' || key === 'R') {
    resetGame();
  }
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

conditional Restart Trigger if (key === 'r' || key === 'R') { resetGame(); }

Checks if the player pressed R (lowercase or uppercase) and resets the game state

if (key === 'r' || key === 'R') {
Checks if the key pressed matches 'r' or 'R' (== compares values, === checks both value and type)
resetGame();
Calls the resetGame() function to restart the game

resetGame()

resetGame() is the nuclear option for restarting—it zeros out every game variable and array. Call this whenever you want to return to the initial state. Notice it resets orbInterval to its starting value, which is why difficulty ramps start over on each play.

function resetGame() {
  score = 0;
  lives = 3;
  orbs = [];
  bursts = [];
  orbInterval = 1800;
  lastOrbSpawn = 0;
  gameOver = false;
  anchorX = width * 0.5;
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Clear Score score = 0;

Resets the score to zero

calculation Restore Lives lives = 3;

Restores three lives

calculation Clear Entities orbs = []; bursts = [];

Empties the arrays so no lingering orbs or effects remain

calculation Reset Spawn Timing orbInterval = 1800; lastOrbSpawn = 0;

Resets the orb spawn timer and interval back to starting values

calculation Clear Game Over gameOver = false;

Sets gameOver to false so the game loop resumes and input works again

calculation Center Anchor anchorX = width * 0.5;

Moves the anchor back to the center of the canvas

score = 0;
Resets score to zero for a fresh game
lives = 3;
Restores three lives at the start
orbs = [];
Clears the orbs array by assigning it to a new empty array
bursts = [];
Clears the bursts array so no lingering particle effects remain
orbInterval = 1800;
Resets the spawn interval back to the starting value (1800ms), making early game easier
lastOrbSpawn = 0;
Resets the last spawn time to 0, so the first orb spawns immediately
gameOver = false;
Sets gameOver to false so draw() resumes updating and processing input
anchorX = width * 0.5;
Moves the anchor back to the center horizontally, giving the player a fresh starting position

drawSkyGradient()

This function demonstrates the map() and lerpColor() pattern for smooth color gradients. By drawing one horizontal line per pixel row and interpolating the color for each, we achieve a smooth gradient without complex shader code. This is a brute-force approach but very readable and effective for static backgrounds.

🔬 This loop draws a gradient from top to bottom by blending two colors. What if you swapped topColor and bottomColor? Or changed the colors to (255,0,0) and (0,0,255) for red to blue?

  const topColor = color(15, 10, 40);
  const bottomColor = color(220, 70, 90);
  for (let y = 0; y < height; y++) {
    const t = map(y, 0, height, 0, 1);
    stroke(lerpColor(topColor, bottomColor, t));
    line(0, y, width, y);
function drawSkyGradient() {
  const topColor = color(15, 10, 40);
  const bottomColor = color(220, 70, 90);
  for (let y = 0; y < height; y++) {
    const t = map(y, 0, height, 0, 1);
    stroke(lerpColor(topColor, bottomColor, t));
    line(0, y, width, y);
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Top and Bottom Colors const topColor = color(15, 10, 40); const bottomColor = color(220, 70, 90);

Defines the two endpoint colors: dark blue at top, warm reddish at bottom

for-loop Gradient Line Sweep for (let y = 0; y < height; y++) { const t = map(y, 0, height, 0, 1); stroke(lerpColor(topColor, bottomColor, t)); line(0, y, width, y); }

Draws horizontal lines from top to bottom, each with a color interpolated between the top and bottom colors

const topColor = color(15, 10, 40);
color() creates a color object representing dark blue (low R, low G, high B)
const bottomColor = color(220, 70, 90);
Defines a warm reddish color (high R, medium G, medium B) for the horizon
for (let y = 0; y < height; y++) {
Loops once for every pixel from top (y=0) to bottom (y=height)
const t = map(y, 0, height, 0, 1);
Converts the y position (0 to height) into a value between 0 and 1; at y=0, t=0 (top); at y=height, t=1 (bottom)
stroke(lerpColor(topColor, bottomColor, t));
lerpColor() interpolates between the two colors based on t; when t=0 it returns topColor, when t=1 it returns bottomColor, and values in between blend smoothly
line(0, y, width, y);
Draws a horizontal line across the full width at the current y position, using the interpolated color

drawStars()

This function animates stars with a twinkling effect using the pulse pattern. Each star's twinkleOffset ensures they twinkle at different times, creating a natural-looking sky rather than all stars pulsing in sync. The pulse affects both brightness (alpha) and size, which is why the twinkle is so noticeable.

🔬 The twinkle effect uses pulse for both alpha and size. What if you remove the pulse from the size, so stars only change brightness but not size? Or change 0.02 to 0.1 to make them twinkle faster?

    const pulse = map(sin(frameCount * 0.02 + star.twinkleOffset), -1, 1, 0.3, 1);
    fill(255, 255 * pulse);
    circle(star.x, star.y, star.size * pulse);
function drawStars() {
  noStroke();
  for (const star of stars) {
    const pulse = map(sin(frameCount * 0.02 + star.twinkleOffset), -1, 1, 0.3, 1);
    fill(255, 255 * pulse);
    circle(star.x, star.y, star.size * pulse);
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Twinkle Pulse const pulse = map(sin(frameCount * 0.02 + star.twinkleOffset), -1, 1, 0.3, 1);

Calculates a pulse value (0.3 to 1.0) that makes the star's brightness and size oscillate

function-call-sequence Animated Star Circles fill(255, 255 * pulse); circle(star.x, star.y, star.size * pulse);

Draws each star with brightness and size modulated by the pulse value

noStroke();
Disables strokes so stars are pure circular fills
for (const star of stars) {
Iterates over every star in the stars array using a for-of loop
const pulse = map(sin(frameCount * 0.02 + star.twinkleOffset), -1, 1, 0.3, 1);
sin(frameCount * 0.02 + star.twinkleOffset) oscillates between -1 and 1; map() converts that to 0.3–1, so stars never completely disappear (minimum brightness 0.3)
fill(255, 255 * pulse);
Sets fill to white (255, 255, 255) with alpha modulated by pulse; as pulse goes from 0.3 to 1, alpha goes from ~76 to 255
circle(star.x, star.y, star.size * pulse);
Draws the star circle, with size scaled by pulse—brighter stars are also larger, enhancing the twinkle effect

drawBuildings()

This function demonstrates nested loops (loops inside loops) and Perlin noise. noise() is a slow-changing random function perfect for organic effects like flickering lights—unlike random(), which jumps around, noise() produces smooth transitions. The nested r/c loops allow us to draw a 2D grid of windows efficiently. The ternary operator (?) is a compact if-then-else—great for simple true/false logic.

🔬 This draws flickering windows. What if you change frameCount * 0.02 to frameCount * 0.05? Or change the threshold from 0.55 to 0.5 or 0.9?

        const flicker = noise(c * 0.2, r * 0.2, frameCount * 0.02);
        const bright = flicker > 0.55 ? 230 : 50;
        fill(bright, bright * 0.8, 0);
        rect(
          b.x + padding + c * (winW + 6),
          height - b.h + padding + r * (winH + 10),
          winW,
          winH,
          3
        );
function drawBuildings() {
  noStroke();
  for (const b of buildings) {
    fill(b.hue);
    rect(b.x, height - b.h, b.w, b.h);

    const padding = 12;
    const winW = (b.w - padding * 2) / b.windowCols - 6;
    const winH = (b.h - padding * 2) / b.windowRows - 10;
    for (let r = 0; r < b.windowRows; r++) {
      for (let c = 0; c < b.windowCols; c++) {
        const flicker = noise(c * 0.2, r * 0.2, frameCount * 0.02);
        const bright = flicker > 0.55 ? 230 : 50;
        fill(bright, bright * 0.8, 0);
        rect(
          b.x + padding + c * (winW + 6),
          height - b.h + padding + r * (winH + 10),
          winW,
          winH,
          3
        );
      }
    }
  }
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

for-loop Building Blocks for (const b of buildings) { fill(b.hue); rect(b.x, height - b.h, b.w, b.h);

Draws each building as a colored rectangle anchored to the bottom of the canvas

nested-for-loop Window Grid for (let r = 0; r < b.windowRows; r++) { for (let c = 0; c < b.windowCols; c++) { const flicker = noise(c * 0.2, r * 0.2, frameCount * 0.02); const bright = flicker > 0.55 ? 230 : 50; fill(bright, bright * 0.8, 0); rect(...); } }

Draws each building's windows in a grid, with flickering brightness using Perlin noise

noStroke();
Disables strokes for crisp, solid shapes
for (const b of buildings) {
Iterates over every building object
fill(b.hue);
Sets the building color to the stored hue
rect(b.x, height - b.h, b.w, b.h);
Draws the building as a rectangle at (b.x, height - b.h) with width b.w and height b.h; (height - b.h) anchors it to the bottom
const padding = 12;
Defines padding (margin) inside the building before windows start
const winW = (b.w - padding * 2) / b.windowCols - 6;
Calculates window width: divide available space by number of columns, subtract 6 pixels for gaps
const winH = (b.h - padding * 2) / b.windowRows - 10;
Calculates window height similarly, subtracting 10 for gaps
for (let r = 0; r < b.windowRows; r++) {
Outer loop over rows
for (let c = 0; c < b.windowCols; c++) {
Inner loop over columns
const flicker = noise(c * 0.2, r * 0.2, frameCount * 0.02);
noise() generates a pseudo-random value (0–1) based on the window's row/column and frame; gives each window a unique flicker pattern
const bright = flicker > 0.55 ? 230 : 50;
Ternary operator: if flicker is above 0.55, the window is bright (230), otherwise dark (50)—creates on/off flickering
fill(bright, bright * 0.8, 0);
Sets window color to orange/yellow: R=bright, G=bright*0.8, B=0, so brighter windows are more saturated yellow
rect(...);
Draws the window rectangle at the calculated position with slight rounding (radius 3)

createCityscape()

This function is a procedural generation algorithm—it automatically creates a varied skyline every time the sketch loads or resizes. The while loop continues until buildings span the canvas, and random values ensure no two skylines are identical. This is the foundation of many game and graphics techniques.

function createCityscape() {
  buildings = [];
  let x = 0;
  while (x < width) {
    const w = random(70, 150);
    const h = random(height * 0.25, height * 0.65);
    buildings.push({
      x,
      w,
      h,
      windowRows: floor(random(3, 8)),
      windowCols: floor(random(3, 8)),
      hue: color(random(10, 30), random(10, 30), random(50, 90))
    });
    x += w + random(20, 60);
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

while-loop Building Placement while (x < width) { const w = random(70, 150); const h = random(height * 0.25, height * 0.65); buildings.push({...}); x += w + random(20, 60); }

Generates buildings with random widths, heights, and gap sizes until the skyline spans the entire canvas width

buildings = [];
Clears any previous buildings to start fresh
let x = 0;
Initializes the x position to the left edge of the canvas
while (x < width) {
Continues generating buildings until they span the entire canvas width
const w = random(70, 150);
Picks a random building width between 70 and 150 pixels
const h = random(height * 0.25, height * 0.65);
Picks a random height between 25% and 65% of the canvas height
windowRows: floor(random(3, 8)),
Generates a random number of window rows between 3 and 8 (floor removes decimals)
windowCols: floor(random(3, 8)),
Generates a random number of window columns
hue: color(random(10, 30), random(10, 30), random(50, 90))
Creates a unique brownish/gray color for the building by randomizing RGB values slightly
x += w + random(20, 60);
Moves x forward by the building width plus a random gap (20–60 pixels), positioning the next building

createStars()

This function populates the stars array with objects that have position, size, and a phase offset. Unlike buildings (which are deterministic based on width calculations), stars are purely random. The twinkleOffset ensures each star has its own twinkle rhythm, creating a living, dynamic sky.

function createStars() {
  stars = [];
  for (let i = 0; i < numStars; i++) {
    stars.push({
      x: random(width),
      y: random(height * 0.45),
      size: random(1, 3),
      twinkleOffset: random(TWO_PI)
    });
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Star Generation for (let i = 0; i < numStars; i++) { stars.push({...}); }

Creates 120 stars (numStars) with random positions in the upper half of the canvas

stars = [];
Clears any previous stars
for (let i = 0; i < numStars; i++) {
Loops numStars (120) times, creating one star per iteration
x: random(width),
Picks a random x position anywhere across the canvas width
y: random(height * 0.45),
Picks a random y position in the upper 45% of the canvas, so stars appear only in the sky, not behind buildings
size: random(1, 3),
Gives each star a random size between 1 and 3 pixels for variety
twinkleOffset: random(TWO_PI)
Assigns a random offset (0 to 2π) so each star twinkles independently at different phases

windowResized()

windowResized() is a p5.js event function that runs whenever the browser window is resized. It ensures the canvas expands/contracts with the window and regenerates procedural elements to fit the new dimensions. Without this, the game would look wrong on a resized window.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  createCityscape();
  createStars();
  anchorY = height * 0.08;
}
Line-by-line explanation (4 lines)
resizeCanvas(windowWidth, windowHeight);
p5.js function that resizes the canvas to match the new window dimensions
createCityscape();
Regenerates the cityscape so buildings fit the new canvas width
createStars();
Regenerates stars so they fit the new sky area
anchorY = height * 0.08;
Recalculates the anchor y position based on the new canvas height

📦 Key Variables

buildings array

Stores all building objects with their position, size, window layout, and color

let buildings = [];
stars array

Stores all star objects with their position, size, and twinkle phase offset

let stars = [];
orbs array

Stores all active energy orbs with position, size, speed, and phase for animation

let orbs = [];
bursts array

Stores all active particle burst effects with position, radius, and alpha for fade-out

let bursts = [];
numStars number

Constant controlling how many stars appear in the sky

const numStars = 120;
webLength number

Constant controlling how far Spider-Man hangs below the anchor point (web radius)

const webLength = 280;
swingSpeed number

Constant controlling how fast Spider-Man swings back and forth (pendulum frequency)

const swingSpeed = 0.025;
anchorX number

The horizontal position of the web anchor point, controlled by player input

let anchorX;
anchorY number

The vertical position of the web anchor point (stays fixed at top of screen)

let anchorY;
anchorBounds number

Constant that restricts anchor movement to 15%–85% of canvas width to keep Spider-Man on screen

const anchorBounds = 0.15;
score number

Tracks the player's current score (incremented by 10 per caught orb)

let score = 0;
lives number

Tracks remaining lives (starts at 3, decremented when orbs are missed)

let lives = 3;
lastOrbSpawn number

Stores the timestamp (in milliseconds) of the last orb spawn for timing new spawns

let lastOrbSpawn = 0;
orbInterval number

Time in milliseconds between orb spawns; decreases over time to increase difficulty

let orbInterval = 1800;
gameOver boolean

Flag that is true when the game has ended (lives <= 0), halting game logic and input processing

let gameOver = false;

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG updateOrbs() collision detection

The collision radius (orb.size * 0.5 + 24) doesn't account for the fact that Spider-Man's actual drawn size is much smaller; orbs can feel like they're caught from unrealistic distances

💡 Use a more accurate radius like 35 for Spider-Man, or calculate it based on the actual drawn ellipse size

PERFORMANCE drawSkyGradient()

Drawing one horizontal line for every pixel from top to bottom (height iterations) every frame is inefficient; on a 1080p monitor that's 1080 lines redrawn 60 times per second

💡 Move gradient rendering to setup() and store it in a createGraphics() buffer, or use a canvas shader for better performance

FEATURE Game mechanics

The game lacks audio feedback for catches, misses, or difficulty increases; visual feedback alone is less satisfying

💡 Add p5.sound library and play a bright 'ding' on catches, a lower tone on misses, and a notification sound when difficulty increases

STYLE computeSwingState() / drawSpiderMan()

The magic number 0.08 for trailing angle and swingAngleOffset()'s map ranges (0.4 to -0.4) are unexplained constants that make tuning the feel difficult

💡 Extract these as named constants at the top of the file (e.g., const TRAILING_LAG = 0.08, const BODY_LEAN_MAX = 0.4) with comments explaining their purpose

FEATURE Game state

There is no way to pause the game; a player who needs a break has to close the sketch

💡 Add a pause toggle (spacebar) that freezes the game loop but keeps the UI visible, allowing players to resume seamlessly

🔄 Code Flow

Code flow showing setup, draw, handleanchorinput, computeswingstate, updateorbs, spaworb, draworbs, drawbursts, renderspiderman, drawspiderman, swingangleoffset, drawmaskwebbing, drawhud, drawinstructions, drawgameoverovlay, keypressed, resetgame, drawskygradiient, drawstars, drawbuildings, createcityscape, createstars, windowresized

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

graph TD start[Start] --> setup[setup] setup --> canvas-creation[Canvas Setup] setup --> angle-mode[Angle Mode] setup --> anchor-initialization[Anchor Position] setup --> createcityscape[Create Cityscape] setup --> createstars[Create Stars] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-creation href "#sub-canvas-creation" click angle-mode href "#sub-angle-mode" click anchor-initialization href "#sub-anchor-initialization" click createcityscape href "#fn-createcityscape" click createstars href "#fn-createstars" draw --> background-render[Background Rendering] draw --> input-handling[Input Processing] draw --> swing-calculation[Swing State Computation] draw --> orb-update[Orb Update Phase] draw --> render-entities[Entity Rendering] draw --> ui-render[UI Layer] click draw href "#fn-draw" click background-render href "#sub-background-render" click input-handling href "#sub-input-handling" click swing-calculation href "#sub-swing-calculation" click orb-update href "#sub-orb-update" click render-entities href "#sub-render-entities" click ui-render href "#sub-ui-render" input-handling --> left-input[Left Movement] input-handling --> right-input[Right Movement] input-handling --> boundary-constraint[Boundary Enforcement] click left-input href "#sub-left-input" click right-input href "#sub-right-input" click boundary-constraint href "#sub-boundary-constraint" swing-calculation --> angle-calculation[Pendulum Angle] swing-calculation --> position-calculation[Spider-Man Position] swing-calculation --> trailing-web[Trailing Web Point] click angle-calculation href "#sub-angle-calculation" click position-calculation href "#sub-position-calculation" click trailing-web href "#sub-trailing-web" orb-update --> spawn-logic[Orb Spawn Timer] orb-update --> orb-motion[Orb Physics Update] orb-update --> collision-detection[Spider-Man Collision] orb-update --> death-condition[Off-Screen Check] click spawn-logic href "#sub-spawn-logic" click orb-motion href "#sub-orb-motion" click collision-detection href "#sub-collision-detection" click death-condition href "#sub-death-condition" orb-motion --> size-randomization[Random Orb Size] orb-motion --> orb-object[Orb Property Bundle] click size-randomization href "#sub-size-randomization" click orb-object href "#sub-orb-object" collision-detection --> glow-animation[Glow Pulsing] collision-detection --> burst-animation[Expanding Rings] collision-detection --> burst-cleanup[Burst Removal] click glow-animation href "#sub-glow-animation" click burst-animation href "#sub-burst-animation" click burst-cleanup href "#sub-burst-cleanup" render-entities --> draworbs[Draw Orbs] render-entities --> drawbursts[Draw Bursts] render-entities --> renderspiderman[Render Spider-Man] click draworbs href "#fn-draworbs" click drawbursts href "#fn-drawbursts" click renderspiderman href "#fn-renderspiderman" renderspiderman --> drawspiderman[Draw Spider-Man] renderspiderman --> main-web[Main Web Line] renderspiderman --> trailing-web-line[Trailing Web] click drawspiderman href "#fn-drawspiderman" click main-web href "#sub-main-web" click trailing-web-line href "#sub-trailing-web-line" ui-render --> score-display[Score Text] ui-render --> lives-display[Lives Text] ui-render --> drawinstructions[Draw Instructions] ui-render --> drawgameoverovlay[Draw Game Over Overlay] click score-display href "#sub-score-display" click lives-display href "#sub-lives-display" click drawinstructions href "#fn-drawinstructions" click drawgameoverovlay href "#fn-drawgameoverovlay" keypressed[keyPressed] --> restart-check[Restart Trigger] restart-check --> score-reset[Clear Score] restart-check --> lives-reset[Restore Lives] restart-check --> arrays-clear[Clear Entities] restart-check --> timing-reset[Reset Spawn Timing] restart-check --> flag-reset[Clear Game Over] restart-check --> anchor-reset[Center Anchor] click keypressed href "#fn-keypressed" click restart-check href "#sub-restart-check" click score-reset href "#sub-score-reset" click lives-reset href "#sub-lives-reset" click arrays-clear href "#sub-arrays-clear" click timing-reset href "#sub-timing-reset" click flag-reset href "#sub-flag-reset" click anchor-reset href "#sub-anchor-reset" drawskygradiient[Draw Sky Gradient] --> color-setup[Top and Bottom Colors] color-setup --> gradient-loop[Gradient Line Sweep] click drawskygradiient href "#fn-drawskygradiient" click color-setup href "#sub-color-setup" click gradient-loop href "#sub-gradient-loop" drawstars[Draw Stars] --> twinkle-calculation[Twinkle Pulse] drawstars --> star-rendering[Animated Star Circles] click drawstars href "#fn-drawstars" click twinkle-calculation href "#sub-twinkle-calculation" click star-rendering href "#sub-star-rendering" drawbuildings[Draw Buildings] --> building-bodies[Building Blocks] drawbuildings --> window-grid[Window Grid] drawbuildings --> building-generation[Building Placement] click drawbuildings href "#fn-drawbuildings" click building-bodies href "#sub-building-bodies" click window-grid href "#sub-window-grid" click building-generation href "#sub-building-generation" windowresized[windowResized] --> createcityscape windowresized --> createstars click windowresized href "#fn-windowresized"

❓ Frequently Asked Questions

What visuals can I expect from the Spider-Man Swing Game created in this p5.js sketch?

The sketch features a vibrant cityscape with animated stars, orbs, and a swinging Spider-Man character, all set against a dynamic sky gradient.

How can I interact with the Spider-Man Swing Game during gameplay?

Users can control Spider-Man's swinging motion by using the left and right arrow keys or the 'A' and 'D' keys to adjust the anchor point.

What creative coding concepts are demonstrated in this p5.js sketch?

This sketch showcases concepts such as real-time user input handling, trigonometric functions for simulating swinging motion, and dynamic object rendering.

Preview

Sketch 2026-03-02 23:21 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2026-03-02 23:21 - Code flow showing setup, draw, handleanchorinput, computeswingstate, updateorbs, spaworb, draworbs, drawbursts, renderspiderman, drawspiderman, swingangleoffset, drawmaskwebbing, drawhud, drawinstructions, drawgameoverovlay, keypressed, resetgame, drawskygradiient, drawstars, drawbuildings, createcityscape, createstars, windowresized
Code Flow Diagram