GUESS WHO!!!

This sketch creates a hilariously dim-witted zombie jockey riding on a wobbly chicken. The bot blinks randomly, tracks the mouse with laggy eyes, and twitches awkwardly while the chicken beneath it wobbles side to side—a delightfully stupid animation powered by noise, transforms, and eye-tracking logic.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the zombie's shirt red — Change the t-shirt color to red and watch the jockey instantly shift from blue to crimson
  2. Speed up the chicken's wobble — Increase the wobble increment so the chicken rocks faster side to side instead of lazily swaying
  3. Make the bot blink much more often — Reduce the blink timer range so the zombie's eyes close almost constantly, making it look more nervous
  4. Make the eyes track the mouse sharply — Increase the lerp factor so the pupils snap to your cursor instead of lagging behind stupidly
  5. Paint the background dark blue — Change the background color from light gray to a dark blue for a spookier mood
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch animates a ridiculous scene: a vacant-eyed zombie jockey riding on a constantly wobbling chicken. The visual humor comes from several layered animations working together—the chicken sways with Perlin noise, the bot rides on top with its own independent wobble, and its eyes track your mouse cursor but with a deliberate lag that makes it look stupidly disconnected. Random blinks complete the vacant stare.

The code teaches you how to build complex animations by layering multiple p5.js techniques: the push() and translate() functions stack coordinate systems so the bot moves relative to the chicken, Perlin noise creates organic-looking wobbles instead of mechanical swinging, lerp() smooths the eye tracking into a lazy follow, and conditional logic handles the blinking state machine. By studying it you'll learn how professional animations layer transforms, how to fake character personality through animation timing, and how to make interactive elements (like eye tracking) feel intentionally broken for comedic effect.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas and positions the chicken at the center with the bot sitting above it. Both start with wobble offsets at zero.
  2. Every frame, draw() clears the background and calculates the chicken's new wobbly position using Perlin noise, then uses translate() to shift the drawing origin to the chicken's center.
  3. The bot is drawn relative to the chicken using a second translate() call—this means all the bot's movements are layered on top of the chicken's, creating a nested animation where they move together but jiggle independently.
  4. Before drawing the bot, the eye-tracking logic calculates where the mouse is and uses lerp() to slowly move the pupil positions toward the target, creating a laggy, dumb-looking gaze effect.
  5. The blink system uses a timer that counts down randomly; when it hits zero, the blinking flag triggers and the eyes draw as lines instead of circles for a few frames.
  6. On every frame, the arms and legs twitch slightly using sin() and cos() multiplied by frameCount, adding jerky zombie-like movements that reinforce the character's stupidity.

🎓 Concepts You'll Learn

Perlin noise for organic motionTransform stacking with push/translate/popRelative positioning and coordinate systemsLerp for smooth interpolationState machines and timersInteractive tracking with constrainTrigonometric animation with sin/cos

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It's where you initialize the canvas size and set up starting values for global variables that both characters will use.

function setup() {
  createCanvas(windowWidth, windowHeight);
  // Initialize chicken position to the center of the canvas
  chickenX = width / 2;
  chickenY = height / 2 + botSize * 0.7; // Position chicken slightly below center so bot can ride
  chickenSize = botSize * 2; // Chicken is twice the size of the bot
  noStroke(); // Disable drawing outlines by default
}
Line-by-line explanation (5 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, allowing the animation to scale responsively
chickenX = width / 2;
Sets the chicken's starting x position to the horizontal center of the canvas
chickenY = height / 2 + botSize * 0.7;
Positions the chicken slightly below the vertical center so there's room for the bot to sit on top without going off-screen
chickenSize = botSize * 2;
Scales the chicken to be twice as large as the bot, establishing a size hierarchy between the two characters
noStroke();
Disables outlines on all shapes drawn afterward, giving the characters a filled-solid appearance by default

draw()

draw() is the heartbeat of the animation, running 60 times per second. Every frame it updates positions using Perlin noise, applies transforms to create nested coordinate systems, and redraws everything. The key insight: push/translate/pop lets you move the entire 'world' so you can draw shapes relative to new origins, making character assembly much easier.

🔬 These four lines layer TWO wobbles on the bot: a slow base wobble and a fast twitch. What happens if you delete the last two lines? What happens if you change the -5, 5 ranges to -20, 20 to make the twitch huge?

  botRelativeX += map(noise(timeOffset), 0, 1, -20, 20);
  botRelativeY += map(noise(timeOffset + 100), 0, 1, -10, 10);
  // Add a faster, smaller jiggle for extra stupidness
  botRelativeX += map(noise(timeOffset * 5), 0, 1, -5, 5);
  botRelativeY += map(noise(timeOffset * 5 + 200), 0, 1, -5, 5);

🔬 This is the magic of transforms: we translate to the chicken's center, then draw it, then translate AGAIN relative to the chicken to place the bot. If you change botRelativeY from -chickenSize * 0.7 to chickenSize * 0.3, where will the bot end up—above, below, or inside the chicken?

  push(); // Save the current drawing state (like translation)
  translate(chickenX, chickenY); // Move origin to the chicken's center

  drawChicken(); // Draw the chicken

  // --- Bot Movement (relative to chicken) ---
  // The bot's base position is now relative to the chicken's back
  // Adjust this based on where the bot should sit on the chicken
  let botRelativeX = 0;
  let botRelativeY = -chickenSize * 0.7; // Position the bot above the chicken's body
function draw() {
  background(220); // Light gray background

  // --- Chicken Movement ---
  // Update chicken position with noise for a more pronounced wobbly effect
  // Its movement is slower and larger than the bot's
  chickenX = width / 2 + map(noise(chickenWobbleOffset), 0, 1, -60, 60);
  chickenY = height / 2 + botSize * 0.7 + map(noise(chickenWobbleOffset + 100), 0, 1, -30, 30);
  chickenWobbleOffset += 0.008; // Slower wobble for the chicken

  push(); // Save the current drawing state (like translation)
  translate(chickenX, chickenY); // Move origin to the chicken's center

  drawChicken(); // Draw the chicken

  // --- Bot Movement (relative to chicken) ---
  // The bot's base position is now relative to the chicken's back
  // Adjust this based on where the bot should sit on the chicken
  let botRelativeX = 0;
  let botRelativeY = -chickenSize * 0.7; // Position the bot above the chicken's body

  // Bot's individual wobbly movement (layered on top of chicken's movement)
  botRelativeX += map(noise(timeOffset), 0, 1, -20, 20);
  botRelativeY += map(noise(timeOffset + 100), 0, 1, -10, 10);
  // Add a faster, smaller jiggle for extra stupidness
  botRelativeX += map(noise(timeOffset * 5), 0, 1, -5, 5);
  botRelativeY += map(noise(timeOffset * 5 + 200), 0, 1, -5, 5);

  translate(botRelativeX, botRelativeY); // Translate again for the bot's position relative to chicken
  drawBot(); // Draw the bot

  pop(); // Restore the previous drawing state

  timeOffset += 0.01; // Bot's own timeOffset for its individual wobble
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

calculation Chicken Wobble Position chickenX = width / 2 + map(noise(chickenWobbleOffset), 0, 1, -60, 60);

Uses Perlin noise to create smooth, organic side-to-side movement for the chicken's horizontal position

calculation Bot Relative Wobble botRelativeX += map(noise(timeOffset), 0, 1, -20, 20);

Adds the bot's own independent wobble on top of the chicken's movement, creating a layered jiggling effect

calculation Transform Coordinate Stacking translate(chickenX, chickenY); drawChicken(); // ... translate(botRelativeX, botRelativeY); drawBot();

Uses nested translate() calls to position the bot relative to the chicken, so they move together as a unit

background(220); // Light gray background
Clears the canvas every frame with a light gray color, erasing the previous frame's drawings
chickenX = width / 2 + map(noise(chickenWobbleOffset), 0, 1, -60, 60);
Calculates the chicken's horizontal position using Perlin noise, creating smooth side-to-side wobbling that swings 60 pixels left and right of center
chickenWobbleOffset += 0.008; // Slower wobble for the chicken
Advances the noise input very slowly (0.008 per frame), making the wobble feel lazy and organic rather than zippy
push(); // Save the current drawing state (like translation)
Saves the current transformation state so we can translate multiple times and then restore everything with pop()
translate(chickenX, chickenY); // Move origin to the chicken's center
Shifts the drawing origin to the chicken's center point, so all subsequent draws (like the chicken itself) are positioned relative to this new origin
drawChicken(); // Draw the chicken
Calls the function that draws all the chicken's body parts (body, head, beak, legs, etc.) at the newly translated origin
let botRelativeX = 0;
Initializes the bot's horizontal offset relative to the chicken—starting at zero means the bot will sit centered on the chicken's back horizontally
let botRelativeY = -chickenSize * 0.7; // Position the bot above the chicken's body
Positions the bot 70% of the chicken's size above its center, so the zombie sits on the chicken's shoulders rather than floating or sinking into it
botRelativeX += map(noise(timeOffset), 0, 1, -20, 20);
Adds the bot's own independent horizontal wobble using a different noise input (timeOffset), creating a jiggle on top of the chicken's movement
botRelativeX += map(noise(timeOffset * 5), 0, 1, -5, 5);
Adds a faster, smaller jiggle (multiplied by 5) to the bot's wobble, creating twitchy, erratic stupidity
translate(botRelativeX, botRelativeY); // Translate again for the bot's position relative to chicken
Applies a second translate() to move the origin to where the bot should sit, relative to the chicken's center
drawBot(); // Draw the bot
Draws the zombie jockey at the newly translated position, which is now on top of the chicken
pop(); // Restore the previous drawing state
Undoes both translate() calls, restoring the original coordinate system for the next frame
timeOffset += 0.01; // Bot's own timeOffset for its individual wobble
Increments the bot's noise input much faster than the chicken's (0.01 vs 0.008), making its wobble cycle quicker and more jittery

drawChicken()

drawChicken() is a pure drawing function—it doesn't update any global state, it just draws shapes relative to the current origin (which was set by translate() in draw()). This function demonstrates how to assemble a character from simple shapes (ellipses, triangles, arcs, lines) by using relative positioning. All dimensions scale automatically because they're calculated from chickenSize.

🔬 These two lines draw the left and right legs. What if you change legLength to legLength * 2? How would a double-legged chicken look? What if you change it to legLength * 0.2?

  // Legs (yellow)
  stroke(255, 200, 0);
  strokeWeight(5);
  line(-chickenSize * 0.3, chickenSize * 0.5, -chickenSize * 0.3, chickenSize * 0.5 + legLength);
  line(chickenSize * 0.3, chickenSize * 0.5, chickenSize * 0.3, chickenSize * 0.5 + legLength);

🔬 The head is positioned at chickenSize * 0.7 horizontally (rightward) and -chickenSize * 0.7 vertically (upward). If you swap these to be chickenSize * 0.2 instead, the head will move toward the center of the body. What would that look like visually?

  // Neck and Head
  ellipse(chickenSize * 0.6, -chickenSize * 0.5, headSize * 0.8, headSize * 1.2); // Neck
  ellipse(chickenSize * 0.7, -chickenSize * 0.7, headSize, headSize); // Head
function drawChicken() {
  let bodyWidth = chickenSize * 1.5;
  let bodyHeight = chickenSize * 1.2;
  let headSize = chickenSize * 0.5;
  let beakSize = chickenSize * 0.2;
  let combHeight = chickenSize * 0.2;
  let wattleHeight = chickenSize * 0.15;
  let legLength = chickenSize * 0.6;
  let footLength = chickenSize * 0.3;

  // Body (light brown/yellowish)
  fill(255, 240, 180); // Light yellow
  ellipse(0, 0, bodyWidth, bodyHeight);

  // Neck and Head
  ellipse(chickenSize * 0.6, -chickenSize * 0.5, headSize * 0.8, headSize * 1.2); // Neck
  ellipse(chickenSize * 0.7, -chickenSize * 0.7, headSize, headSize); // Head

  // Beak (yellow)
  fill(255, 200, 0);
  triangle(
    chickenSize * 0.7 + headSize * 0.25, -chickenSize * 0.7 - headSize * 0.1,
    chickenSize * 0.7 + headSize * 0.25, -chickenSize * 0.7 + headSize * 0.1,
    chickenSize * 0.7 + headSize * 0.25 + beakSize, -chickenSize * 0.7
  );

  // Comb (red)
  fill(200, 0, 0);
  arc(chickenSize * 0.7, -chickenSize * 0.7 - headSize * 0.2, headSize * 0.6, combHeight, PI, TWO_PI);
  ellipse(chickenSize * 0.7, -chickenSize * 0.7 - headSize * 0.2 - combHeight * 0.3, headSize * 0.3, combHeight * 0.5);

  // Wattle (red)
  arc(chickenSize * 0.7, -chickenSize * 0.7 + headSize * 0.2, headSize * 0.2, wattleHeight, 0, PI);

  // Eye (black)
  fill(0);
  ellipse(chickenSize * 0.7 + headSize * 0.1, -chickenSize * 0.7 - headSize * 0.05, headSize * 0.1, headSize * 0.1);

  // Wings (light brown)
  fill(255, 230, 160);
  arc(-bodyWidth * 0.25, bodyHeight * 0.1, bodyWidth * 0.6, bodyHeight * 0.4, 0, PI);
  arc(bodyWidth * 0.25, bodyHeight * 0.1, bodyWidth * 0.6, bodyHeight * 0.4, 0, PI);

  // Legs (yellow)
  stroke(255, 200, 0);
  strokeWeight(5);
  line(-chickenSize * 0.3, chickenSize * 0.5, -chickenSize * 0.3, chickenSize * 0.5 + legLength);
  line(chickenSize * 0.3, chickenSize * 0.5, chickenSize * 0.3, chickenSize * 0.5 + legLength);

  // Feet (yellow)
  line(-chickenSize * 0.3, chickenSize * 0.5 + legLength, -chickenSize * 0.3 - footLength * 0.5, chickenSize * 0.5 + legLength + footLength * 0.2);
  line(-chickenSize * 0.3, chickenSize * 0.5 + legLength, -chickenSize * 0.3 + footLength * 0.5, chickenSize * 0.5 + legLength + footLength * 0.2);
  line(chickenSize * 0.3, chickenSize * 0.5 + legLength, chickenSize * 0.3 - footLength * 0.5, chickenSize * 0.5 + legLength + footLength * 0.2);
  line(chickenSize * 0.3, chickenSize * 0.5 + legLength, chickenSize * 0.3 + footLength * 0.5, chickenSize * 0.5 + legLength + footLength * 0.2);
  noStroke(); // Reset stroke
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Chicken Body Assembly ellipse(0, 0, bodyWidth, bodyHeight);

Draws the main body as an ellipse at the origin (the center point passed in from the translate)

calculation Head and Beak ellipse(chickenSize * 0.7, -chickenSize * 0.7, headSize, headSize); // Head

Positions the head at an offset from the body origin, creating the neck connection point

for-loop Legs and Feet line(-chickenSize * 0.3, chickenSize * 0.5, -chickenSize * 0.3, chickenSize * 0.5 + legLength);

Draws the legs extending downward from the body using stroked lines

let bodyWidth = chickenSize * 1.5;
Scales all body dimensions relative to the global botSize variable, so changing botSize in setup() resizes the entire chicken automatically
fill(255, 240, 180); // Light yellow
Sets the fill color to light yellow for the chicken's body and subsequent shapes until a different fill() is called
ellipse(0, 0, bodyWidth, bodyHeight);
Draws the chicken's main body as an ellipse centered at (0, 0), which is the position this function was translate()-d to
ellipse(chickenSize * 0.6, -chickenSize * 0.5, headSize * 0.8, headSize * 1.2); // Neck
Draws a slightly squished ellipse to represent the chicken's neck, positioned above and to the side of the body to create a natural connection
triangle(...);
Uses three coordinate pairs to draw the beak as a triangle pointing rightward from the chicken's head
arc(chickenSize * 0.7, -chickenSize * 0.7 - headSize * 0.2, headSize * 0.6, combHeight, PI, TWO_PI);
Draws an arc (the red comb on top of the head) from angle PI to TWO_PI, creating a curved ridge shape
stroke(255, 200, 0);
Changes the stroke color to yellow for the legs and feet that follow
strokeWeight(5);
Sets the thickness of the stroke lines to 5 pixels, making the legs visible and prominent
line(-chickenSize * 0.3, chickenSize * 0.5, -chickenSize * 0.3, chickenSize * 0.5 + legLength);
Draws a straight line from the chicken's body down to the foot, representing the left leg
noStroke(); // Reset stroke
Disables stroke drawing for subsequent shapes in the next function, resetting the drawing state so the bot doesn't inherit these stroke settings

drawBot()

drawBot() is the most complex drawing function because it combines static shapes (body, head, clothing) with dynamic animations (eye tracking, blinking, arm twitching). The eye tracking uses map() and constrain() to calculate target positions, then lerp() to smoothly approach them. The blink system is a simple state machine: a timer counts down, triggers a state change, counts down again, and resets. This pattern (timer-based state transitions) is fundamental to character animation.

🔬 This is the blink timer: it counts down and when it hits zero, a random new timer is set. The range is 120–360 frames. If you change that to random(30, 60), blinks will happen much faster. What visual effect does this create—does the bot look more nervous or more sleepy?

  // --- Random Blinks ---
  if (!blinking) {
    if (blinkTimer <= 0) {
      blinkTimer = floor(random(120, 360)); // Between 2 to 6 seconds at 60fps
    }
    blinkTimer--;
  }

🔬 Both arms have armTwitch applied, but the right arm subtracts it (- armTwitch) while the left adds it (+ armTwitch). This makes them twitch opposite each other. What happens if you remove the minus sign and make both lines add armTwitch—would the arms look more or less coordinated?

  // Left arm (offset slightly)
  line(-botSize * 0.8, 0 + armTwitch * botSize, -botSize * 0.4, botSize * 0.2 + armTwitch * botSize);
  // Right arm (offset slightly)
  line(botSize * 0.8, 0 - armTwitch * botSize, botSize * 0.4, botSize * 0.2 - armTwitch * botSize);
function drawBot() {
  // Body (zombie-like dull green/gray)
  fill(90, 110, 90); // Slightly darker dull green for body to contrast clothes
  ellipse(0, 0, botSize * 1.2, botSize * 1.5); // Wider and taller than the head

  // --- Clothing ---
  // T-shirt (light blue)
  fill(173, 216, 230); // Light blue
  rect(-botSize * 0.6, -botSize * 0.6, botSize * 1.2, botSize * 0.8); // Main shirt body
  rect(-botSize * 0.7, -botSize * 0.4, botSize * 0.2, botSize * 0.4); // Left sleeve
  rect(botSize * 0.5, -botSize * 0.4, botSize * 0.2, botSize * 0.4); // Right sleeve

  // Pants (dark blue)
  fill(0, 0, 139); // Dark blue
  rect(-botSize * 0.6, botSize * 0.2, botSize * 1.2, botSize * 0.55); // Main pants body


  // Head (zombie-like dull green/gray)
  fill(110, 130, 110); // Slightly lighter dull green
  ellipse(0, -botSize * 0.4, botSize, botSize); // Positioned above the body

  // --- Lagging Eye Tracking (for vacant stare) ---
  let pupilSize = botSize * 0.08; // Smaller pupils for vacant stare
  let eyeOffset = botSize * 0.2; // Distance of eyes from the center

  // Calculate target pupil position based on mouse, but make it very subtle
  // This global tracking makes its gaze slightly disconnected, adding to its stupidity.
  let targetPupilX = map(mouseX, 0, width, -eyeOffset / 4, eyeOffset / 4); // Reduced range
  let targetPupilY = map(mouseY, 0, height, -eyeOffset / 4, eyeOffset / 4); // Reduced range

  targetPupilX = constrain(targetPupilX, -eyeOffset / 4, eyeOffset / 4);
  targetPupilY = constrain(targetPupilY, -eyeOffset / 4, eyeOffset / 4);

  currentPupilX = lerp(currentPupilX, targetPupilX, eyeLagFactor * 0.5); // Even slower lag
  currentPupilY = lerp(currentPupilY, targetPupilY, eyeLagFactor * 0.5);

  // --- Random Blinks ---
  if (!blinking) {
    if (blinkTimer <= 0) {
      blinkTimer = floor(random(120, 360)); // Between 2 to 6 seconds at 60fps
    }
    blinkTimer--;
  }

  if (blinkTimer <= 0 && !blinking) {
    blinking = true;
    blinkTimer = blinkDuration;
  }

  if (blinking) {
    blinkTimer--;
    if (blinkTimer <= 0) {
      blinking = false;
    }
  }

  // Draw Eyes
  // Left Eye
  fill(255, 255, 200); // Sickly yellow-white eye background
  ellipse(-eyeOffset, -botSize * 0.5, botSize * 0.4, botSize * 0.4); // White part of the eye
  if (!blinking) {
    fill(0); // Black pupil
    ellipse(-eyeOffset + currentPupilX, -botSize * 0.5 + currentPupilY, pupilSize, pupilSize); // Pupil following mouse
  } else {
    // During blink, draw a thin black line
    stroke(0);
    strokeWeight(2);
    line(-eyeOffset - botSize * 0.2, -botSize * 0.5, -eyeOffset + botSize * 0.2, -botSize * 0.5);
    noStroke();
  }

  // Right Eye
  fill(255, 255, 200); // Sickly yellow-white eye background
  ellipse(eyeOffset, -botSize * 0.5, botSize * 0.4, botSize * 0.4);
  if (!blinking) {
    fill(0); // Black pupil
    ellipse(eyeOffset + currentPupilX, -botSize * 0.5 + currentPupilY, pupilSize, pupilSize);
  } else {
    // During blink, draw a thin black line
    stroke(0);
    strokeWeight(2);
    line(eyeOffset - botSize * 0.2, -botSize * 0.5, eyeOffset + botSize * 0.2, -botSize * 0.5);
    noStroke();
  }

  // --- Vacant Mouth ---
  fill(50); // Dark gray for mouth cavity
  // A more open, slightly jagged mouth
  arc(0, -botSize * 0.15, botSize * 0.7, botSize * 0.3, 0, PI);
  // Add a small rectangle for a hanging tongue or just a darker patch
  fill(150, 50, 50); // Dull red for a possible tongue
  rect(-botSize * 0.1, -botSize * 0.1, botSize * 0.2, botSize * 0.1);

  // --- Simple Zombie Scars/Patches ---
  fill(80, 100, 80, 180); // Darker dull green, slightly transparent
  rect(-botSize * 0.3, -botSize * 0.6, botSize * 0.15, botSize * 0.1); // Small scar on forehead
  ellipse(botSize * 0.2, -botSize * 0.2, botSize * 0.1, botSize * 0.1); // Missing chunk on cheek

  // Arms (simple black lines)
  stroke(80, 100, 80); // Darker dull green for limbs
  strokeWeight(5); // Set stroke thickness

  // --- Awkward Twitching Arms/Legs ---
  let armTwitch = sin(frameCount * 0.1) * 0.1; // Small oscillation for arms
  let legTwitch = cos(frameCount * 0.08) * 0.05; // Small oscillation for legs

  // Left arm (offset slightly)
  line(-botSize * 0.8, 0 + armTwitch * botSize, -botSize * 0.4, botSize * 0.2 + armTwitch * botSize);
  // Right arm (offset slightly)
  line(botSize * 0.8, 0 - armTwitch * botSize, botSize * 0.4, botSize * 0.2 - armTwitch * botSize);

  // Bot's legs are now dangling feet appropriate for a jockey
  strokeWeight(3); // Thinner legs for dangling feet
  line(-botSize * 0.2, botSize * 0.7, -botSize * 0.2 + legTwitch * botSize, botSize * 0.9); // Left dangling leg
  line(botSize * 0.2, botSize * 0.7, botSize * 0.2 - legTwitch * botSize, botSize * 0.9); // Right dangling leg

  noStroke(); // Reset stroke
}
Line-by-line explanation (21 lines)

🔧 Subcomponents:

calculation Lagging Eye Tracking currentPupilX = lerp(currentPupilX, targetPupilX, eyeLagFactor * 0.5);

Uses lerp() to slowly move the pupil toward the mouse target, creating a deliberately dumb, slow-tracking eye effect

calculation Twitching Animation let armTwitch = sin(frameCount * 0.1) * 0.1;

Uses sin() with frameCount to create oscillating, jittery arm movements that reinforce the zombie character

fill(90, 110, 90); // Slightly darker dull green for body to contrast clothes
Sets the fill color to a dull zombie-like green for the body
ellipse(0, 0, botSize * 1.2, botSize * 1.5); // Wider and taller than the head
Draws the bot's body as an ellipse at the origin (0, 0), which is where this function's translate() positioned it in draw()
fill(173, 216, 230); // Light blue
Changes the fill color to light blue for the t-shirt rectangles that follow
rect(-botSize * 0.6, -botSize * 0.6, botSize * 1.2, botSize * 0.8); // Main shirt body
Draws a rectangle for the t-shirt's main body, positioned above the bot's center (negative y = up)
let pupilSize = botSize * 0.08; // Smaller pupils for vacant stare
Scales the pupil size to be small relative to the bot, making the eyes look glassy and stupid
let targetPupilX = map(mouseX, 0, width, -eyeOffset / 4, eyeOffset / 4); // Reduced range
Maps the mouse's x position to a small target position for the pupil, so the eyes follow the mouse but with a reduced, subtle range
targetPupilX = constrain(targetPupilX, -eyeOffset / 4, eyeOffset / 4);
Clamps the target position to stay within the eye's bounds, preventing the pupil from looking too far and breaking the illusion
currentPupilX = lerp(currentPupilX, targetPupilX, eyeLagFactor * 0.5); // Even slower lag
Smoothly interpolates the actual pupil position toward the target using lerp(), with a very small factor (eyeLagFactor * 0.5) creating lazy, disconnected eye tracking
if (!blinking) {
Checks whether the bot is currently blinking to decide whether to update the blink timer
if (blinkTimer <= 0) { blinkTimer = floor(random(120, 360)); }
When the timer reaches zero, generates a new random wait time between 120 and 360 frames (2 to 6 seconds at 60fps)
blinkTimer--;
Decrements the timer every frame, counting down toward the next blink
if (blinkTimer <= 0 && !blinking) { blinking = true; blinkTimer = blinkDuration; }
When the countdown reaches zero, switches to blinking state and sets a new timer for how many frames the blink lasts
if (blinking) { blinkTimer--; if (blinkTimer <= 0) { blinking = false; } }
While blinking, counts down the blink duration; when it reaches zero, stops blinking and resumes normal eye drawing
fill(255, 255, 200); // Sickly yellow-white eye background
Sets the fill color to a sickly yellow-white for the eye whites, giving the zombie an undead appearance
ellipse(-eyeOffset, -botSize * 0.5, botSize * 0.4, botSize * 0.4); // White part of the eye
Draws the left eye's white part, positioned eyeOffset pixels to the left of the bot's center
if (!blinking) {
Checks whether the bot is blinking to decide whether to draw the pupil or a blink line
ellipse(-eyeOffset + currentPupilX, -botSize * 0.5 + currentPupilY, pupilSize, pupilSize); // Pupil following mouse
When not blinking, draws the pupil offset from the eye center by the current tracked values, making it appear to follow the mouse
line(-eyeOffset - botSize * 0.2, -botSize * 0.5, -eyeOffset + botSize * 0.2, -botSize * 0.5);
When blinking, draws a horizontal line where the eye's opening would be, creating the visual effect of a closed eyelid
arc(0, -botSize * 0.15, botSize * 0.7, botSize * 0.3, 0, PI);
Draws an arc for the bot's mouth cavity, with a dark gray fill to look like an open, vacant mouth
let armTwitch = sin(frameCount * 0.1) * 0.1; // Small oscillation for arms
Uses sin() with a time-based input (frameCount) to create smooth oscillation, multiplied by botSize to make the arms twitch back and forth
line(-botSize * 0.8, 0 + armTwitch * botSize, -botSize * 0.4, botSize * 0.2 + armTwitch * botSize);
Draws the left arm as a line from the bot's shoulder to below the body, with the twitching offset applied to both ends to create jerky movement

windowResized()

windowResized() is a p5.js special function that automatically runs whenever the browser window is resized. It's essential for responsive sketches that should adapt to different screen sizes. Without it, the canvas would resize but the chicken and bot wouldn't reposition, potentially ending up off-screen or in a corner.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Re-center the chicken and bot when the preview panel is resized
  chickenX = width / 2;
  chickenY = height / 2 + botSize * 0.7;
}
Line-by-line explanation (3 lines)
resizeCanvas(windowWidth, windowHeight);
Updates the canvas size to match the current browser window dimensions whenever the window is resized
chickenX = width / 2;
Re-centers the chicken horizontally whenever the canvas is resized, so it stays in the middle of the new screen size
chickenY = height / 2 + botSize * 0.7;
Re-centers the chicken vertically whenever the canvas is resized, maintaining the same relative position as the initial setup()

📦 Key Variables

botSize number

Controls the overall scale of the zombie jockey and derived dimensions (head size, eye offset, etc.) so the entire character resizes proportionally

let botSize = 100;
timeOffset number

Advances each frame to drive the bot's independent Perlin noise wobble and twitching animations

let timeOffset = 0;
currentPupilX number

Stores the actual x position of the pupils, smoothly interpolating toward the mouse target using lerp()

let currentPupilX = 0;
currentPupilY number

Stores the actual y position of the pupils, smoothly interpolating toward the mouse target using lerp()

let currentPupilY = 0;
eyeLagFactor number

Controls how slowly the pupils move toward the mouse (lower = lazier, more dumb-looking eye tracking)

let eyeLagFactor = 0.08;
blinking boolean

Tracks whether the bot is currently in a blinking state, used to switch between drawing pupils vs. eyelid lines

let blinking = false;
blinkTimer number

Counts down the frames until the next blink triggers, or counts down the duration of a blink if one is active

let blinkTimer = 0;
blinkDuration number

Specifies how many frames a blink lasts (how long the eyelid line is drawn)

let blinkDuration = 5;
chickenX number

Stores the chicken's current horizontal position, updated every frame by Perlin noise for wobbly movement

let chickenX;
chickenY number

Stores the chicken's current vertical position, updated every frame by Perlin noise for wobbly movement

let chickenY;
chickenSize number

Scales all chicken dimensions relative to botSize, making the chicken twice as large as the bot

let chickenSize;
chickenWobbleOffset number

Advances slowly each frame to drive the chicken's Perlin noise wobble, creating smooth organic side-to-side motion

let chickenWobbleOffset = 0;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

PERFORMANCE drawChicken() and drawBot()

Many calculations (like chickenSize * 0.5, botSize * 0.6, etc.) are recalculated every single frame in drawing functions, even though they never change

💡 Pre-calculate these scale factors in setup() as constants (e.g., let headSize = chickenSize * 0.5;) and reuse them. This saves CPU cycles, especially when botSize and chickenSize are large.

STYLE drawBot() pupil tracking

The pupil tracking logic has subtle asymmetries: currentPupilX and Y are updated with eyeLagFactor * 0.5, while the targetPupil calculations divide eyeOffset by 4, making the eye-tracking behavior unintuitive to modify

💡 Create a dedicated variable like let trackingScale = 0.5 at the top of draw(), and use it consistently throughout eye tracking: currentPupilX = lerp(..., eyeLagFactor * trackingScale). This makes tuning easier.

BUG drawBot() blink state machine

If blinkDuration is longer than the interval before the next blink is scheduled, a blink can be interrupted mid-cycle or overlap unpredictably

💡 Add a guard: only set blinkTimer = floor(random(120, 360)) if blinkDuration has completed; or ensure random() range is always much larger than blinkDuration (currently they're unrelated, creating potential conflicts)

FEATURE drawBot() eye tracking

The pupils track the mouse globally across the page, but if the bot moves (via translate in draw()), the eye tracking reference frame doesn't account for the bot's visual position on screen, causing the eyes to track 'past' where the mouse actually appears relative to the bot's visible head

💡 Add a comment explaining this limitation, or (advanced) adjust targetPupil calculations to account for the translate offset so eyes track relative to the bot's drawn position rather than global canvas space

🔄 Code Flow

Code flow showing setup, draw, drawchicken, drawbot, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> chickenwobble[chicken-wobble] draw --> botwobble[bot-wobble] draw --> transformstacking[transform-stacking] draw --> drawchicken[drawChicken] draw --> drawbot[drawBot] draw --> windowresized[windowResized] chickenwobble --> chickenbody[chicken-body] chickenbody --> chickenhead[chicken-head] chickenhead --> chickenlegs[chicken-legs] botwobble --> eyeTrackingLogic[eye-tracking-logic] eyeTrackingLogic --> blinkStateMachine[blink-state-machine] blinkStateMachine --> armTwitch[arm-twitch] click setup href "#fn-setup" click draw href "#fn-draw" click chickenwobble href "#sub-chicken-wobble" click botwobble href "#sub-bot-wobble" click transformstacking href "#sub-transform-stacking" click drawchicken href "#fn-drawchicken" click drawbot href "#fn-drawbot" click windowresized href "#fn-windowresized" click chickenbody href "#sub-chicken-body" click chickenhead href "#sub-chicken-head" click chickenlegs href "#sub-chicken-legs" click eyeTrackingLogic href "#sub-eye-tracking-logic" click blinkStateMachine href "#sub-blink-state-machine" click armTwitch href "#sub-arm-twitch"

❓ Frequently Asked Questions

What visual elements are featured in the GUESS WHO!!! sketch?

The sketch creates a whimsical animation of a wobbly chicken with a bot riding on its back, set against a light gray background.

Is there any user interaction in the GUESS WHO!!! sketch?

The sketch does not currently include interactive elements; it primarily focuses on visual animations of the chicken and bot.

What creative coding techniques are showcased in this p5.js sketch?

This sketch demonstrates the use of Perlin noise for creating smooth, organic movement and animation effects for both the chicken and the bot.

Preview

GUESS WHO!!! - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of GUESS WHO!!! - Code flow showing setup, draw, drawchicken, drawbot, windowresized
Code Flow Diagram