Red guides and his friends

This sketch fills the screen with three colorful abstract characters—the "weird guys"—made of wiggly organic shapes with bright expressive eyes and curved mouths. Click or tap any character to trigger a gentle pulsing animation that brings them to life.

🧪 Try This!

Experiment with the code by making these changes:

  1. Multiply the number of characters — Changing numGuys fills the screen with more colorful creatures for a busier, more playful scene
  2. Make the background dark — A dark background (low numbers) makes the bright character colors pop and creates a nighttime mood
  3. Increase the pulse strength — Characters will grow bigger when tapped, creating a more dramatic reaction to interaction
  4. Make bodies extra wiggly — Higher noise multipliers create more extreme bumps and indentations in the body outlines
  5. Make eyes bigger — Increasing the eye ellipse sizes makes characters look more expressive and cartoonish
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates three abstract animated characters that you can interact with by tapping or clicking. Each "weird guy" is a unique blend of organic, wiggly geometry—a noisy body outline, paired eyes with colorful pupils, and a curved smile mouth. What makes this sketch special is its use of Perlin noise to generate natural-looking irregular shapes, and the way tapping any character triggers a satisfying pulsing scale animation using lerp to smooth the motion.

The code is built around a WeirdGuy class that encapsulates all the logic for one character: its position, size, random colors, and the display and interaction methods. By studying it, you'll learn how classes organize complex visual objects in p5.js, how Perlin noise creates organic irregular forms, how lerp smooths animations frame-by-frame, and how to detect clicks and touches to trigger interactive responses. The sketch also demonstrates responsive canvas resizing and the difference between desktop mouse interaction and mobile touch interaction.

⚙️ How It Works

  1. When the sketch loads, setup() creates a canvas that fills the window and then generates three WeirdGuy objects with random positions, sizes, and random color pairs stored in each instance.
  2. Every frame, draw() clears the background and loops through all the guys, calling display() on each one to draw its body, eyes, and mouth on screen.
  3. In the display() method, the code uses Perlin noise to generate a wiggly organic body outline: for each segment around a circle, noise() adds variation to the radius, creating an irregular, life-like silhouette instead of a perfect circle.
  4. The pulse variable animates smoothly toward zero using lerp(), which blends the current value with the target value by a small amount each frame—this creates a gentle spring-like effect when the character is tapped.
  5. When you click or tap the screen, mousePressed() or touchStarted() checks the distance from your finger to each character; if you hit one, its tap() method sets pulse to 0.05, triggering the scale-up animation on the next frames.
  6. If the window is resized, windowResized() rebuilds the canvas and regenerates all the guys with new random positions and sizes so they adapt to the new space.

🎓 Concepts You'll Learn

Object-oriented programming with classesPerlin noise for organic generationLerp for smooth easing animationTouch and mouse interactionCollision detection with distanceResponsive canvas resizingTrigonometry for circular layout

📝 Code Breakdown

WeirdGuy (Class)

A class is a blueprint for creating objects. Each WeirdGuy stores its own position, size, colors, and animation state. When you create a new WeirdGuy, the constructor runs once to set up all these properties.

class WeirdGuy {
  constructor(x, y, size) {
    this.x = x;
    this.y = y;
    this.size = size;
    this.color1 = color(random(100, 255), random(50, 150), random(150, 255));
    this.color2 = color(random(50, 150), random(150, 255), random(100, 255));
    this.detail = random(5, 20);
    this.offset = random(1000);
    this.pulse = 0;
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Random color generation this.color1 = color(random(100, 255), random(50, 150), random(150, 255));

Creates two unique random colors for each character—the body outline and facial features—ensuring visual variety across all instances

calculation Body complexity randomization this.detail = random(5, 20);

Determines how many segments make up the character's body outline, affecting how wiggly or angular the shape looks

this.x = x;
Stores the character's horizontal position on the canvas so display() knows where to draw it
this.y = y;
Stores the character's vertical position on the canvas
this.size = size;
Stores the character's scale factor; all body parts, eyes, and mouth scale proportionally to this value
this.color1 = color(random(100, 255), random(50, 150), random(150, 255));
Generates a random RGB color for the body outline; random() picks a value in each channel range to ensure bright but not white colors
this.color2 = color(random(50, 150), random(150, 255), random(100, 255));
Generates a second random color for the eyes' pupils and mouth; note the different ranges to create visual contrast with color1
this.detail = random(5, 20);
Sets how many segments form the body outline; more segments = smoother curves, fewer = pointier shapes
this.offset = random(1000);
Stores a unique random starting point for Perlin noise so each character's wiggly body outline looks different
this.pulse = 0;
Initializes the pulse animation value to 0; when a character is tapped, this becomes 0.05 and lerp() animates it back down

display()

display() is called once per frame for each character. It uses push() and pop() to isolate transformations—translate moves the origin, scale grows the character for the pulse animation, and all the shapes draw relative to that new origin. The key innovation is using Perlin noise with angle as input to create an organic, irregular outline that looks alive.

display() {
  push();
  translate(this.x, this.y);

  this.pulse = lerp(this.pulse, 0, 0.05);
  scale(1 + this.pulse);

  noFill();
  strokeWeight(this.size * 0.02);
  stroke(this.color1);
  beginShape();
  for (let i = 0; i < this.detail; i++) {
    let angle = map(i, 0, this.detail, 0, TWO_PI);
    let r = this.size * 0.4 + noise(angle * 0.5, this.offset) * this.size * 0.2;
    let x = r * cos(angle);
    let y = r * sin(angle);
    vertex(x, y);
  }
  endShape(CLOSE);

  fill(255);
  noStroke();
  ellipse(this.size * -0.15, this.size * -0.1, this.size * 0.1);
  ellipse(this.size * 0.15, this.size * -0.1, this.size * 0.1);

  fill(this.color2);
  ellipse(this.size * -0.15, this.size * -0.1, this.size * 0.05);
  ellipse(this.size * 0.15, this.size * -0.1, this.size * 0.05);

  stroke(this.color2);
  strokeWeight(this.size * 0.02);
  noFill();
  curve(
    this.size * -0.3, this.size * 0.1,
    this.size * -0.15, this.size * 0.2,
    this.size * 0.15, this.size * 0.2,
    this.size * 0.3, this.size * 0.1
  );

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

🔧 Subcomponents:

calculation Smooth pulse easing this.pulse = lerp(this.pulse, 0, 0.05);

Gradually reduces the pulse value toward zero each frame, creating a spring-like animation that smoothly fades after a tap

for-loop Wiggly body generation for (let i = 0; i < this.detail; i++) { ... }

Loops around a circle, using noise to vary the radius at each angle, creating an organic irregular outline instead of a perfect circle

calculation Noise-based radius variation let r = this.size * 0.4 + noise(angle * 0.5, this.offset) * this.size * 0.2;

Uses Perlin noise to add unpredictable bumps and indentations to the body outline, making it look alive and organic

calculation Polar coordinate conversion let x = r * cos(angle); let y = r * sin(angle);

Converts an angle and radius into x, y coordinates so the shape can be drawn as a vertex

calculation White eye whites fill(255); noStroke(); ellipse(this.size * -0.15, this.size * -0.1, this.size * 0.1); ellipse(this.size * 0.15, this.size * -0.1, this.size * 0.1);

Draws two white circles for the outer eyes, positioned symmetrically on the character's face

calculation Colored pupils fill(this.color2); ellipse(this.size * -0.15, this.size * -0.1, this.size * 0.05); ellipse(this.size * 0.15, this.size * -0.1, this.size * 0.05);

Draws two smaller colored circles on top of the white eyes, creating the pupil effect

push();
Saves the current drawing state (position, rotation, scale, colors) so changes only affect this character
translate(this.x, this.y);
Moves the drawing origin to the character's x, y position, so all shapes draw relative to that point instead of the canvas origin
this.pulse = lerp(this.pulse, 0, 0.05);
Smoothly animates pulse toward zero by 5% each frame, creating a gradual decay after being tapped
scale(1 + this.pulse);
Grows the entire character by the pulse amount; when pulse is 0.05, the character becomes 1.05x larger
noFill();
Turns off fill so only the outline of the body shape is drawn, not a solid interior
strokeWeight(this.size * 0.02);
Sets the outline thickness proportionally to the character's size so big guys have thicker outlines
stroke(this.color1);
Sets the body outline color to this character's first random color
for (let i = 0; i < this.detail; i++) {
Loops from 0 to detail-1, creating that many points around a circle for the body outline
let angle = map(i, 0, this.detail, 0, TWO_PI);
Converts the loop counter into an angle from 0 to 360 degrees (TWO_PI radians), spreading points evenly around a circle
let r = this.size * 0.4 + noise(angle * 0.5, this.offset) * this.size * 0.2;
Calculates the distance from center: a base radius (0.4x size) plus Perlin noise variations (up to ±0.2x size), creating the wiggly effect
let x = r * cos(angle);
Converts polar radius and angle into a Cartesian x coordinate using cosine
let y = r * sin(angle);
Converts polar radius and angle into a Cartesian y coordinate using sine
vertex(x, y);
Adds this point to the shape being drawn
endShape(CLOSE);
Completes the shape by connecting the last vertex back to the first, creating a closed outline
fill(255);
Sets fill color to white for drawing the eye whites
ellipse(this.size * -0.15, this.size * -0.1, this.size * 0.1);
Draws a white circle for the left eye, positioned 0.15x size to the left and 0.1x size above center
fill(this.color2);
Changes fill color to this character's second random color for the pupils
curve(...);
Draws a curved line for the mouth using four control points, creating a smile shape
pop();
Restores the drawing state saved by push(), undoing all transforms so the next character doesn't inherit this one's position

tap()

tap() is a method that gets called when this character is clicked or touched. It's a simple setter that triggers the pulse animation by setting an initial value; the animation itself happens in display() using lerp().

tap() {
  this.pulse = 0.05;
}
Line-by-line explanation (1 lines)
this.pulse = 0.05;
Sets pulse to 0.05, triggering a scale animation on the next frames as lerp() gradually reduces it back to zero

setup()

setup() runs once when the sketch starts. It creates the canvas and initializes the guys array with new WeirdGuy instances, each with random properties.

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

  for (let i = 0; i < numGuys; i++) {
    let x = random(width * 0.2, width * 0.8);
    let y = random(height * 0.2, height * 0.8);
    let size = random(min(width, height) * 0.15, min(width, height) * 0.3);
    guys.push(new WeirdGuy(x, y, size));
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Character instantiation for (let i = 0; i < numGuys; i++) { ... guys.push(...) }

Creates numGuys WeirdGuy instances with random positions and sizes, populating the guys array

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, making the sketch responsive
for (let i = 0; i < numGuys; i++) {
Loops numGuys times (default 3) to create that many characters
let x = random(width * 0.2, width * 0.8);
Picks a random x position between 20% and 80% of the canvas width, keeping characters away from the edges
let y = random(height * 0.2, height * 0.8);
Picks a random y position between 20% and 80% of the canvas height for similar edge spacing
let size = random(min(width, height) * 0.15, min(width, height) * 0.3);
Picks a random size between 15% and 30% of the smaller canvas dimension, ensuring characters scale with the window
guys.push(new WeirdGuy(x, y, size));
Creates a new WeirdGuy with the random x, y, and size, then adds it to the guys array

draw()

draw() runs 60 times per second (by default). Each frame, it erases the background and redraws all characters, creating the animation loop. The pulse animations in each character fade between frames, so you see smooth motion.

function draw() {
  background(220);

  for (let guy of guys) {
    guy.display();
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

for-loop Character rendering loop for (let guy of guys) { guy.display(); }

Iterates through all characters and calls their display() method to draw them on screen each frame

background(220);
Clears the canvas with a light gray color (220 = light gray in grayscale), erasing the previous frame so characters don't leave trails
for (let guy of guys) {
Loops through each WeirdGuy object in the guys array
guy.display();
Calls the display() method on the current character, drawing its body, eyes, and mouth

windowResized()

windowResized() is a built-in p5.js function that p5.js calls automatically whenever the browser window is resized. This sketch regenerates all characters with new random positions and sizes to adapt to the new canvas dimensions, ensuring they always fit nicely on screen.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  guys = [];
  for (let i = 0; i < numGuys; i++) {
    let x = random(width * 0.2, width * 0.8);
    let y = random(height * 0.2, height * 0.8);
    let size = random(min(width, height) * 0.15, min(width, height) * 0.3);
    guys.push(new WeirdGuy(x, y, size));
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Character regeneration for (let i = 0; i < numGuys; i++) { ... }

Creates a new set of characters with positions and sizes adapted to the new window dimensions

resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the new browser window dimensions
guys = [];
Clears the guys array, deleting all old characters so they won't be drawn anymore
for (let i = 0; i < numGuys; i++) {
Loops numGuys times to create a fresh set of characters
let x = random(width * 0.2, width * 0.8);
Generates a new random x position based on the updated canvas width
guys.push(new WeirdGuy(x, y, size));
Creates a new WeirdGuy and adds it to the refreshed guys array

touchStarted()

touchStarted() is called by p5.js whenever a finger touches the screen. It checks collision between the touch point and each character using distance math; if a character is hit, tap() is called to animate it.

function touchStarted() {
  for (let guy of guys) {
    let d = dist(touches[0].x, touches[0].y, guy.x, guy.y);
    if (d < guy.size / 2) {
      guy.tap();
      break;
    }
  }
  return false;
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Touch hit detection if (d < guy.size / 2) { guy.tap(); break; }

Tests if the touch position is within the character's radius; if so, triggers its tap animation and stops checking other characters

for (let guy of guys) {
Loops through each character to check if any was touched
let d = dist(touches[0].x, touches[0].y, guy.x, guy.y);
Calculates the distance from the first touch point to this character's center using p5.js's dist() function
if (d < guy.size / 2) {
Checks if the distance is less than the character's radius (size/2), meaning the touch hit the character
guy.tap();
Calls tap() on the hit character, triggering its pulse animation
break;
Exits the loop so only one character responds per tap, even if multiple characters overlap
return false;
Tells the browser to prevent default touch behavior like scrolling or zooming, so tapping the sketch only triggers the animation

mousePressed()

mousePressed() is the desktop equivalent of touchStarted(). It uses mouseX and mouseY instead of touches[] to detect clicks. The collision logic is identical—distance-based hit detection with a circular radius.

function mousePressed() {
  for (let guy of guys) {
    let d = dist(mouseX, mouseY, guy.x, guy.y);
    if (d < guy.size / 2) {
      guy.tap();
      break;
    }
  }
  return false;
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Mouse click hit detection if (d < guy.size / 2) { guy.tap(); break; }

Tests if the mouse click is within a character's radius; if so, triggers its tap animation and stops checking others

for (let guy of guys) {
Loops through each character to check if any was clicked
let d = dist(mouseX, mouseY, guy.x, guy.y);
Calculates the distance from the mouse click position to this character's center
if (d < guy.size / 2) {
Checks if the distance is less than the character's radius, meaning the click hit the character
guy.tap();
Calls tap() on the hit character, triggering its pulse animation
break;
Exits the loop so only one character responds per click
return false;
Prevents the browser's default mouse behavior, isolating the interaction to the sketch

📦 Key Variables

guys array

Stores all WeirdGuy objects currently on screen; the sketch loops through this array each frame to display and interact with them

let guys = [];
numGuys number

Controls how many WeirdGuy characters are created in setup() and regenerated in windowResized()

const numGuys = 3;
this.x number

Stores a character's horizontal position on the canvas

this.x = 200;
this.y number

Stores a character's vertical position on the canvas

this.y = 300;
this.size number

Stores a character's scale factor; all body parts scale proportionally to this value

this.size = 150;
this.color1 p5.Color

Stores the character's body outline color as an RGB color object

this.color1 = color(255, 100, 200);
this.color2 p5.Color

Stores the character's eye pupil and mouth color as an RGB color object

this.color2 = color(100, 200, 255);
this.detail number

Determines how many segments form the character's wiggly body outline; more segments = smoother, fewer = pointier

this.detail = 12;
this.offset number

A unique starting point for Perlin noise, ensuring each character's wiggly body looks different

this.offset = 537.8;
this.pulse number

Tracks the current pulse animation value; when a character is tapped, this becomes 0.05 and lerp() gradually reduces it to 0

this.pulse = 0;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG touchStarted() and mousePressed()

If touches array is empty or mouseX/mouseY is undefined, the distance calculation could fail, though p5.js handles this gracefully. However, the code doesn't check if touches array exists before accessing touches[0].

💡 Add a safety check: 'if (touches.length === 0) return false;' at the start of touchStarted() to prevent potential errors on devices without touch support.

PERFORMANCE display()

The character's colors are generated once per instance but never updated; this is fine for static characters. However, if you ever add animation to colors, recalculating random colors every frame in the constructor would be wasteful.

💡 Colors are already optimized—they're generated once in the constructor. Keep it this way.

FEATURE draw()

All characters are static after creation; while they respond to taps with a pulse, they don't move or change organically over time.

💡 Add time-based noise animation to this.offset in display() (e.g., 'this.offset += 0.001') to make bodies wiggle and breathe continuously, making them feel more alive even before being tapped.

STYLE WeirdGuy constructor

Color generation uses separate random() calls for each RGB channel, making the ranges inconsistent and sometimes hard to read

💡 Consider using HSB color mode (colorMode(HSB)) for more intuitive color control and consistent saturation/brightness across characters.

🔄 Code Flow

Code flow showing weirdguy, display, tap, setup, draw, windowresized, touchstarted, mousepressed

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

graph TD start[Start] --> setup[setup] setup --> guy-creation-loop[Character Instantiation Loop] guy-creation-loop --> draw[draw loop] draw --> display-loop[Character Rendering Loop] display-loop --> display[display] display --> body-loop[Wiggly Body Generation] body-loop --> noise-radius[Noise-based Radius Variation] noise-radius --> polar-to-cartesian[Polar to Cartesian Conversion] polar-to-cartesian --> white-eyes[White Eye Whites] white-eyes --> colored-pupils[Colored Pupils] display --> pulse-animation[Smooth Pulse Easing] pulse-animation --> tap[tap] tap --> touch-collision-check[Touch Hit Detection] touch-collision-check --> draw draw --> mousepressed[mousePressed] mousepressed --> mouse-collision-check[Mouse Click Hit Detection] mouse-collision-check --> draw draw --> windowresized[windowResized] windowresized --> guy-regeneration-loop[Character Regeneration Loop] guy-regeneration-loop --> draw click setup href "#fn-setup" click draw href "#fn-draw" click guy-creation-loop href "#sub-guy-creation-loop" click display-loop href "#sub-display-loop" click display href "#fn-display" click body-loop href "#sub-body-loop" click noise-radius href "#sub-noise-radius" click polar-to-cartesian href "#sub-polar-to-cartesian" click white-eyes href "#sub-white-eyes" click colored-pupils href "#sub-colored-pupils" click pulse-animation href "#sub-pulse-animation" click tap href "#fn-tap" click touch-collision-check href "#sub-touch-collision-check" click mousepressed href "#fn-mousepressed" click mouse-collision-check href "#sub-mouse-collision-check" click windowresized href "#fn-windowresized" click guy-regeneration-loop href "#sub-guy-regeneration-loop"

❓ Frequently Asked Questions

What visual elements are featured in the Red Guides and His Friends sketch?

The sketch showcases colorful, abstract 'weird guys' made of wiggly shapes with bright eyes, filling the entire screen.

How can users interact with the Red Guides and His Friends sketch?

Users can tap or click on the colorful characters to make their bodies gently pulse and animate.

What creative coding techniques does the sketch utilize?

The sketch demonstrates the use of noise for creating organic shapes and lerping for smooth animations, showcasing principles of generative art.

Preview

Red guides and his friends - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Red guides and his friends - Code flow showing weirdguy, display, tap, setup, draw, windowresized, touchstarted, mousepressed
Code Flow Diagram