spider man

This sketch animates Spider-Man swinging back and forth on a web across a night-time cityscape filled with flickering building windows and twinkling stars. The character rotates and leans naturally with the swing motion while a dynamic sky gradient transitions from deep purple to sunset orange.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up Spider-Man's swing — Higher swingSpeed makes Spider-Man oscillate faster back and forth
  2. Make the web shorter — Lower webLength makes Spider-Man swing in a tighter arc closer to the anchor point
  3. Fill the sky with stars — Higher numStars creates a more crowded, magical night sky
  4. Make Spider-Man blue
  5. Create a slower, spookier anchor drift — The 0.4 multiplier in the anchor animation controls how fast it moves—lower values make it drift more slowly
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a cinematic Spider-Man animation that combines multiple p5.js techniques into one polished scene. Spider-Man swings on a web using pendulum physics (sine wave motion), while behind him a procedurally generated cityscape features buildings with flickering windows powered by Perlin noise, and the sky glows with a color gradient and twinkling stars. What makes this sketch special is how it layers different visual systems—the backdrop, the character, and the animation—into a cohesive animated scene.

The code is organized into three main sections: backdrop systems (sky gradient, stars, buildings with windows), the swinging physics and web drawing, and the Spider-Man character itself. By studying this sketch, you will learn how to build complex scenes by combining simpler parts, use trigonometric functions to create realistic motion, apply procedural techniques like Perlin noise for variety, and structure code so that each system updates independently. This is a masterclass in organizing a multi-system p5.js animation.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-screen canvas, switches to radians mode for trigonometry, generates a cityscape of random buildings, and scatters stars across the sky.
  2. Every frame, draw() clears and redraws the entire scene: a sky gradient from deep purple at the top to sunset orange at the bottom, twinkling stars that pulse using sine waves, and buildings with windows that flicker using Perlin noise.
  3. Spider-Man's swing is powered by a sine wave: the frameCount multiplied by swingSpeed produces a smooth back-and-forth pendulum motion, and the swing angle controls both his x/y position and his body rotation.
  4. The web anchor point moves slowly left and right along the top of the screen (using another sine wave), so Spider-Man traces an arc through space rather than swinging in place.
  5. A motion blur line trails slightly behind Spider-Man to enhance the sense of speed, and his body leans forward or backward depending on swing direction using swingAngleOffset().
  6. When the window resizes, windowResized() regenerates both the cityscape and stars so the animation always fills the screen.

🎓 Concepts You'll Learn

Trigonometric animation (sine waves)Pendulum physics simulationProcedural generation (random buildings)Perlin noise for organic variationColor gradients and lerpingTransform stacks (push/pop, translate, rotate)Motion blur and trailing effectsWindow resize handling

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch starts. It initializes the canvas size, sets up the coordinate system (radians), and pre-generates all the static data structures (buildings and stars) that draw() will use every frame.

function setup() {
  createCanvas(windowWidth, windowHeight);
  angleMode(RADIANS);
  createCityscape();
  createStars();
}
Line-by-line explanation (4 lines)
createCanvas(windowWidth, windowHeight);
Creates a full-screen canvas that matches the browser window dimensions
angleMode(RADIANS);
Tells p5.js to use radians (0 to TWO_PI) instead of degrees—required for Math.sin() to work intuitively with animation
createCityscape();
Calls the helper function to generate an array of random buildings with varying heights, window counts, and colors
createStars();
Calls the helper function to populate the stars array with random positions and twinkling offsets

draw()

draw() runs 60 times per second and redraws the entire scene each frame. Notice the function calls are ordered from background (sky) to foreground (Spider-Man) to create proper layering. This is the animation loop's heartbeat.

function draw() {
  drawSkyGradient();
  drawStars();
  drawBuildings();
  drawSpiderManSwing();
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

function-calls Backdrop rendering drawSkyGradient(); drawStars(); drawBuildings();

Draws the scene layers from back to front: sky, stars, then buildings—creates depth

function-call Character and animation drawSpiderManSwing();

Draws Spider-Man on his swinging web using updated physics each frame

drawSkyGradient();
Draws a gradient background from deep purple at top to sunset orange at bottom—sets the mood
drawStars();
Renders all stars with twinkling animation using sine wave pulses
drawBuildings();
Draws the cityscape—rectangular buildings with flickering windows that use Perlin noise for organic variation
drawSpiderManSwing();
Calculates Spider-Man's position based on swing physics and draws him swinging on the web

createCityscape()

This function demonstrates procedural generation: using randomness to create a unique cityscape every time the sketch runs. The while loop is a common pattern for filling space—keep adding objects until you run out of room. Each building is stored as an object with properties (x, w, h, windowRows, etc.) so drawBuildings() can later access them.

🔬 These two lines set building width and height. What happens if you change both ranges to be smaller, like random(40, 80) and random(height * 0.1, height * 0.3)? Do you get more, smaller buildings?

    const w = random(70, 150);
    const h = random(height * 0.25, height * 0.65);
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 generation loop while (x < width) { ... x += w + random(20, 60); }

Generates buildings left to right across the screen until they fill the entire width

object-creation Building object structure buildings.push({ x, w, h, windowRows, windowCols, hue });

Stores all data needed to draw and animate one building

buildings = [];
Empties the buildings array to start fresh (called when the window resizes)
let x = 0;
Initializes the x-position tracker at the left edge of the canvas
while (x < width) {
Keeps creating buildings until we've covered the entire screen width
const w = random(70, 150);
Gives each building a random width between 70 and 150 pixels for variety
const h = random(height * 0.25, height * 0.65);
Gives each building a random height—somewhere between 25% and 65% of screen height so they look realistic
windowRows: floor(random(3, 8)),
Randomly decides how many rows of windows this building will have (3 to 7 rows)
windowCols: floor(random(3, 8)),
Randomly decides how many columns of windows (3 to 7 columns)
hue: color(random(10, 30), random(10, 30), random(50, 90))
Creates a darkish, slightly reddish color for the building—each building gets its own hue for variation
x += w + random(20, 60);
Moves to the next building's starting position, adding a random gap between buildings

createStars()

Each star stores four properties: position (x, y), size, and twinkleOffset. The twinkleOffset is key—by giving every star a different offset, the sin() wave in drawStars() will evaluate at different phases, making some stars bright while others are dim. This creates the illusion of independent twinkling.

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 loop for (let i = 0; i < numStars; i++) { ... }

Creates numStars (120) individual star objects with randomized properties

stars = [];
Clears the stars array to regenerate when the window resizes
for (let i = 0; i < numStars; i++) {
Loops 120 times (numStars constant) to create that many star objects
x: random(width),
Gives each star a random x-position anywhere across the canvas width
y: random(height * 0.45),
Places stars only in the upper 45% of the screen, keeping them above the cityscape
size: random(1, 3),
Each star gets a random size between 1 and 3 pixels for visual variety
twinkleOffset: random(TWO_PI)
Gives each star a unique starting phase (0 to 2π) so they don't all twinkle in sync—creates a natural shimmer

drawSkyGradient()

This function demonstrates gradient rendering in p5.js: instead of using a built-in gradient function, it draws many thin lines with gradually changing colors. The map() and lerpColor() functions do the math—map() normalizes y to 0–1, and lerpColor() blends between two colors at that ratio. This technique is simple but powerful.

🔬 These two color() calls define the start and end of the gradient. What if you swap them—put the orange at the top and purple at the bottom? Or try a completely different color like color(100, 150, 200) for a daytime sky?

  const topColor = color(15, 10, 40);
  const bottomColor = color(220, 70, 90);
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:

for-loop Horizontal line gradient for (let y = 0; y < height; y++) { stroke(lerpColor(...)); line(...); }

Draws many thin horizontal lines, each a slightly different interpolated color, creating a smooth gradient

const topColor = color(15, 10, 40);
Deep purple color for the sky top (dark red=15, green=10, blue=40)
const bottomColor = color(220, 70, 90);
Sunset orange-red color for the horizon bottom (bright red=220, moderate green=70, blue=90)
for (let y = 0; y < height; y++) {
Loops from top of canvas (y=0) to bottom (y=height), drawing one line per pixel row
const t = map(y, 0, height, 0, 1);
Converts the pixel row (0 to height) into a blend factor (0 to 1): 0 at top, 1 at bottom
stroke(lerpColor(topColor, bottomColor, t));
Uses lerpColor() to smoothly interpolate between purple and orange based on t—creates the gradient
line(0, y, width, y);
Draws a horizontal line across the full width at this pixel row in the interpolated color

drawStars()

This function is a masterclass in animation: the sine wave generates smooth oscillation, frameCount advances the wave, twinkleOffset desynchronizes each star, and map() rescales the result to a useful range. The pulse value is reused for both brightness and size, multiplying effects for a more convincing twinkle.

🔬 The pulse value controls both alpha (brightness) and size. What happens if you use pulse for size but NOT for brightness—try fill(255, 255) instead? Stars will grow and shrink but stay equally bright—do they look less magical?

    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:

for-of-loop Star rendering loop for (const star of stars) { ... }

Iterates through every star object and draws it with a twinkling effect

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

Uses a sine wave oscillation to create a smooth brightness and size pulse over time

noStroke();
Disables outline drawing so stars are just filled circles
for (const star of stars) {
For-of loop: iterates through every star in the stars array
const pulse = map(sin(frameCount * 0.02 + star.twinkleOffset), -1, 1, 0.3, 1);
Creates a pulsing value: frameCount * 0.02 makes time move slowly, sin() oscillates -1 to 1, star.twinkleOffset desynchronizes each star, and map() rescales it to 0.3–1 so stars fade between 30% and 100% brightness
fill(255, 255 * pulse);
Sets fill color to white with alpha (opacity) multiplied by pulse, so stars brighten and dim
circle(star.x, star.y, star.size * pulse);
Draws a circle at the star's position with size scaled by pulse—stars shrink and grow as they twinkle

drawBuildings()

This function combines two key techniques: nested loops for 2D grid layout, and Perlin noise for procedural variation. Perlin noise creates smooth, natural-looking randomness—unlike random() which is chaotic, noise() produces correlated values, so nearby windows have similar brightness. This makes the flickering feel organic rather than random.

🔬 Perlin noise creates the flicker. What happens if you remove frameCount * 0.02 and just use noise(c * 0.2, r * 0.2)? Will the windows stop flickering and become static? Try it and predict what the skyline will look like!

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

    // Windows
    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-of-loop Building rendering for (const b of buildings) { ... }

Iterates through every building object and draws its rectangle and windows

nested-for-loops Window grid rendering for (let r = 0; r < b.windowRows; r++) { for (let c = 0; c < b.windowCols; c++) { ... }

Creates a 2D grid of windows using two nested loops (rows and columns)

conditional Window brightness based on Perlin noise const flicker = noise(c * 0.2, r * 0.2, frameCount * 0.02); const bright = flicker > 0.55 ? 230 : 50;

Uses Perlin noise to create organic, correlated flickering—windows near each other have similar brightness

noStroke();
Disables outlines so buildings and windows are just solid blocks
for (const b of buildings) {
For-of loop: iterates through every building in the buildings array
fill(b.hue);
Sets the fill color to the building's pre-generated hue (stored as a color object)
rect(b.x, height - b.h, b.w, b.h);
Draws the building rectangle: x, y positioned at bottom of screen minus height, width w, height h
const padding = 12;
Defines the margin (in pixels) between the building edge and the first window
const winW = (b.w - padding * 2) / b.windowCols - 6;
Calculates window width: total building width minus padding, divided by number of columns, minus spacing
const winH = (b.h - padding * 2) / b.windowRows - 10;
Calculates window height: total building height minus padding, divided by number of rows, minus spacing
for (let r = 0; r < b.windowRows; r++) {
Outer loop: iterates through each row of windows
for (let c = 0; c < b.windowCols; c++) {
Inner loop: iterates through each column of windows in this row
const flicker = noise(c * 0.2, r * 0.2, frameCount * 0.02);
Generates a Perlin noise value (0–1) seeded by column, row, and time—creates organic, smooth flickering
const bright = flicker > 0.55 ? 230 : 50;
If noise value is above 0.55, the window is bright (230), otherwise dim (50)—creates a flickering on/off effect
fill(bright, bright * 0.8, 0);
Sets window color to yellowish: (bright red, slightly less green, no blue) to look like city lights
rect( ... winW, winH, 3);
Draws a rounded rectangle window at the calculated row and column position with 3-pixel corner radius

drawSpiderManSwing()

This function is the physics heart of the sketch: using sine waves and trigonometry to convert a single angle parameter into smooth circular motion. The anchor point drifts independently (slower sine wave), and the motion blur line adds visual polish. This is textbook procedural animation.

🔬 These three lines define the pendulum motion using trigonometry: sin() controls horizontal swing, cos() controls vertical. What if you swap them—use sin() for Y and cos() for X? Will Spider-Man swing vertically instead of horizontally?

  const swingAngle = sin(t) * 0.85;
  const spideyX = anchorX + webLength * sin(swingAngle);
  const spideyY = anchorY + webLength * cos(swingAngle);
function drawSpiderManSwing() {
  const t = frameCount * swingSpeed;

  // Anchor moves slowly along the skyline
  const anchorX = map(sin(t * 0.4), -1, 1, width * 0.2, width * 0.8);
  const anchorY = height * 0.08;

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

  // Web line
  stroke(230, 240);
  strokeWeight(3);
  line(anchorX, anchorY, spideyX, spideyY); // https://p5js.org/reference/#/p5/line

  // Motion blur accent
  stroke(255, 255, 255, 80);
  strokeWeight(1);
  const trailingAngle = swingAngle - 0.08;
  const trailingX = anchorX + webLength * sin(trailingAngle);
  const trailingY = anchorY + webLength * cos(trailingAngle);
  line(spideyX, spideyY, trailingX, trailingY);

  drawSpiderMan(spideyX, spideyY, swingAngle);
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

calculation Time scaling const t = frameCount * swingSpeed;

Converts frameCount into a time variable that controls swing speed

calculation Anchor point animation const anchorX = map(sin(t * 0.4), -1, 1, width * 0.2, width * 0.8);

Makes the web anchor move left and right across the top of the screen

calculation Pendulum swing calculation const swingAngle = sin(t) * 0.85; const spideyX = anchorX + webLength * sin(swingAngle); const spideyY = anchorY + webLength * cos(swingAngle);

Uses trigonometry (sin/cos) to convert swing angle into x/y position—creates the pendulum arc

calculation Motion blur trail const trailingAngle = swingAngle - 0.08; const trailingX = anchorX + webLength * sin(trailingAngle); const trailingY = anchorY + webLength * cos(trailingAngle); line(spideyX, spideyY, trailingX, trailingY);

Draws a faint line from Spider-Man to a slightly earlier position, simulating motion blur

const t = frameCount * swingSpeed;
Multiplies frameCount by swingSpeed (0.025) to create a slower, controllable time variable
const anchorX = map(sin(t * 0.4), -1, 1, width * 0.2, width * 0.8);
Maps a slow sine wave (t * 0.4 is 4x slower than swingAngle) to x-positions between 20% and 80% of canvas width—anchor drifts side to side
const anchorY = height * 0.08;
Anchor stays fixed at 8% of screen height—near the top
const swingAngle = sin(t) * 0.85;
Creates the main swing angle using sine wave: oscillates between -0.85 and +0.85 radians
const spideyX = anchorX + webLength * sin(swingAngle);
Converts swing angle to x-offset using sin()—spider moves horizontally based on angle
const spideyY = anchorY + webLength * cos(swingAngle);
Converts swing angle to y-offset using cos()—spider moves down and up as he swings
stroke(230, 240);
Sets web line color to light gray/white
strokeWeight(3);
Makes the web line 3 pixels thick so it's visible against the background
line(anchorX, anchorY, spideyX, spideyY);
Draws the web from the anchor point to Spider-Man's current position
stroke(255, 255, 255, 80);
Sets motion blur line to white with alpha 80 (semi-transparent)
strokeWeight(1);
Makes the blur line thinner than the main web (1 pixel)
const trailingAngle = swingAngle - 0.08;
Calculates a slightly earlier swing angle (0.08 radians behind current) for the blur effect
const trailingX = anchorX + webLength * sin(trailingAngle); const trailingY = anchorY + webLength * cos(trailingAngle);
Converts the trailing angle to an earlier x/y position—where Spider-Man was a moment ago
line(spideyX, spideyY, trailingX, trailingY);
Draws a faint line from current position to earlier position, creating motion blur
drawSpiderMan(spideyX, spideyY, swingAngle);
Calls the character drawing function to render Spider-Man at his calculated position with current swing angle

drawSpiderMan()

This function demonstrates transform stacks: push() and pop() create a local coordinate system where Spider-Man is drawn relative to his own origin. All his body parts are drawn at (0, 0) in this local space, then the entire drawing is translated and rotated as a group. This is much cleaner than calculating absolute positions for every body part.

🔬 These shapes compose Spider-Man's body. What if you add another ellipse or rect of a different size or color between these shapes? Can you add a belt or utility pouch to make him more detailed?

  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
function drawSpiderMan(x, y, angle) {
  push();
  translate(x, y);
  rotate(swingAngleOffset(angle));

  // Body
  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

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

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

  // Eye outlines
  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);

  // Webbing lines on mask
  drawMaskWebbing();

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

🔧 Subcomponents:

transform Transform stack and positioning push(); translate(x, y); rotate(swingAngleOffset(angle));

Saves the drawing context, moves to Spider-Man's position, and rotates him based on swing angle

shape-drawing Body shape composition fill(200, 0, 20); ellipse(0, 30, 50, 70); // torso fill(30, 30, 140); rect(0, 70, 26, 40, 8); // legs group

Draws torso (red ellipse) and legs (blue rectangle) relative to origin

shape-drawing Mask and eyes fill(200, 0, 20); ellipse(0, -10, 36, 40); fill(255); ellipse(-eyeOffset, -10, eyeWidth, eyeHeight);

Draws Spider-Man's iconic red mask with white eye areas

shape-drawing Eye outlines and details arc(-eyeOffset, -10, eyeWidth, eyeHeight, PI + QUARTER_PI, TWO_PI - QUARTER_PI);

Draws black arc outlines on the white eyes to suggest expression

push();
Saves the current transform state (position, rotation) so we can restore it later with pop()
translate(x, y);
Moves the origin to Spider-Man's position—all following shapes are drawn relative to this point
rotate(swingAngleOffset(angle));
Rotates the entire Spider-Man drawing based on swing angle, creating a natural forward lean
noStroke();
Disables outlines for the body shapes
fill(200, 0, 20);
Sets fill color to Spider-Man red
ellipse(0, 30, 50, 70); // torso
Draws the red torso as an ellipse at (0, 30) with width 50, height 70
fill(30, 30, 140);
Changes fill to dark blue for the suit details
rectMode(CENTER);
Sets rectangle drawing mode so rect() draws from center (x, y) instead of top-left
rect(0, 70, 26, 40, 8); // legs group
Draws blue legs rectangle at (0, 70) with width 26, height 40, and 8-pixel corner radius
rect(0, 15, 38, 25, 10); // shoulders
Draws blue shoulders rectangle at (0, 15) centered—broader than legs
fill(200, 0, 20); ellipse(0, -10, 36, 40);
Draws the red mask (head) as an ellipse above the torso at (0, -10)
fill(255); ellipse(-eyeOffset, -10, eyeWidth, eyeHeight);
Draws left white eye ellipse at (-10, -10) with width 16, height 12
ellipse(eyeOffset, -10, eyeWidth, eyeHeight);
Draws right white eye ellipse at (+10, -10)—mirrored from left
stroke(0); strokeWeight(2); noFill();
Switches to black stroke, 2 pixels thick, no fill—for drawing eye outlines
arc(-eyeOffset, -10, eyeWidth, eyeHeight, PI + QUARTER_PI, TWO_PI - QUARTER_PI);
Draws a black arc on the left eye from angle PI+π/4 to 2π-π/4 (bottom half), creating an expression
arc(eyeOffset, -10, eyeWidth, eyeHeight, PI + QUARTER_PI, TWO_PI - QUARTER_PI);
Draws a matching arc on the right eye
drawMaskWebbing();
Calls helper function to draw the webbing pattern on the mask
pop();
Restores the previously saved transform state, undoing the translate() and rotate()

swingAngleOffset()

This is a pure mapping function: it takes the swing angle and converts it to a body lean angle. When Spider-Man swings left (negative angle), his body rotates forward positive; when he swings right, his body rotates backward negative. This creates the illusion of natural body mechanics.

function swingAngleOffset(angle) {
  // Slight forward lean depending on swing direction
  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);
Maps the swing angle (-0.9 to 0.9) to a rotation offset (0.4 to -0.4): when swinging left, Spider-Man leans forward-left; when swinging right, he leans forward-right

drawMaskWebbing()

This function creates Spider-Man's iconic mask pattern using simple geometry: vertical lines and horizontal arcs combine into a web-like grid. The arcs get progressively smaller to suggest perspective, making the mask look 3D despite being drawn in 2D.

🔬 This loop draws 5 vertical webbing lines with spacing 7. What if you change the step from i += 7 to i += 5? Will you get more lines drawn closer together? Try it and see the webbing pattern get finer!

  for (let i = -15; i <= 15; i += 7) {
    line(i, -25, i, 5);
  }
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 webbing lines for (let i = -15; i <= 15; i += 7) { line(i, -25, i, 5); }

Draws 5 vertical lines spaced 7 pixels apart to create vertical webbing ribs

arc-drawing Horizontal webbing arcs arc(0, -10, 30, 42, 0, PI); arc(0, -6, 24, 32, 0, PI); arc(0, -2, 18, 22, 0, PI);

Draws three horizontal arc lines to create the grid pattern of Spider-Man's mask webbing

stroke(40);
Sets stroke color to dark gray (40, 40, 40)
strokeWeight(1);
Makes webbing lines 1 pixel thick
for (let i = -15; i <= 15; i += 7) {
Loops with i = -15, -8, -1, 6, 13—five iterations to draw vertical lines
line(i, -25, i, 5);
Draws a vertical line from (i, -25) to (i, 5), spacing them 7 pixels apart
arc(0, -10, 30, 42, 0, PI);
Draws the top arc of the webbing grid—a semicircle at (0, -10) with width 30, height 42, from 0 to PI (bottom half of circle)
arc(0, -6, 24, 32, 0, PI);
Draws a smaller arc below the first one
arc(0, -2, 18, 22, 0, PI);
Draws the bottom arc—smallest one, creating perspective in the webbing grid

windowResized()

This function is called automatically by p5.js whenever the browser window is resized. It ensures that the canvas always fills the screen and regenerates the procedural elements (buildings and stars) so they adapt to the new dimensions. Without this, the sketch would look stretched or incomplete after a resize.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  createCityscape();
  createStars();
}
Line-by-line explanation (3 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the new browser window size
createCityscape();
Regenerates the buildings array with new procedurally generated buildings for the new canvas width
createStars();
Regenerates the stars array so stars fill the new canvas dimensions

📦 Key Variables

buildings array

Stores an array of building objects, each containing position (x), dimensions (w, h), window layout (windowRows, windowCols), and color (hue)

let buildings = [];
stars array

Stores an array of star objects, each containing position (x, y), size, and twinkleOffset for independent twinkling animation

let stars = [];
numStars number

Constant controlling how many stars are generated at startup (120)

const numStars = 120;
webLength number

Constant controlling the length of Spider-Man's web (280 pixels)—affects swing arc size

const webLength = 280;
swingSpeed number

Constant controlling how fast Spider-Man swings—multiplied by frameCount to produce slow, smooth motion (0.025)

const swingSpeed = 0.025;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

PERFORMANCE drawSkyGradient()

Drawing one line per pixel row (up to ~1000 lines per frame) is inefficient—this recalculates colors and renders hundreds of strokes continuously

💡 Pre-render the gradient to a graphics buffer in setup() once, then draw that buffer every frame instead: const gradientBuffer = createGraphics(width, height); then image(gradientBuffer) in draw()

BUG drawBuildings() window positioning

Window layout calculation may produce negative or zero window dimensions if padding is too large relative to building size, causing visual glitches or invisible windows

💡 Add validation: const winW = max(1, (b.w - padding * 2) / b.windowCols - 6); to ensure windows never disappear

STYLE drawMaskWebbing()

Hard-coded line positions and arc sizes (e.g., -15, 15, 7) are magic numbers with no explanation—difficult to tune or understand

💡 Add constants at the top of the function or sketch level: const maskRadius = 15, const maskLineSpacing = 7 for clarity and easier tweaking

FEATURE drawSpiderMan()

Spider-Man has no animation beyond rotation—his pose is completely static while swinging, which reduces the sense of dynamic motion

💡 Add arm and leg animation that respond to swingAngle: compute arm positions using sin(swingAngle + offset) to make limbs swing naturally, enhancing realism

🔄 Code Flow

Code flow showing setup, draw, createcityscape, createstars, drawskygradient, drawstars, drawbuildings, drawspidermanswing, drawspiderman, swingangleoffset, drawmaskwebbing, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> backdrop-layers[Backdrop Layers] draw --> character-render[Character Render] backdrop-layers --> drawskygradient[drawskygradient] backdrop-layers --> drawstars[drawstars] backdrop-layers --> drawbuildings[drawbuildings] character-render --> drawspidermanswing[drawspidermanswing] character-render --> drawspiderman[drawspiderman] drawbuildings --> building-loop[Building Loop] building-loop --> building-object[Building Object] building-object -->|stores| drawbuildings building-loop -->|fills space| drawbuildings drawstars --> star-loop[Star Loop] star-loop -->|creates| createstars[createstars] createstars -->|stores| drawstars drawskygradient --> gradient-loop[Gradient Loop] gradient-loop -->|draws| drawskygradient drawstars --> star-loop[Star Loop] star-loop --> twinkling-math[Twinkling Math] twinkling-math -->|calculates| drawstars drawbuildings --> building-loop[Building Loop] building-loop --> window-grid[Window Grid] window-grid --> flicker-logic[Flicker Logic] flicker-logic -->|uses| drawbuildings drawspidermanswing --> time-scaling[Time Scaling] time-scaling --> anchor-motion[Anchor Motion] anchor-motion --> pendulum-physics[Pendulum Physics] pendulum-physics --> motion-blur[Motion Blur] drawspiderman --> transform-setup[Transform Setup] transform-setup --> body-drawing[Body Drawing] body-drawing --> mask-drawing[Mask Drawing] mask-drawing --> eye-details[Eye Details] drawspiderman --> vertical-lines[Vertical Lines] vertical-lines --> horizontal-arcs[Horizontal Arcs] click setup href "#fn-setup" click draw href "#fn-draw" click backdrop-layers href "#sub-backdrop-layers" click character-render href "#sub-character-render" click drawskygradient href "#fn-drawskygradient" click drawstars href "#fn-drawstars" click drawbuildings href "#fn-drawbuildings" click drawspidermanswing href "#fn-drawspidermanswing" click drawspiderman href "#fn-drawspiderman" click building-loop href "#sub-building-loop" click building-object href "#sub-building-object" click star-loop href "#sub-star-loop" click gradient-loop href "#sub-gradient-loop" click twinkling-math href "#sub-twinkling-math" click window-grid href "#sub-window-grid" click flicker-logic href "#sub-flicker-logic" click time-scaling href "#sub-time-scaling" click anchor-motion href "#sub-anchor-motion" click pendulum-physics href "#sub-pendulum-physics" click motion-blur href "#sub-motion-blur" click transform-setup href "#sub-transform-setup" click body-drawing href "#sub-body-drawing" click mask-drawing href "#sub-mask-drawing" click eye-details href "#sub-eye-details" click vertical-lines href "#sub-vertical-lines" click horizontal-arcs href "#sub-horizontal-arcs"

❓ Frequently Asked Questions

What visual elements does the Spider-Man sketch create?

The sketch features a dynamic cityscape with buildings, a gradient sky, and twinkling stars, all set against the backdrop of a swinging Spider-Man.

Is there any interactivity in the Spider-Man sketch for users?

The sketch currently does not include interactive elements; it focuses on animation and visual effects.

What creative coding techniques are demonstrated in the Spider-Man animation?

The sketch showcases techniques such as procedural generation for cityscapes and stars, as well as gradient rendering for the sky.

Preview

spider man - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of spider man - Code flow showing setup, draw, createcityscape, createstars, drawskygradient, drawstars, drawbuildings, drawspidermanswing, drawspiderman, swingangleoffset, drawmaskwebbing, windowresized
Code Flow Diagram