Sketch 2026-02-22 21:31

This sketch simulates multiple NPC characters autonomously cutting and eating a pumpkin and food items displayed on the canvas. NPCs move toward randomly chosen targets (either the pumpkin or food emojis), and when targeting food, they shrink it as they 'eat' it until it disappears.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make NPCs move faster — NPCs move more quickly toward their targets, eating food and carving the pumpkin in seconds instead of minutes.
  2. Make food disappear faster — Change the eating rate so food shrinks much quicker as NPCs consume it, making the scene feel more hectic.
  3. NPCs prefer food over pumpkins — Change the probability so NPCs target food 70% of the time instead of 30%, making the right side the scene of action.
  4. Change background to dark theme — Swap the light blue background to a dark charcoal, making the orange pumpkin and colorful food emojis pop more.
  5. Increase initial food population — Start with more food items for NPCs to hunt, making the food area busier and the scene more lively.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a playful scene where five NPC characters autonomously roam between a pumpkin on the left and an array of food emoji items on the right, deciding whether to cut the pumpkin or 'eat' food items. Each NPC is an intelligent agent that targets destinations, moves toward them using trigonometry, and shrinks food items as they consume them. The sketch demonstrates object-oriented programming with the NPCCutter class, autonomous agent behavior, collision-free targeting, and real-time array management.

The code is organized around the NPCCutter class, which encapsulates each agent's position, target, speed, and current activity. The setup() function initializes the canvas, pumpkins, food, and NPC fleet; the draw() loop updates and displays each NPC; and helper functions manage food population and visual rendering. By studying this sketch, you will learn how to design intelligent agents using classes, how to make objects move toward targets using angle calculations, and how to manage dynamic arrays of objects with shared state.

⚙️ How It Works

  1. When the sketch loads, setup() creates a landscape canvas, positions a pumpkin on the left side and a food area on the right side, populates the food area with 15 random food emojis, and spawns 5 NPC cutters at random starting positions.
  2. Each frame, draw() clears the background, renders the pumpkin and food emojis, updates each NPC's position, and checks if the food population has dropped below 5 items—spawning new food if needed.
  3. Each NPC continuously runs update(), which calculates the distance to its target and moves toward it using trigonometry (atan2 to find angle, cos/sin to move in that direction).
  4. When an NPC reaches its target within one frame's movement distance, it checks if it was cutting food; if so, it shrinks that food emoji by 0.1 each frame until it reaches zero size and is removed from the array.
  5. When an NPC finishes with a target, setNewTarget() randomly decides (30% chance) whether to target a food emoji or the pumpkin next; if food is chosen, it marks that food as 'targeted' to prevent multiple NPCs from attacking the same item.
  6. The entire scene adapts to window resizing via windowResized(), which recalculates all positions and resets the NPC cutters to new locations.

🎓 Concepts You'll Learn

Object-oriented programming with classesAutonomous agent behaviorTrigonometric movement (atan2, cos, sin)Array filtering and dynamic removalState management (isTargeted, isCuttingFood flags)Canvas resizing and responsive design

📝 Code Breakdown

NPCCutter (class)

Classes in p5.js let you bundle related data (x, y, speed) and behaviors (update, draw) into reusable objects. The constructor runs once when you create a new NPCCutter, setting up all the initial values.

class NPCCutter {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.targetX = x;
    this.targetY = y;
    this.speed = random(2, 5); // Random cutting speed for each NPC
    this.isCuttingFood = false; // Flag to know if currently cutting food
    this.targetFoodIndex = -1; // Index of the specific food emoji this NPC is targeting

    this.setNewTarget(); // Set an initial target
  }
Line-by-line explanation (8 lines)

🔧 Subcomponents:

constructor Constructor initialization constructor(x, y) {

Sets up an individual NPC with starting position, target position, speed, and flags for current state

this.x = x;
Stores the NPC's current horizontal position
this.y = y;
Stores the NPC's current vertical position
this.targetX = x;
Initializes the target position to the current position; will be updated by setNewTarget()
this.targetY = y;
Initializes the target position to the current position; will be updated by setNewTarget()
this.speed = random(2, 5); // Random cutting speed for each NPC
Each NPC gets a unique random speed between 2 and 5 pixels per frame, making movement feel natural and varied
this.isCuttingFood = false; // Flag to know if currently cutting food
A boolean that tracks whether this NPC is currently eating a food emoji (true) or targeting the pumpkin (false)
this.targetFoodIndex = -1; // Index of the specific food emoji this NPC is targeting
Stores which food emoji in the displayedEmojis array this NPC is eating; -1 means no specific food target
this.setNewTarget(); // Set an initial target
Calls setNewTarget() to decide whether to pursue food or pumpkin and set the target position

update()

The update() method is called every frame for each NPC. It encapsulates all the NPC's behavior: arriving at targets, starting new tasks, and eating food. The trigonometric movement (atan2/cos/sin) is a core technique for making objects move toward a point in any direction.

🔬 These two lines move the NPC toward its target using trigonometry. What happens if you multiply this.speed by 2, making NPCs move twice as fast?

      this.x += cos(angle) * this.speed;
      this.y += sin(angle) * this.speed;

🔬 This code shrinks food by 0.1 each frame and removes it when size hits zero. What happens if you change 0.1 to 0.01 so food takes 10 times longer to disappear?

        displayedEmojis[this.targetFoodIndex].currentSize -= 0.1; // Shrink the emoji
        if (displayedEmojis[this.targetFoodIndex].currentSize <= 0) {
  update() {
    let d = dist(this.x, this.y, this.targetX, this.targetY);

    if (d < this.speed) {
      // Reached target, set a new one
      this.x = this.targetX;
      this.y = this.targetY;

      // If we were cutting food, mark it as no longer targeted
      if (this.isCuttingFood && this.targetFoodIndex !== -1 && displayedEmojis[this.targetFoodIndex]) {
        displayedEmojis[this.targetFoodIndex].isTargeted = false;
      }

      this.isCuttingFood = false; // Reset flag
      this.targetFoodIndex = -1; // Reset target index
      this.setNewTarget();
    } else {
      // Move towards target
      let angle = atan2(this.targetY - this.y, this.targetX - this.x);
      this.x += cos(angle) * this.speed;
      this.y += sin(angle) * this.speed;

      // "Eat" the targeted food emoji
      if (this.isCuttingFood && this.targetFoodIndex !== -1 && displayedEmojis[this.targetFoodIndex]) {
        displayedEmojis[this.targetFoodIndex].currentSize -= 0.1; // Shrink the emoji
        if (displayedEmojis[this.targetFoodIndex].currentSize <= 0) {
          displayedEmojis[this.targetFoodIndex].isTargeted = false; // Release target
          displayedEmojis.splice(this.targetFoodIndex, 1); // Remove from array
          this.isCuttingFood = false; // Stop cutting food
          this.targetFoodIndex = -1; // Reset target index
          this.setNewTarget(); // Find a new target
        }
      }
    }
  }
Line-by-line explanation (16 lines)

🔧 Subcomponents:

conditional Target reached check if (d < this.speed) {

Determines if the NPC is close enough to its target to stop and choose a new one

calculation Trigonometric movement toward target let angle = atan2(this.targetY - this.y, this.targetX - this.x);

Calculates the angle from the NPC's current position to its target using inverse tangent

calculation Food consumption shrinking displayedEmojis[this.targetFoodIndex].currentSize -= 0.1; // Shrink the emoji

Decreases the size of the targeted food emoji each frame to simulate eating

conditional Remove fully eaten food if (displayedEmojis[this.targetFoodIndex].currentSize <= 0) {

Checks if food has been completely consumed and removes it from the array

let d = dist(this.x, this.y, this.targetX, this.targetY);
Calculates the pixel distance from the NPC's current position to its target position using p5.js's dist() function
if (d < this.speed) {
Checks if the distance to target is less than one frame's movement distance—if true, the NPC has arrived at its target
this.x = this.targetX;
Snaps the NPC's x position to the exact target x to prevent overshooting
this.y = this.targetY;
Snaps the NPC's y position to the exact target y to prevent overshooting
if (this.isCuttingFood && this.targetFoodIndex !== -1 && displayedEmojis[this.targetFoodIndex]) {
Checks three conditions: the NPC was eating food, it has a valid food index, and that food still exists in the array before clearing its targeted status
displayedEmojis[this.targetFoodIndex].isTargeted = false;
Marks the food as no longer targeted so another NPC can pursue it
this.isCuttingFood = false; // Reset flag
Clears the flag indicating this NPC was eating, preparing it for a new task
this.targetFoodIndex = -1; // Reset target index
Resets the food index to -1 (no target), clearing any reference to the previous food
this.setNewTarget();
Calls setNewTarget() to choose a new destination (pumpkin or food)
let angle = atan2(this.targetY - this.y, this.targetX - this.x);
Calculates the angle from the NPC to its target using atan2, which handles all quadrants correctly
this.x += cos(angle) * this.speed;
Moves the NPC toward the target by adding the horizontal component (cosine of angle times speed) to x
this.y += sin(angle) * this.speed;
Moves the NPC toward the target by adding the vertical component (sine of angle times speed) to y
if (this.isCuttingFood && this.targetFoodIndex !== -1 && displayedEmojis[this.targetFoodIndex]) {
Before eating, verify that the NPC is cutting food, has a valid target index, and that food item exists
displayedEmojis[this.targetFoodIndex].currentSize -= 0.1; // Shrink the emoji
Decreases the food's size by 0.1 pixels (in fontSize terms) each frame while the NPC is targeting it, simulating consumption
if (displayedEmojis[this.targetFoodIndex].currentSize <= 0) {
Checks if the food has shrunk to zero or below, meaning it has been completely eaten
displayedEmojis.splice(this.targetFoodIndex, 1); // Remove from array
Removes the fully eaten food object from the displayedEmojis array so it is no longer drawn or targetable

setNewTarget()

setNewTarget() demonstrates decision-making in agents: randomness drives high-level behavior choice, and filtering techniques find valid targets. The isTargeted flag is a coordination mechanism—it prevents multiple NPCs from pursuing the same food, creating emergent teamwork.

🔬 This code filters food to find items that no one is targeting and haven't been eaten. What happens if you remove the condition && emoji.currentSize > 0 so NPCs can target food that has already been partially eaten?

      let availableFood = displayedEmojis.filter(emoji => !emoji.isTargeted && emoji.currentSize > 0);
      if (availableFood.length > 0) {
  setNewTarget() {
    // Randomly decide if the next cut is for the pumpkin or the food
    if (random(1) < 0.3) { // 30% chance to cut food
      // Try to find an available food emoji to target
      let availableFood = displayedEmojis.filter(emoji => !emoji.isTargeted && emoji.currentSize > 0);
      if (availableFood.length > 0) {
        let targetEmoji = random(availableFood);
        this.targetFoodIndex = displayedEmojis.indexOf(targetEmoji);
        displayedEmojis[this.targetFoodIndex].isTargeted = true; // Mark as targeted
        this.isCuttingFood = true;
        this.targetX = targetEmoji.x;
        this.targetY = targetEmoji.y;
        this.speed = random(3, 6); // Faster speed for food slicing
      } else {
        // No food available, fall back to pumpkin
        this.isCuttingFood = false;
        this.targetFoodIndex = -1;
        this.setPumpkinTarget();
      }
    } else {
      this.isCuttingFood = false;
      this.targetFoodIndex = -1;
      this.setPumpkinTarget();
    }
  }
Line-by-line explanation (11 lines)

🔧 Subcomponents:

conditional Food vs pumpkin decision if (random(1) < 0.3) { // 30% chance to cut food

Uses randomness to decide whether the NPC will pursue food (30%) or pumpkin (70%)

calculation Find available food let availableFood = displayedEmojis.filter(emoji => !emoji.isTargeted && emoji.currentSize > 0);

Filters the food array to find only foods that aren't already being eaten and haven't been completely consumed

calculation Pick random food target let targetEmoji = random(availableFood);

Randomly selects one of the available food items to pursue

if (random(1) < 0.3) { // 30% chance to cut food
Generates a random number between 0 and 1; if it's less than 0.3, this NPC will try to target food (30% of the time)
let availableFood = displayedEmojis.filter(emoji => !emoji.isTargeted && emoji.currentSize > 0);
Uses the filter() method to create a new array containing only food items that haven't been targeted (!emoji.isTargeted) and haven't been completely eaten (emoji.currentSize > 0)
if (availableFood.length > 0) {
Checks if there is at least one food item available to target; if the array is empty, all food is either targeted or eaten
let targetEmoji = random(availableFood);
Picks a random food item from the availableFood array to become this NPC's next target
this.targetFoodIndex = displayedEmojis.indexOf(targetEmoji);
Finds the index of the chosen food in the main displayedEmojis array so the NPC can reference it during eating
displayedEmojis[this.targetFoodIndex].isTargeted = true; // Mark as targeted
Sets the isTargeted flag to true on that food item, preventing other NPCs from targeting the same item
this.isCuttingFood = true;
Sets this NPC's isCuttingFood flag to true, signaling that update() should shrink the food each frame
this.targetX = targetEmoji.x;
Sets the x coordinate of the target to the food emoji's current x position
this.targetY = targetEmoji.y;
Sets the y coordinate of the target to the food emoji's current y position
this.speed = random(3, 6); // Faster speed for food slicing
Gives this NPC a faster speed (3-6 pixels per frame) for food compared to pumpkin targeting, making food pursuit feel more aggressive
this.setPumpkinTarget();
Calls the setPumpkinTarget() helper method to set a target position somewhere on or in the pumpkin

setPumpkinTarget()

This method demonstrates polar-to-Cartesian conversion: angle and radius are converted to x and y coordinates using trigonometry. This technique is essential for placing objects in a circle, creating orbital motion, or distributing targets uniformly around a point.

  setPumpkinTarget() {
    // Generate a random target within the pumpkin's approximate bounds
    let angle = random(TWO_PI);
    let r = random(pumpkinWidth * 0.3, pumpkinWidth * 0.4); // Target slightly in from edge
    this.targetX = pumpkinX + cos(angle) * r;
    this.targetY = pumpkinY + sin(angle) * r;
    this.speed = random(2, 5); // Normal speed for pumpkin cutting
  }
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Random angle around pumpkin let angle = random(TWO_PI);

Generates a random angle from 0 to 2π radians (a full circle)

calculation Random distance from pumpkin center let r = random(pumpkinWidth * 0.3, pumpkinWidth * 0.4);

Picks a random distance that keeps the target near the pumpkin's surface rather than at the very edge

let angle = random(TWO_PI);
Generates a random angle between 0 and 2π (a full circle in radians), ensuring targets are picked uniformly around the pumpkin
let r = random(pumpkinWidth * 0.3, pumpkinWidth * 0.4);
Picks a random distance from the pumpkin center, constraining it to 30-40% of the pumpkin's width so targets are on or near its surface
this.targetX = pumpkinX + cos(angle) * r;
Calculates the x coordinate of the target by adding the horizontal component (cosine of angle times radius) to the pumpkin's center x
this.targetY = pumpkinY + sin(angle) * r;
Calculates the y coordinate of the target by adding the vertical component (sine of angle times radius) to the pumpkin's center y
this.speed = random(2, 5); // Normal speed for pumpkin cutting
Sets a slower speed (2-5 pixels per frame) for pumpkin targeting compared to food, making pumpkin work appear more deliberate

draw() [NPCCutter method]

This draw() method demonstrates the push/pop/translate pattern, which is fundamental to organizing complex drawings in p5.js. By translating to the NPC's position, you can draw shapes relative to a moving object without calculating absolute coordinates.

  draw() {
    // The NPC icon (small red circle) is now visible again.
    push();
    translate(this.x, this.y);
    fill(200, 50, 50); // Red color for NPC
    noStroke();
    ellipse(0, 0, 15, 15); // Small circle representing the NPC
    pop();
  }
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Transform isolation push();

Saves the current drawing state before applying transformations, ensuring they don't affect other drawings

calculation Move coordinate system to NPC position translate(this.x, this.y);

Moves the origin (0,0) to the NPC's position so shapes drawn at (0,0) appear at the NPC's actual location

push();
Saves the current transformation matrix and drawing state; any changes after this line won't affect drawings outside this function
translate(this.x, this.y);
Moves the coordinate origin to the NPC's x,y position, so shapes drawn at (0,0) will appear at the NPC's location
fill(200, 50, 50); // Red color for NPC
Sets the fill color to a reddish tone (RGB: 200, 50, 50) for all shapes drawn after this line
noStroke();
Removes the outline/stroke from the shape, so only the fill color is visible
ellipse(0, 0, 15, 15); // Small circle representing the NPC
Draws a circle (15 pixels wide and tall) at (0,0), which is now at the NPC's position due to translate()
pop();
Restores the saved transformation matrix and drawing state, so translate() no longer affects subsequent drawings

setup()

setup() runs once when the sketch launches and prepares all the data structures and initial state. It uses responsive calculations (based on windowWidth and windowHeight) so the sketch works on any screen size. The NPC spawning loop demonstrates how to populate an array of objects with varying initial conditions.

function setup() {
  // Create a canvas that fills the window
  createCanvas(windowWidth, windowHeight);

  // Ensure the canvas is landscape, as specified for your device
  if (width < height) {
    resizeCanvas(height, width);
  }

  // Calculate pumpkin size and position (left side)
  pumpkinWidth = min(width * 0.4, height * 0.7);
  pumpkinHeight = pumpkinWidth * 0.8;
  pumpkinX = width / 4; // Shift to the left
  pumpkinY = height / 2 + pumpkinHeight * 0.1;

  // Calculate food area size and position (right side)
  foodDiameter = min(width * 0.3, height * 0.5);
  foodX = (width / 4) * 3; // Shift to the right
  foodY = height / 2;

  // Populate displayedEmojis array initially
  populateEmojis(15); // Start with 15 food items

  // Initialize NPC cutters
  for (let i = 0; i < numNPCs; i++) {
    // Start NPCs at random positions within the pumpkin or food area
    let startX, startY;
    if (random(1) < 0.5) { // 50% chance to start on pumpkin
      let angle = random(TWO_PI);
      let r = random(pumpkinWidth * 0.3);
      startX = pumpkinX + cos(angle) * r;
      startY = pumpkinY + sin(angle) * r;
    } else { // 50% chance to start on food
      let angle = random(TWO_PI);
      let r = random(foodDiameter * 0.3);
      startX = foodX + cos(angle) * r;
      startY = foodY + cos(angle) * r;
    }
    npcCutters.push(new NPCCutter(startX, startY));
  }
}
Line-by-line explanation (18 lines)

🔧 Subcomponents:

calculation Canvas setup and landscape check createCanvas(windowWidth, windowHeight);

Creates a canvas that fills the browser window and ensures it is in landscape orientation

calculation Pumpkin dimensions and position pumpkinWidth = min(width * 0.4, height * 0.7);

Scales the pumpkin size relative to window dimensions and positions it on the left side

calculation Food area dimensions and position foodDiameter = min(width * 0.3, height * 0.5);

Scales the food area size and positions it on the right side, opposite the pumpkin

calculation Initial emoji population populateEmojis(15); // Start with 15 food items

Fills the food area with 15 randomly placed emoji objects

for-loop NPC initialization loop for (let i = 0; i < numNPCs; i++) {

Creates the specified number of NPC cutters at random starting positions

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window by using the built-in windowWidth and windowHeight variables
if (width < height) {
Checks if the canvas is taller than it is wide (portrait orientation)
resizeCanvas(height, width);
If portrait, swaps the dimensions to force landscape orientation by making width larger than height
pumpkinWidth = min(width * 0.4, height * 0.7);
Calculates the pumpkin's width as either 40% of the canvas width or 70% of the canvas height, whichever is smaller, ensuring it fits nicely on the left
pumpkinHeight = pumpkinWidth * 0.8;
Makes the pumpkin slightly shorter than it is wide (80% of width), giving it a natural pumpkin shape
pumpkinX = width / 4; // Shift to the left
Positions the pumpkin's center at one-quarter of the canvas width from the left edge
pumpkinY = height / 2 + pumpkinHeight * 0.1;
Positions the pumpkin vertically at the middle of the canvas, slightly lower by adding 10% of its height
foodDiameter = min(width * 0.3, height * 0.5);
Calculates the food area's diameter (30% of width or 50% of height, whichever is smaller) to balance the pumpkin
foodX = (width / 4) * 3; // Shift to the right
Positions the food area's center at three-quarters of the canvas width from the left, placing it on the right side
foodY = height / 2;
Positions the food area vertically at the exact middle of the canvas, aligned with the pumpkin's vertical center
populateEmojis(15); // Start with 15 food items
Calls populateEmojis() to create 15 food emoji objects with random positions in the food area
for (let i = 0; i < numNPCs; i++) {
Loops numNPCs times (5 times by default) to create each NPC cutter
if (random(1) < 0.5) { // 50% chance to start on pumpkin
Randomly decides whether this NPC will spawn near the pumpkin (50%) or near the food area (50%)
let angle = random(TWO_PI);
Picks a random angle (0 to 2π radians) to place the NPC somewhere around the pumpkin or food area
let r = random(pumpkinWidth * 0.3);
Picks a random distance from the pumpkin's center, no more than 30% of the pumpkin's width, placing the NPC within the pumpkin area
startX = pumpkinX + cos(angle) * r;
Calculates the NPC's starting x position by combining the pumpkin's center x with the horizontal component of the angle and radius
startY = pumpkinY + sin(angle) * r;
Calculates the NPC's starting y position by combining the pumpkin's center y with the vertical component of the angle and radius
npcCutters.push(new NPCCutter(startX, startY));
Creates a new NPCCutter object at the calculated starting position and adds it to the npcCutters array

draw()

draw() is the heartbeat of the sketch, running 60 times per second. Every frame, it clears the canvas, redraws static elements (pumpkin, food), updates all dynamic NPCs, and respawns food as needed. The structure is typical for any p5.js animation: clear, update, draw, check conditions.

function draw() {
  background(240, 248, 255); // Light blue background

  drawPumpkin(); // Draw the pumpkin
  drawFoodArea(); // Draw the food emojis
  // drawCuts() and drawFoodCuts() were already removed

  // Update and draw each NPC cutter
  for (let cutter of npcCutters) {
    cutter.update();
    cutter.draw(); // This now draws the red circle
  }

  // Respawn food if the amount drops too low
  if (displayedEmojis.length < 5) {
    populateEmojis(1); // Add one new food item at a time
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Background clearing background(240, 248, 255); // Light blue background

Clears the canvas with a light blue color at the start of each frame, erasing the previous frame's drawings

for-loop NPC update and draw loop for (let cutter of npcCutters) {

Iterates through all NPC cutters, updating their positions and redrawing them

conditional Food respawn check if (displayedEmojis.length < 5) {

Checks if food population has dropped too low and adds more items if needed

background(240, 248, 255); // Light blue background
Clears the canvas with light blue (RGB: 240, 248, 255) at the start of each frame, erasing everything drawn in the previous frame
drawPumpkin(); // Draw the pumpkin
Calls the drawPumpkin() function to render the pumpkin and its ridges at the left side of the canvas
drawFoodArea(); // Draw the food emojis
Calls the drawFoodArea() function to render all the food emoji items in the right-side food area
for (let cutter of npcCutters) {
Starts a for-of loop that iterates through each NPC cutter object stored in the npcCutters array
cutter.update();
Calls the update() method on the NPC, which moves it toward its target and handles eating/shrinking food
cutter.draw(); // This now draws the red circle
Calls the draw() method on the NPC, which renders a small red circle at the NPC's current position
if (displayedEmojis.length < 5) {
Checks if the number of food items has dropped below 5, indicating the NPCs have eaten most of the food
populateEmojis(1); // Add one new food item at a time
Calls populateEmojis(1) to add one new food emoji to the displayedEmojis array, maintaining a continuous supply of food to hunt

populateEmojis()

populateEmojis() creates food objects dynamically and is called both at startup and during gameplay to respawn eaten food. It demonstrates how to create custom objects in JavaScript using object literals and how to use polar-to-Cartesian conversion to distribute items in a circle.

🔬 This code uses polar coordinates (angle and radius) to place food in a circle. What happens if you change 0.45 to 1.0 so food can spawn anywhere within the full food area diameter?

    let angle = random(TWO_PI);
    let r = random(foodDiameter * 0.45); // Position within food area
    let x = foodX + cos(angle) * r;
    let y = foodY + sin(angle) * r;
function populateEmojis(count) {
  let emojiSize = foodDiameter * 0.15; // Size of each emoji

  for (let i = 0; i < count; i++) {
    let angle = random(TWO_PI);
    let r = random(foodDiameter * 0.45); // Position within food area
    let x = foodX + cos(angle) * r;
    let y = foodY + sin(angle) * r;
    let emoji = random(foodEmojis); // Pick a random emoji
    displayedEmojis.push({
      emoji: emoji,
      x: x,
      y: y,
      initialSize: emojiSize, // Store original size
      currentSize: emojiSize, // Mutable size
      isTargeted: false // Flag to prevent multiple NPCs targeting same food
    });
  }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

calculation Emoji size calculation let emojiSize = foodDiameter * 0.15; // Size of each emoji

Calculates the font size for emojis based on the food area diameter

for-loop Food item creation loop for (let i = 0; i < count; i++) {

Loops 'count' times, creating a new food object each iteration

calculation Random position within food area let angle = random(TWO_PI);

Picks a random direction and distance to place the emoji within the circular food area

calculation Food object structure displayedEmojis.push({

Creates a new food object with all required properties and adds it to the displayedEmojis array

let emojiSize = foodDiameter * 0.15; // Size of each emoji
Calculates the font size for rendering emojis as 15% of the food area's diameter, scaling food size with the canvas
for (let i = 0; i < count; i++) {
Loops from 0 to count-1, creating 'count' food items in each iteration
let angle = random(TWO_PI);
Generates a random angle (0 to 2π) to uniformly distribute food around the food area
let r = random(foodDiameter * 0.45); // Position within food area
Picks a random distance from the food area's center (up to 45% of its diameter), ensuring food stays within the visible area
let x = foodX + cos(angle) * r;
Calculates the emoji's x coordinate using the center, angle, and radius—a polar-to-Cartesian conversion
let y = foodY + sin(angle) * r;
Calculates the emoji's y coordinate using the center, angle, and radius
let emoji = random(foodEmojis); // Pick a random emoji
Selects a random emoji from the foodEmojis array to give variety to the food items
displayedEmojis.push({
Begins creating a new object literal (a JavaScript object with key-value pairs) and adds it to the displayedEmojis array
emoji: emoji,
Stores the randomly chosen emoji character in the object
x: x,
Stores the calculated x position
y: y,
Stores the calculated y position
initialSize: emojiSize, // Store original size
Saves the original emoji size for reference (used if you want to restore or compare)
currentSize: emojiSize, // Mutable size
Stores the emoji's current size, which will decrease as NPCs eat it
isTargeted: false // Flag to prevent multiple NPCs targeting same food
Initializes the flag as false (not being targeted); NPCs set this to true when they choose to eat this food

drawPumpkin()

drawPumpkin() demonstrates compositing (drawing multiple shapes to create one visual object) and using loops with trigonometry to create radial patterns. The map() function elegantly converts loop indices into angles, a core technique for creating symmetrical shapes.

🔬 This loop maps the ridge counter to angles from -72° to +72°, showing only the front of the pumpkin. What happens if you change the angle range to go full circle: map(i, 0, numRidges, 0, TWO_PI)?

  for (let i = 0; i < numRidges; i++) {
    let angle = map(i, 0, numRidges, -HALF_PI * 0.8, HALF_PI * 0.8);
function drawPumpkin() {
  // Stem
  fill(34, 139, 34);
  noStroke();
  rect(pumpkinX - pumpkinWidth * 0.05, pumpkinY - pumpkinHeight * 0.6, pumpkinWidth * 0.1, pumpkinHeight * 0.2);

  // Pumpkin body
  fill(255, 140, 0);
  noStroke();
  ellipse(pumpkinX, pumpkinY, pumpkinWidth, pumpkinHeight);

  // Ridges
  stroke(200, 100, 0);
  strokeWeight(2);
  let numRidges = 8;
  for (let i = 0; i < numRidges; i++) {
    let angle = map(i, 0, numRidges, -HALF_PI * 0.8, HALF_PI * 0.8);
    let x1 = pumpkinX + cos(angle) * pumpkinWidth / 2;
    let y1 = pumpkinY + sin(angle) * pumpkinHeight / 2;
    let x2 = pumpkinX + cos(angle) * pumpkinWidth * 0.4;
    let y2 = pumpkinY + sin(angle) * pumpkinHeight * 0.4;
    line(x1, y1, x2, y2);
  }
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

calculation Stem rendering rect(pumpkinX - pumpkinWidth * 0.05, pumpkinY - pumpkinHeight * 0.6, pumpkinWidth * 0.1, pumpkinHeight * 0.2);

Draws a green rectangle at the top of the pumpkin to represent the stem

calculation Pumpkin body rendering ellipse(pumpkinX, pumpkinY, pumpkinWidth, pumpkinHeight);

Draws the main orange ellipse that forms the pumpkin's body

for-loop Pumpkin ridge lines for (let i = 0; i < numRidges; i++) {

Loops 8 times to draw evenly spaced vertical ridge lines on the pumpkin

fill(34, 139, 34);
Sets the fill color to forest green (a standard dark green) for the stem
noStroke();
Removes the outline stroke so the stem is a solid green rectangle
rect(pumpkinX - pumpkinWidth * 0.05, pumpkinY - pumpkinHeight * 0.6, pumpkinWidth * 0.1, pumpkinHeight * 0.2);
Draws a rectangle for the stem: positioned at the top of the pumpkin (pumpkinY - 0.6*height), centered horizontally (±5% of width), with a width of 10% and height of 20%
fill(255, 140, 0);
Sets the fill color to orange (RGB: 255, 140, 0), the classic pumpkin color
ellipse(pumpkinX, pumpkinY, pumpkinWidth, pumpkinHeight);
Draws the main pumpkin body as an ellipse centered at (pumpkinX, pumpkinY) with the specified width and height
stroke(200, 100, 0);
Sets the stroke (outline) color to a darker orange for the ridge lines
strokeWeight(2);
Sets the stroke thickness to 2 pixels, making the ridge lines visible but not overwhelming
let numRidges = 8;
Defines that the pumpkin will have 8 vertical ridges for a natural appearance
for (let i = 0; i < numRidges; i++) {
Loops from 0 to 7 (8 iterations) to create the 8 ridge lines
let angle = map(i, 0, numRidges, -HALF_PI * 0.8, HALF_PI * 0.8);
Maps the loop counter i from 0-8 to angles from -1.26 to +1.26 radians (~-72° to +72°), spreading ridges across the front half of the pumpkin
let x1 = pumpkinX + cos(angle) * pumpkinWidth / 2;
Calculates the starting x coordinate of a ridge line at the pumpkin's edge using cosine
let y1 = pumpkinY + sin(angle) * pumpkinHeight / 2;
Calculates the starting y coordinate of a ridge line at the pumpkin's edge using sine
let x2 = pumpkinX + cos(angle) * pumpkinWidth * 0.4;
Calculates the ending x coordinate, pulled inward to 40% of the pumpkin's width
let y2 = pumpkinY + sin(angle) * pumpkinHeight * 0.4;
Calculates the ending y coordinate, pulled inward to 40% of the pumpkin's height
line(x1, y1, x2, y2);
Draws a line from the outer point to the inner point, creating the ridge effect

drawFoodArea()

drawFoodArea() demonstrates text rendering at dynamic sizes and filtering arrays to conditionally draw items. The filter() call is an elegant way to skip rendering food that has been eaten without needing to manually delete it in update().

function drawFoodArea() {
  // Draw a very light background circle just to define the area
  fill(255, 250, 240, 50); // Light cream with some transparency
  noStroke();
  ellipse(foodX, foodY, foodDiameter, foodDiameter);

  // Draw the randomly selected emojis
  textAlign(CENTER, CENTER);
  // Filter out emojis that have been completely eaten
  for (let foodItem of displayedEmojis.filter(emoji => emoji.currentSize > 0)) {
    textSize(foodItem.currentSize); // Use currentSize for scaling
    text(foodItem.emoji, foodItem.x, foodItem.y);
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Food area background circle ellipse(foodX, foodY, foodDiameter, foodDiameter);

Draws a faint circular boundary to visually define the food area

for-loop Food emoji rendering loop for (let foodItem of displayedEmojis.filter(emoji => emoji.currentSize > 0)) {

Loops through all food items that still have size > 0 and renders their emojis at their current size

fill(255, 250, 240, 50); // Light cream with some transparency
Sets fill to a very light cream color with 50 alpha (transparency out of 255), making it almost invisible but subtly visible
noStroke();
Removes the outline, so only the filled interior is visible
ellipse(foodX, foodY, foodDiameter, foodDiameter);
Draws a circular boundary at the food area center with the calculated diameter, defining the space where food appears
textAlign(CENTER, CENTER);
Sets text alignment to center both horizontally and vertically, so emojis are centered at their x,y coordinates
for (let foodItem of displayedEmojis.filter(emoji => emoji.currentSize > 0)) {
Loops through displayedEmojis, but only for items where currentSize is greater than 0 (not completely eaten), using filter() to exclude eaten food
textSize(foodItem.currentSize); // Use currentSize for scaling
Sets the font size to the food item's current size, which shrinks as NPCs eat it, making the visual eating effect
text(foodItem.emoji, foodItem.x, foodItem.y);
Renders the emoji character at its current position and size

windowResized()

windowResized() is called automatically by p5.js whenever the browser window is resized. It demonstrates responsive design: recalculating positions based on new dimensions, clearing stale data (emojis), and resetting dynamic objects (NPCs) to match the new layout. This is essential for sketches that adapt to any screen size.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);

  if (width < height) {
    resizeCanvas(height, width);
  }

  // Re-calculate pumpkin size and position
  pumpkinWidth = min(width * 0.4, height * 0.7);
  pumpkinHeight = pumpkinWidth * 0.8;
  pumpkinX = width / 4;
  pumpkinY = height / 2 + pumpkinHeight * 0.1;

  // Re-calculate food area size and position
  foodDiameter = min(width * 0.3, height * 0.5);
  foodX = (width / 4) * 3;
  foodY = height / 2;

  // Clear and re-populate emojis for the new size
  displayedEmojis = [];
  populateEmojis(15);

  // Reset NPC cutters to new positions and targets based on the resized canvas
  for (let cutter of npcCutters) {
    let startX, startY;
    if (random(1) < 0.5) { // 50% chance to start on pumpkin
      let angle = random(TWO_PI);
      let r = random(pumpkinWidth * 0.3);
      startX = pumpkinX + cos(angle) * r;
      startY = pumpkinY + sin(angle) * r;
    } else { // 50% chance to start on food
      let angle = random(TWO_PI);
      let r = random(foodDiameter * 0.3);
      startX = foodX + cos(angle) * r;
      startY = foodY + sin(angle) * r;
    }
    cutter.x = startX;
    cutter.y = startY;
    cutter.isCuttingFood = false; // Reset cutting state
    cutter.targetFoodIndex = -1; // Reset target index
    cutter.setNewTarget(); // Set a new target based on resized canvas
  }
}
Line-by-line explanation (18 lines)

🔧 Subcomponents:

conditional Landscape orientation check if (width < height) {

Ensures the canvas maintains landscape orientation after a resize

calculation Food respawning on resize displayedEmojis = [];

Clears existing food and creates a fresh set at the new positions

for-loop NPC reset loop for (let cutter of npcCutters) {

Resets each NPC to a new starting position and clears its current task

resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to the new window dimensions
if (width < height) {
Checks if the new canvas is portrait (taller than wide)
resizeCanvas(height, width);
If portrait, swaps dimensions to force landscape orientation
pumpkinWidth = min(width * 0.4, height * 0.7);
Recalculates the pumpkin width to fit the resized canvas
pumpkinHeight = pumpkinWidth * 0.8;
Recalculates pumpkin height proportionally
pumpkinX = width / 4;
Repositions the pumpkin's center on the left side of the new canvas
pumpkinY = height / 2 + pumpkinHeight * 0.1;
Repositions the pumpkin vertically
foodDiameter = min(width * 0.3, height * 0.5);
Recalculates the food area diameter for the new canvas
foodX = (width / 4) * 3;
Repositions the food area on the right side
foodY = height / 2;
Repositions the food area vertically
displayedEmojis = [];
Clears the entire displayedEmojis array to remove stale food from the old canvas size
populateEmojis(15);
Repopulates with 15 fresh food items positioned for the new canvas dimensions
for (let cutter of npcCutters) {
Loops through each NPC to reset its position and state
cutter.x = startX;
Sets the NPC to the new starting position
cutter.y = startY;
Sets the NPC's y position
cutter.isCuttingFood = false; // Reset cutting state
Clears the flag so the NPC is not frozen mid-eating
cutter.targetFoodIndex = -1; // Reset target index
Clears any reference to a food target that may no longer exist
cutter.setNewTarget(); // Set a new target based on resized canvas
Calls setNewTarget() to give the NPC a fresh task in the resized environment

📦 Key Variables

pumpkinX number

Stores the horizontal center position of the pumpkin on the canvas

let pumpkinX = width / 4;
pumpkinY number

Stores the vertical center position of the pumpkin on the canvas

let pumpkinY = height / 2 + pumpkinHeight * 0.1;
pumpkinWidth number

Stores the width of the pumpkin ellipse in pixels, calculated to scale with the canvas

let pumpkinWidth = min(width * 0.4, height * 0.7);
pumpkinHeight number

Stores the height of the pumpkin ellipse, calculated as 80% of its width

let pumpkinHeight = pumpkinWidth * 0.8;
foodX number

Stores the horizontal center position of the food area on the right side of the canvas

let foodX = (width / 4) * 3;
foodY number

Stores the vertical center position of the food area

let foodY = height / 2;
foodDiameter number

Stores the diameter of the circular food area in pixels

let foodDiameter = min(width * 0.3, height * 0.5);
npcCutters array

Stores all NPCCutter objects, one for each autonomous agent in the scene

let npcCutters = [];
numNPCs number

Controls how many autonomous agents are created and active in the sketch

let numNPCs = 5;
foodEmojis array

A constant array of food emoji strings that are randomly chosen when populating the food area

const foodEmojis = ['🌶️', '🥨', '🧇', '🥓', ...];
displayedEmojis array

Stores all active food objects currently visible on the canvas, each with position, size, and targeting status

let displayedEmojis = [];

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG setup() NPC spawning - food area branch

In the else branch that spawns NPCs near the food area, the line 'startY = foodY + cos(angle) * r;' uses cosine instead of sine, causing incorrect vertical distribution.

💡 Change 'startY = foodY + cos(angle) * r;' to 'startY = foodY + sin(angle) * r;' to match the pumpkin spawning logic and distribute NPCs properly around the food circle.

PERFORMANCE drawFoodArea()

The filter() call in the for-of loop creates a new array every frame, even if most food items are visible. This wastes memory allocation on every frame.

💡 Cache the filtered results or use a simpler for-loop with an if-statement: 'for (let foodItem of displayedEmojis) { if (foodItem.currentSize > 0) { textSize(...); text(...); } }' to avoid unnecessary array creation.

STYLE Global variables

Some variable names like 'displayedEmojis' could be more consistent with the naming style used elsewhere (e.g., 'foodItems' would be clearer).

💡 Consider renaming 'displayedEmojis' to 'foodItems' throughout the sketch for improved readability and consistency.

FEATURE NPCCutter class

NPCs that are eating food don't visually change or shrink, so it's hard to see which NPC is actively consuming what.

💡 Add a visual indicator in the draw() method: change the NPC's color or size when isCuttingFood is true, like 'fill(this.isCuttingFood ? 255 : 100, 50, 50);' to show active vs. idle states.

BUG update() food consumption

When an NPC finishes eating food and calls setNewTarget(), it's possible that food regeneration happens in populateEmojis() on the same frame, causing the targetFoodIndex reference to become invalid (indices shift after splice).

💡 Use a 'markedForDeletion' flag instead of splice(), then delete marked items at the end of each draw() loop, ensuring index consistency throughout the frame.

🔄 Code Flow

Code flow showing npccutter, update, setnewtarget, setpumpkintarget, draw, setup, draw, populateemojis, drawpumpkin, drawfoodarea, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> background-clear[background-clear] draw --> npc-loop[npc-loop] draw --> food-respawn[food-respawn] draw --> drawpumpkin[drawPumpkin] draw --> drawfoodarea[drawFoodArea] setup --> canvas-creation[canvas-creation] setup --> pumpkin-sizing[pumpkin-sizing] setup --> food-sizing[food-sizing] setup --> emoji-initialization[emoji-initialization] setup --> npc-spawning[npc-spawning] npc-loop --> npc-repositioning[npc-repositioning] npc-loop --> update[update] update --> distance-check[distance-check] update --> movement-calculation[movement-calculation] update --> setnewtarget[setNewTarget] setnewtarget --> random-choice[random-choice] setnewtarget --> filter-food[filter-food] setnewtarget --> random-selection[random-selection] setnewtarget --> setpumpkintarget[setPumpkinTarget] setpumpkintarget --> angle-calc[angle-calc] setpumpkintarget --> radius-calc[radius-calc] movement-calculation --> shrink-food[shrink-food] movement-calculation --> food-removal[food-removal] drawpumpkin --> push-pop[push-pop] drawpumpkin --> translate[translate] drawpumpkin --> stem-draw[stem-draw] drawpumpkin --> body-draw[body-draw] drawpumpkin --> ridge-loop[ridge-loop] drawfoodarea --> area-bg[area-bg] drawfoodarea --> emoji-loop[emoji-loop] emoji-loop --> size-calc[size-calc] emoji-loop --> random-position[random-position] emoji-loop --> object-creation[object-creation] click setup href "#fn-setup" click draw href "#fn-draw" click background-clear href "#sub-background-clear" click npc-loop href "#sub-npc-loop" click food-respawn href "#sub-food-respawn" click drawpumpkin href "#fn-drawpumpkin" click drawfoodarea href "#fn-drawfoodarea" click canvas-creation href "#sub-canvas-creation" click pumpkin-sizing href "#sub-pumpkin-sizing" click food-sizing href "#sub-food-sizing" click emoji-initialization href "#sub-emoji-initialization" click npc-spawning href "#sub-npc-spawning" click npc-repositioning href "#sub-npc-repositioning" click update href "#fn-update" click distance-check href "#sub-distance-check" click movement-calculation href "#sub-movement-calculation" click setnewtarget href "#fn-setnewtarget" click random-choice href "#sub-random-choice" click filter-food href "#sub-filter-food" click random-selection href "#sub-random-selection" click setpumpkintarget href "#fn-setpumpkintarget" click angle-calc href "#sub-angle-calc" click radius-calc href "#sub-radius-calc" click shrink-food href "#sub-shrink-food" click food-removal href "#sub-food-removal" click push-pop href "#sub-push-pop" click translate href "#sub-translate" click stem-draw href "#sub-stem-draw" click body-draw href "#sub-body-draw" click ridge-loop href "#sub-ridge-loop" click area-bg href "#sub-area-bg" click emoji-loop href "#sub-emoji-loop" click size-calc href "#sub-size-calc" click random-position href "#sub-random-position" click object-creation href "#sub-object-creation"

❓ Frequently Asked Questions

What visual elements can I expect to see in the p5.js Sketch 2026-02-22 21:31?

This sketch features animated NPC cutters that move towards and 'cut' food emojis, which are randomly displayed on the canvas.

How can I interact with the p5.js Sketch 2026-02-22 21:31?

While the sketch primarily runs on its own, you can observe the NPCs as they navigate the canvas and engage with the food emojis.

What creative coding concepts are showcased in the p5.js Sketch 2026-02-22 21:31?

The sketch demonstrates object-oriented programming through the NPCCutter class and incorporates random movement and targeting mechanics for the NPCs.

Preview

Sketch 2026-02-22 21:31 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2026-02-22 21:31 - Code flow showing npccutter, update, setnewtarget, setpumpkintarget, draw, setup, draw, populateemojis, drawpumpkin, drawfoodarea, windowresized
Code Flow Diagram