Sketch 2026-02-22 22:27

This sketch simulates a busy emoji restaurant where customers arrive, find seats at tables, eat food, and leave. Click or tap the kitchen area to generate food items that customers will eat, creating a dynamic, interactive restaurant simulation with state machines and object-oriented design.

🧪 Try This!

Experiment with the code by making these changes:

  1. Double customer arrival rate — Customers arrive twice as fast, creating more crowding and chaos at the tables.
  2. Make all emojis the same food
  3. Make the restaurant darker — Changes the floor color from warm beige to a dark brown, creating a more upscale mood.
  4. Speed up eating — Customers eat twice as fast and leave sooner, reducing table occupancy.
  5. Add more tables
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a lively emoji restaurant where small colored circles (customers) walk in, find empty chairs at four tables, eat food you generate by clicking, and leave. The magic lies in three custom classes—Customer, Table, and Food—that work together using state machines to create believable behavior. You will see customers arrive continuously, wander if all tables are full, sit down and animate eating, then depart. Each customer moves through distinct states (ARRIVING, SEEKING_TABLE, MOVING_TO_CHAIR, EATING, LEAVING), and the code uses `atan2()` and distance calculations to navigate them toward their target chairs.

The sketch is organized into a setup() that creates the four tables, a draw() loop that updates and displays all objects, and three class definitions that handle the logic for customers, tables, and food. By studying it, you will learn how to structure complex simulations using classes, how state machines simplify behavior, how to use object references to link related entities (chairs to customers, food to tables), and how to clean up finished objects from arrays to keep memory efficient.

⚙️ How It Works

  1. When the sketch loads, setup() creates a canvas, defines four table objects positioned around the screen, and spawns an initial batch of customers starting off-screen to the left.
  2. Every frame, draw() refreshes the background, draws the kitchen and tables, and loops through all customers and food items to update and display them.
  3. New customers arrive automatically every 180 frames if the maximum hasn't been reached; each new customer enters from the left with a random speed and skin tone.
  4. Each customer runs an update() method that follows a state machine: ARRIVING (walk right until x ≥ 200), SEEKING_TABLE (scan tables for empty chairs and navigate toward one), MOVING_TO_CHAIR (move toward the target chair using atan2 and trigonometry), EATING (stay seated and animate eating for 180–360 frames), and LEAVING (exit to the right).
  5. When a customer sits, the eat() method creates a new Food object with a random emoji at the chair and links it; the customer's mouth opens periodically to animate eating.
  6. You can click or tap the kitchen to generate new food items; they appear in the kitchen area ready for customers to eat.
  7. When a customer finishes eating, the food is marked as eaten and removed; the chair is freed and the customer exits.

🎓 Concepts You'll Learn

State machines (ARRIVING, SEEKING_TABLE, MOVING_TO_CHAIR, EATING, LEAVING)Object-oriented design with classesPathfinding with atan2 and trigonometryObject references and linkingArray cleanup and memory managementInput handling (mousePressed, touchStarted)Window responsiveness (windowResized)

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It is the perfect place to initialize the canvas, create game objects, and set up starting conditions. Notice how we use width and height (p5.js variables that automatically match the canvas size) to position tables responsively.

function setup() {
  createCanvas(windowWidth, windowHeight);
  textFont('Arial'); // Emojis usually work well with a default system font

  // Define table layout (adjust positions as needed)
  tables.push(new Table(width / 3, height / 3, 2));
  tables.push(new Table(2 * width / 3, height / 3, 2));
  tables.push(new Table(width / 3, 2 * height / 3, 2));
  tables.push(new Table(2 * width / 3, 2 * height / 3, 2));

  // Initialize initial customers
  for (let i = 0; i < numInitialCustomers; i++) {
    customers.push(new Customer(random(-100, -50), random(height))); // Start off-screen
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

function-call Canvas Creation createCanvas(windowWidth, windowHeight);

Creates a fullscreen canvas that fills the entire browser window

for-loop Table Layout tables.push(new Table(width / 3, height / 3, 2));

Four tables are created in a grid pattern—one in each quadrant of the screen

for-loop Initial Customer Spawn for (let i = 0; i < numInitialCustomers; i++) { customers.push(new Customer(random(-100, -50), random(height))); }

Spawns the initial batch of customers off-screen to the left so they enter from outside

createCanvas(windowWidth, windowHeight);
Creates a canvas that spans the full browser window, making the restaurant responsive to screen size
textFont('Arial');
Sets Arial as the font—used when drawing emojis and text labels like 'Kitchen'
tables.push(new Table(width / 3, height / 3, 2));
Creates and adds the first table to the upper-left quadrant (at 1/3 width and 1/3 height) with 2 chairs
tables.push(new Table(2 * width / 3, height / 3, 2));
Adds the second table to the upper-right quadrant at 2/3 width and 1/3 height
tables.push(new Table(width / 3, 2 * height / 3, 2));
Adds the third table to the lower-left quadrant at 1/3 width and 2/3 height
tables.push(new Table(2 * width / 3, 2 * height / 3, 2));
Adds the fourth table to the lower-right quadrant at 2/3 width and 2/3 height
for (let i = 0; i < numInitialCustomers; i++) {
Loop that repeats numInitialCustomers times (default 6) to spawn the starting crowd
customers.push(new Customer(random(-100, -50), random(height)));
Creates a new Customer at a random position off-screen left (x between -100 and -50) and a random vertical position, then adds it to the customers array

draw()

draw() is called 60 times per second by p5.js. It is where animation happens: background() clears the old frame, then display() and update() methods on all objects redraw the new frame. Notice we loop backwards through customers when removing them—this prevents skipping items when the array shrinks.

🔬 This loop counts backwards (from length-1 down to 0) instead of forwards. What do you think would break if you changed it to count forwards (let i = 0; i < customers.length; i++)? Try it and remove a customer while the loop runs—why does something disappear?

  // Update and display customers
  for (let i = customers.length - 1; i >= 0; i--) {
    let customer = customers[i];
    customer.update(); // Update customer state and position
    customer.display(); // Draw the customer

🔬 The !food.inKitchen check prevents kitchen food from being removed. What happens if you remove that check and change the condition to just if (food.isEaten()) {}? Try generating food in the kitchen, letting it be eaten, then clicking the kitchen again—do you think kitchen food should stick around?

  // Remove food if eaten (and not in kitchen)
    if (food.isEaten() && !food.inKitchen) {
      foodItems.splice(i, 1);
    }
function draw() {
  background(240, 230, 200); // Restaurant floor color

  // Draw kitchen
  fill(180);
  rect(kitchenPos.x, kitchenPos.y, kitchenPos.width, kitchenPos.height);
  fill(0);
  textSize(18);
  text("Kitchen", kitchenPos.x + kitchenPos.width / 2, kitchenPos.y + kitchenPos.height / 2);

  // Draw tables and chairs
  for (let table of tables) {
    table.display();
  }

  // Update and display customers
  for (let i = customers.length - 1; i >= 0; i--) {
    let customer = customers[i];
    customer.update(); // Update customer state and position
    customer.display(); // Draw the customer

    // Remove customer if they've left the screen
    if (customer.state === 'LEAVING' && customer.x > width + 50) {
      customers.splice(i, 1);
    }
  }

  // Attempt to add new customers
  if (frameCount - lastCustomerArrival > customerArrivalInterval && customers.length < maxCustomers) {
    customers.push(new Customer(random(-100, -50), random(height)));
    lastCustomerArrival = frameCount;
  }

  // Update and display food items (only those currently on tables or in kitchen)
  for (let i = foodItems.length - 1; i >= 0; i--) {
    let food = foodItems[i];
    food.display();

    // Remove food if eaten (and not in kitchen)
    if (food.isEaten() && !food.inKitchen) {
      foodItems.splice(i, 1);
    }
  }
}
Line-by-line explanation (19 lines)

🔧 Subcomponents:

function-call Background Clear background(240, 230, 200);

Clears the canvas each frame with a warm beige color, erasing everything from the previous frame

block Kitchen Drawing fill(180); rect(kitchenPos.x, kitchenPos.y, kitchenPos.width, kitchenPos.height);

Draws the kitchen area as a gray rectangle in the upper-left corner

for-loop Table Display Loop for (let table of tables) { table.display(); }

Loops through all tables and calls their display() method to draw them and their chairs

for-loop Customer Update Loop for (let i = customers.length - 1; i >= 0; i--) { let customer = customers[i]; customer.update(); customer.display(); if (customer.state === 'LEAVING' && customer.x > width + 50) { customers.splice(i, 1); } }

Updates each customer's state and position, draws them, and removes them from the array once they exit the right side of the screen

conditional New Customer Arrival if (frameCount - lastCustomerArrival > customerArrivalInterval && customers.length < maxCustomers) { customers.push(new Customer(random(-100, -50), random(height))); lastCustomerArrival = frameCount; }

Checks if enough frames have passed since the last arrival and if we haven't hit the max customer limit; if so, spawns a new customer

for-loop Food Display Loop for (let i = foodItems.length - 1; i >= 0; i--) { let food = foodItems[i]; food.display(); if (food.isEaten() && !food.inKitchen) { foodItems.splice(i, 1); } }

Displays all food items and removes eaten food from the array (except kitchen food for safety)

background(240, 230, 200);
Clears the entire canvas with a light beige RGB color (240, 230, 200)—this must be first so nothing flickers
fill(180);
Sets the fill color to gray (180, 180, 180) for the kitchen rectangle
rect(kitchenPos.x, kitchenPos.y, kitchenPos.width, kitchenPos.height);
Draws the kitchen as a rectangle using the position and size defined in the kitchenPos constant object
fill(0);
Changes fill color to black (0, 0, 0) for the 'Kitchen' text label
textSize(18);
Sets the text size to 18 pixels for the kitchen label
text("Kitchen", kitchenPos.x + kitchenPos.width / 2, kitchenPos.y + kitchenPos.height / 2);
Draws the text 'Kitchen' centered inside the kitchen rectangle using the center coordinates
for (let table of tables) {
Loops through every table in the tables array using a for-of loop (modern and clean syntax)
table.display();
Calls the table's display() method, which draws the table itself and all its chairs
for (let i = customers.length - 1; i >= 0; i--) {
Loops backwards through the customers array (i starts at the last index and counts down)—this is safe because we remove items during iteration
customer.update();
Calls update() on the customer to process their state machine logic and move them
customer.display();
Calls display() on the customer to draw their body, head, eyes, and mouth
if (customer.state === 'LEAVING' && customer.x > width + 50) {
Checks if the customer is in LEAVING state AND has moved far enough off the right edge (beyond width + 50 pixels)
customers.splice(i, 1);
Removes this customer from the array to save memory (splice removes 1 item at index i)
if (frameCount - lastCustomerArrival > customerArrivalInterval && customers.length < maxCustomers) {
Checks two conditions: enough frames have elapsed since the last arrival (frameCount is a p5.js variable counting frames), AND we haven't hit the max customer limit
customers.push(new Customer(random(-100, -50), random(height)));
Creates and adds a new Customer at a random off-screen position to the left and a random height
lastCustomerArrival = frameCount;
Records the current frame number so we can wait customerArrivalInterval frames before allowing another arrival
food.display();
Calls display() on the food item to draw its emoji
if (food.isEaten() && !food.inKitchen) {
Checks if the food has been eaten AND is not in the kitchen (kitchen food stays visible as a supply)
foodItems.splice(i, 1);
Removes the eaten food from the array to free memory

windowResized()

windowResized() is a special p5.js function that fires whenever the user resizes their browser window. By recreating the tables with fractional coordinates (width / 3, etc.), we make the restaurant responsive to different screen sizes.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Re-adjust table positions for new window size
  tables = [];
  tables.push(new Table(width / 3, height / 3, 2));
  tables.push(new Table(2 * width / 3, height / 3, 2));
  tables.push(new Table(width / 3, 2 * height / 3, 2));
  tables.push(new Table(2 * width / 3, 2 * height / 3, 2));
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

function-call Canvas Resize resizeCanvas(windowWidth, windowHeight);

Adjusts the canvas dimensions to match the new browser window size

block Table Repositioning tables = []; tables.push(new Table(width / 3, height / 3, 2));

Clears and recreates all four tables at positions proportional to the new canvas size

resizeCanvas(windowWidth, windowHeight);
p5.js automatically calls this function when the browser window is resized; it stretches the canvas to the new dimensions
tables = [];
Clears the tables array so we can recreate tables at the correct new positions (essential—otherwise old tables stay at old positions)
tables.push(new Table(width / 3, height / 3, 2));
Recreates the first table using the new width and height, which now proportionally position it in the resized canvas

mousePressed()

mousePressed() is a p5.js event handler that fires once per click. It is the main way players interact with the restaurant—you are the chef generating food. Notice the fourth parameter is true, marking food as inKitchen so it won't be garbage-collected when eaten.

// Function to generate new food on touch/click (in kitchen)
function mousePressed() {
  foodItems.push(new Food(kitchenPos.x + random(kitchenPos.width), kitchenPos.y + random(kitchenPos.height), random(foodEmojis), true));
}
Line-by-line explanation (2 lines)
function mousePressed() {
p5.js automatically calls this function whenever the mouse is clicked anywhere on the canvas
foodItems.push(new Food(kitchenPos.x + random(kitchenPos.width), kitchenPos.y + random(kitchenPos.height), random(foodEmojis), true));
Creates a new Food object with: x position = kitchen's left edge plus a random offset within its width, y position = kitchen's top plus a random offset within its height, emoji = a random one from foodEmojis, and inKitchen = true (marking it as kitchen food)

touchStarted()

touchStarted() mirrors mousePressed() to support mobile and tablet users. Returning false prevents the browser from interpreting the touch as a scroll or zoom, keeping focus on your game.

// Allow touch input to also generate food (in kitchen)
function touchStarted() {
  foodItems.push(new Food(kitchenPos.x + random(kitchenPos.width), kitchenPos.y + random(kitchenPos.height), random(foodEmojis), true));
  return false; // Prevent default browser touch actions (like scrolling)
}
Line-by-line explanation (3 lines)
function touchStarted() {
p5.js automatically calls this function when the user touches the screen (on mobile devices)
foodItems.push(new Food(kitchenPos.x + random(kitchenPos.width), kitchenPos.y + random(kitchenPos.height), random(foodEmojis), true));
Identical to mousePressed()—creates a new Food object at a random position within the kitchen with a random emoji
return false;
Tells the browser NOT to perform its default touch actions (like scrolling or zooming)—essential for touch games to feel responsive

Customer (Class)

The Customer constructor runs once when you create a new Customer with new Customer(x, y). Every property defined with 'this.' becomes a variable that belongs to that specific customer and can be accessed later in other methods like update() and display().

class Customer {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.size = random(30, 45); // Random size for variety
    this.color = color(random(100, 200), random(100, 200), random(100, 200)); // Random skin tone-ish color
    this.speed = random(1.5, 2.5); // Movement speed
    this.state = 'ARRIVING'; // Current state: ARRIVING, SEEKING_TABLE, MOVING_TO_CHAIR, EATING, LEAVING
    this.targetChair = null; // Reference to the chair this customer is aiming for
    this.mouthOpen = false; // For eating animation
    this.eatingTimer = 0; // Timer to keep mouth open briefly
    this.eatingDuration = random(180, 360); // How long the customer eats (frames)
  }
Line-by-line explanation (10 lines)

🔧 Subcomponents:

function-call Constructor Initialization constructor(x, y) {

Initializes a new Customer with position, appearance, speed, and state

this.x = x;
Stores the customer's x coordinate (horizontal position on canvas)
this.y = y;
Stores the customer's y coordinate (vertical position on canvas)
this.size = random(30, 45);
Picks a random size between 30 and 45 pixels so each customer looks slightly different
this.color = color(random(100, 200), random(100, 200), random(100, 200));
Creates a random color with RGB values each between 100 and 200, giving varied but realistic skin tones
this.speed = random(1.5, 2.5);
Picks a random movement speed between 1.5 and 2.5 pixels per frame—customers move at slightly different paces
this.state = 'ARRIVING';
Sets the initial state to ARRIVING—the first state in the customer's journey through the restaurant
this.targetChair = null;
Initializes the target chair reference to null—it will be set once the customer finds an empty chair
this.mouthOpen = false;
Starts with the mouth closed; it will open periodically during eating to animate chewing
this.eatingTimer = 0;
Initializes the eating timer to 0; it will be set to 20 whenever the mouth should open
this.eatingDuration = random(180, 360);
Picks how long this customer will eat (180–360 frames, roughly 3–6 seconds at 60 fps)—makes eating times vary

Customer.display()

display() is called every frame by the draw() loop. It uses translate() to position everything relative to the customer (via push/pop), scales shapes by this.size so bigger customers look proportionally bigger, and animates the mouth by conditionally drawing an arc vs. a line based on the eating state.

🔬 These two lines draw the left and right eyes symmetrically. What happens if you change the first -this.size * 0.15 to -this.size * 0.3 (further left)? Try it—can you make the eyes cross-eyed or give each eye a different size?

    // Eyes
    fill(0);
    ellipse(-this.size * 0.15, -this.size * 0.6, this.size * 0.1);
    ellipse(this.size * 0.15, -this.size * 0.6, this.size * 0.1);
  display() {
    push();
    translate(this.x, this.y);

    // Body
    fill(this.color);
    noStroke();
    rect(-this.size / 4, -this.size / 2, this.size / 2, this.size); // Simple rectangular body

    // Head
    ellipse(0, -this.size / 2, this.size * 0.8);

    // Eyes
    fill(0);
    ellipse(-this.size * 0.15, -this.size * 0.6, this.size * 0.1);
    ellipse(this.size * 0.15, -this.size * 0.6, this.size * 0.1);

    // Mouth
    fill(255);
    stroke(0);
    if (this.mouthOpen) {
      arc(0, -this.size * 0.4, this.size * 0.3, this.size * 0.2, 0, PI); // Open mouth
      this.eatingTimer--;
      if (this.eatingTimer <= 0) {
        this.mouthOpen = false;
      }
    } else if (this.state === 'EATING') {
      // Keep mouth closed while eating, but not open unless eatingTimer active
      line(-this.size * 0.15, -this.size * 0.4, this.size * 0.15, -this.size * 0.4); // Closed mouth
    } else {
      line(-this.size * 0.15, -this.size * 0.4, this.size * 0.15, -this.size * 0.4); // Closed mouth
    }
    pop();
  }
Line-by-line explanation (19 lines)

🔧 Subcomponents:

function-call Push/Pop Transforms push(); translate(this.x, this.y); ... pop();

Saves the drawing state, translates to the customer's position so all shapes are drawn relative to them, then restores the state—prevents transforms from affecting other drawings

block Body and Head Drawing rect(-this.size / 4, -this.size / 2, this.size / 2, this.size); ellipse(0, -this.size / 2, this.size * 0.8);

Draws a simple rectangular body and circular head centered at the origin (0,0)

block Eyes Drawing ellipse(-this.size * 0.15, -this.size * 0.6, this.size * 0.1); ellipse(this.size * 0.15, -this.size * 0.6, this.size * 0.1);

Draws two black eyes positioned symmetrically on the head

conditional Mouth Animation Logic if (this.mouthOpen) { arc(0, -this.size * 0.4, this.size * 0.3, this.size * 0.2, 0, PI); this.eatingTimer--; if (this.eatingTimer <= 0) { this.mouthOpen = false; } }

If the mouth is open, draws an arc (open-mouth shape), decrements the eating timer, and closes the mouth when the timer runs out

push();
Saves the current drawing state (position, rotation, scale, etc.) so later changes don't affect other objects
translate(this.x, this.y);
Moves the origin (0,0) to the customer's x,y position—all shapes drawn after this are relative to the customer
fill(this.color);
Sets the fill color to this customer's random skin tone color
noStroke();
Removes the outline from shapes so they look solid
rect(-this.size / 4, -this.size / 2, this.size / 2, this.size);
Draws a rectangle for the body: starting at x = -size/4 (left of center), y = -size/2 (above center), width = size/2, height = size
ellipse(0, -this.size / 2, this.size * 0.8);
Draws a circle for the head at x=0 (center), y = -size/2 (above the body), with diameter = size * 0.8
fill(0);
Changes fill color to black (0, 0, 0) for the eyes
ellipse(-this.size * 0.15, -this.size * 0.6, this.size * 0.1);
Draws the left eye: positioned 15% of size to the left of center, 60% of size above center, with diameter 10% of size
ellipse(this.size * 0.15, -this.size * 0.6, this.size * 0.1);
Draws the right eye: positioned symmetrically on the right side
fill(255);
Changes fill color to white (255, 255, 255) for the inside of the mouth
stroke(0);
Adds a black outline to the mouth shape
if (this.mouthOpen) {
Checks if the mouth should be drawn open (during eating animation)
arc(0, -this.size * 0.4, this.size * 0.3, this.size * 0.2, 0, PI);
Draws an arc (semicircle) representing an open mouth, centered below the eyes
this.eatingTimer--;
Decrements the eating timer by 1 each frame—after 20 frames, the timer reaches 0
if (this.eatingTimer <= 0) {
Checks if the timer has expired
this.mouthOpen = false;
Closes the mouth when the timer finishes
} else if (this.state === 'EATING') {
If the mouth isn't open but the customer is eating, draw a closed mouth
line(-this.size * 0.15, -this.size * 0.4, this.size * 0.15, -this.size * 0.4);
Draws a straight line representing a closed mouth from left to right across the face
pop();
Restores the drawing state, undoing the translate() so other objects draw at their correct positions

Customer.update()

update() is the state machine engine. It runs every frame and uses a switch statement to decide what the customer does based on this.state. Each case transitions to the next state in a chain: ARRIVING → SEEKING_TABLE → MOVING_TO_CHAIR → EATING → LEAVING. The MOVING_TO_CHAIR case demonstrates pathfinding using atan2() and trigonometric functions to move smoothly toward a target.

🔬 This loop searches ALL tables and claims the FIRST empty chair it finds. What happens if you remove the break statement? What happens if you remove foundChair = true? Try removing both and watch what happens—does the customer claim multiple chairs?

      case 'SEEKING_TABLE':
        let foundChair = false;
        for (let table of tables) {
          let chairIndex = table.findEmptyChair();
          if (chairIndex !== -1) {
            this.targetChair = table.chairs[chairIndex];
            this.targetChair.customer = this; // Link chair to customer
            this.state = 'MOVING_TO_CHAIR';
            foundChair = true;
            break;
  update() {
    switch (this.state) {
      case 'ARRIVING':
        this.x += this.speed;
        if (this.x >= 200) { // Entrance area
          this.state = 'SEEKING_TABLE';
        }
        break;

      case 'SEEKING_TABLE':
        let foundChair = false;
        for (let table of tables) {
          let chairIndex = table.findEmptyChair();
          if (chairIndex !== -1) {
            this.targetChair = table.chairs[chairIndex];
            this.targetChair.customer = this; // Link chair to customer
            this.state = 'MOVING_TO_CHAIR';
            foundChair = true;
            break;
          }
        }
        // If no chair found, wander slightly
        if (!foundChair) {
          this.x += random(-1, 1) * this.speed * 0.2;
          this.y += random(-1, 1) * this.speed * 0.2;
          this.x = constrain(this.x, 0, width);
          this.y = constrain(this.y, 0, height);
        }
        break;

      case 'MOVING_TO_CHAIR':
        if (this.targetChair) {
          let dx = this.targetChair.x - this.x;
          let dy = this.targetChair.y - this.y;
          let angle = atan2(dy, dx);

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

          // Check if customer has reached the chair
          if (dist(this.x, this.y, this.targetChair.x, this.targetChair.y) < 5) {
            this.x = this.targetChair.x; // Snap to chair
            this.y = this.targetChair.y;
            this.state = 'EATING';
            this.targetChair.occupied = true; // Mark chair as occupied
            this.eat(); // Start eating process
          }
        }
        break;

      case 'EATING':
        this.eatingDuration--;
        // Periodically open mouth while eating
        if (this.eatingDuration % 60 === 0) {
          this.mouthOpen = true;
          this.eatingTimer = 20; // Keep mouth open for 20 frames
        }
        if (this.eatingDuration <= 0) {
          this.state = 'LEAVING';
          this.targetChair.occupied = false; // Free up the chair
          this.targetChair.customer = null; // Unlink customer from chair
          if (this.targetChair.food) {
            this.targetChair.food.setEaten(); // Mark food as eaten
            this.targetChair.food = null; // Remove food from table
          }
        }
        break;

      case 'LEAVING':
        this.x += this.speed; // Move off-screen to the right
        break;
    }
  }
Line-by-line explanation (47 lines)

🔧 Subcomponents:

switch-case ARRIVING State case 'ARRIVING': this.x += this.speed; if (this.x >= 200) { this.state = 'SEEKING_TABLE'; } break;

Moves the customer right until they reach x=200, then transitions to seeking a table

switch-case SEEKING_TABLE State case 'SEEKING_TABLE': let foundChair = false; for (let table of tables) { let chairIndex = table.findEmptyChair(); if (chairIndex !== -1) { this.targetChair = table.chairs[chairIndex]; this.targetChair.customer = this; this.state = 'MOVING_TO_CHAIR'; foundChair = true; break; } }

Scans all tables for an empty chair; if found, claims it and transitions to moving

block Wandering Fallback if (!foundChair) { this.x += random(-1, 1) * this.speed * 0.2; this.y += random(-1, 1) * this.speed * 0.2; this.x = constrain(this.x, 0, width); this.y = constrain(this.y, 0, height); }

If no chair found, the customer wanders slightly while waiting, staying within canvas bounds

switch-case MOVING_TO_CHAIR State case 'MOVING_TO_CHAIR': if (this.targetChair) { let dx = this.targetChair.x - this.x; let dy = this.targetChair.y - this.y; let angle = atan2(dy, dx); this.x += cos(angle) * this.speed; this.y += sin(angle) * this.speed;

Calculates the angle to the target chair using atan2 and moves toward it using cos and sin

conditional Arrival and Snap to Chair if (dist(this.x, this.y, this.targetChair.x, this.targetChair.y) < 5) { this.x = this.targetChair.x; this.y = this.targetChair.y; this.state = 'EATING'; this.targetChair.occupied = true; this.eat(); }

Checks if the customer is close enough to the chair (within 5 pixels), snaps them to it, marks it occupied, and starts eating

switch-case EATING State case 'EATING': this.eatingDuration--; if (this.eatingDuration % 60 === 0) { this.mouthOpen = true; this.eatingTimer = 20; } if (this.eatingDuration <= 0) { this.state = 'LEAVING'; this.targetChair.occupied = false; this.targetChair.customer = null; if (this.targetChair.food) { this.targetChair.food.setEaten(); this.targetChair.food = null; } } break;

Counts down eating time, opens mouth every 60 frames, and transitions to leaving once eating time expires

switch-case LEAVING State case 'LEAVING': this.x += this.speed; break;

Moves the customer right off the screen

switch (this.state) {
Starts a switch statement that checks the current state and runs different code for each one—the heart of the state machine
case 'ARRIVING':
Handles the ARRIVING state: customer is walking in from the left
this.x += this.speed;
Moves the customer right by their speed each frame
if (this.x >= 200) {
Once the customer reaches x=200 (past the entrance), they've entered the restaurant
this.state = 'SEEKING_TABLE';
Transitions to the next state: looking for an empty chair
case 'SEEKING_TABLE':
Handles the SEEKING_TABLE state: customer is looking for an empty chair
let foundChair = false;
Flag that tracks whether we found an empty chair—starts false
for (let table of tables) {
Loops through all tables to search for an empty chair
let chairIndex = table.findEmptyChair();
Calls the table's findEmptyChair() method to get the index of an empty chair (or -1 if none)
if (chairIndex !== -1) {
If findEmptyChair() returned a valid index (not -1), there is an empty chair
this.targetChair = table.chairs[chairIndex];
Stores a reference to the chair object so the customer can move toward it
this.targetChair.customer = this;
Links the chair back to the customer—now the chair knows which customer is coming to sit
this.state = 'MOVING_TO_CHAIR';
Transitions to the next state: walking toward the claimed chair
foundChair = true;
Sets the flag to true to indicate success
break;
Exits the for loop early since we found a chair and don't need to keep searching
if (!foundChair) {
If no chair was found, the customer must wait and wander
this.x += random(-1, 1) * this.speed * 0.2;
Adds a small random movement left or right (scaled to 20% of speed) so the customer looks like they're wandering nervously
this.y += random(-1, 1) * this.speed * 0.2;
Also wanders slightly up and down
this.x = constrain(this.x, 0, width);
Clamps x between 0 and width so the wandering customer doesn't leave the screen
this.y = constrain(this.y, 0, height);
Clamps y between 0 and height for the same reason
case 'MOVING_TO_CHAIR':
Handles the MOVING_TO_CHAIR state: customer walks toward their target chair
if (this.targetChair) {
Safety check: only move if we actually have a target chair
let dx = this.targetChair.x - this.x;
Calculates the horizontal distance from customer to target chair
let dy = this.targetChair.y - this.y;
Calculates the vertical distance from customer to target chair
let angle = atan2(dy, dx);
Converts the dx and dy distances to an angle (in radians) pointing from the customer toward the chair
this.x += cos(angle) * this.speed;
Moves the customer toward the target by moving along the angle using cos() to extract the x-component
this.y += sin(angle) * this.speed;
Moves the customer by using sin() to extract the y-component of motion along the angle
if (dist(this.x, this.y, this.targetChair.x, this.targetChair.y) < 5) {
Checks if the distance between customer and chair is less than 5 pixels—close enough to sit
this.x = this.targetChair.x;
Snaps the customer's x position exactly to the chair's x
this.y = this.targetChair.y;
Snaps the customer's y position exactly to the chair's y (now perfectly seated)
this.state = 'EATING';
Transitions to the eating state
this.targetChair.occupied = true;
Marks the chair as occupied so no other customer will try to sit there
this.eat();
Calls the eat() method to generate food at the table
case 'EATING':
Handles the EATING state: customer sits and eats
this.eatingDuration--;
Decrements the eating countdown by 1 frame each call
if (this.eatingDuration % 60 === 0) {
Every 60 frames (roughly every second), the modulo operator % returns 0
this.mouthOpen = true;
Opens the mouth to animate eating
this.eatingTimer = 20;
Sets the mouth timer to 20 frames so it stays open for a brief animation
if (this.eatingDuration <= 0) {
Once eating time runs out, transition to leaving
this.state = 'LEAVING';
Changes state to LEAVING
this.targetChair.occupied = false;
Frees up the chair for the next customer
this.targetChair.customer = null;
Unlinks the customer from the chair
if (this.targetChair.food) {
Checks if there is food on the table
this.targetChair.food.setEaten();
Marks the food as eaten (but kitchen food may persist for reuse)
this.targetChair.food = null;
Removes the food reference from the chair
case 'LEAVING':
Handles the LEAVING state: customer walks off to the right
this.x += this.speed;
Moves the customer right off the canvas

Customer.eat()

eat() is called once when the customer reaches their chair. It creates the food emoji at the table and opens the mouth. Notice the y position is offset by this.size * 0.6 to place food above the customer's head rather than at the chair center.

  eat() {
    // Generate food directly at the customer's chair
    let food = new Food(this.targetChair.x, this.targetChair.y + this.size * 0.6, random(foodEmojis), false);
    foodItems.push(food);
    this.targetChair.food = food; // Link food to chair
    this.mouthOpen = true;
    this.eatingTimer = 20;
  }
Line-by-line explanation (5 lines)
let food = new Food(this.targetChair.x, this.targetChair.y + this.size * 0.6, random(foodEmojis), false);
Creates a new Food object: x = chair's x position (above the customer's head), y = chair's y plus a small offset, emoji = random from foodEmojis array, inKitchen = false (this food is on a table, not in the kitchen)
foodItems.push(food);
Adds the food to the global foodItems array so draw() will display it
this.targetChair.food = food;
Links the food object to the chair so we can track which food belongs to which table
this.mouthOpen = true;
Opens the customer's mouth as they start eating
this.eatingTimer = 20;
Sets the eating timer to 20 frames to animate the open mouth briefly

Food (Class)

The Food class is simple: it stores position, emoji, and state. The eaten flag is crucial—it prevents the food from being drawn and allows draw() to garbage-collect it. The inKitchen flag prevents kitchen food from being deleted prematurely.

class Food {
  constructor(x, y, emoji, inKitchen = false) {
    this.x = x;
    this.y = y;
    this.emoji = emoji;
    this.size = random(20, 36); // Random size for variety
    this.eaten = false;
    this.inKitchen = inKitchen; // Flag to indicate if food is in the kitchen
  }

  display() {
    if (!this.eaten) {
      textSize(this.size);
      textAlign(CENTER, CENTER);
      text(this.emoji, this.x, this.y);
    }
  }

  isEaten() {
    return this.eaten;
  }

  setEaten() {
    this.eaten = true;
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

function-call Food Constructor constructor(x, y, emoji, inKitchen = false) {

Initializes a Food object with position, emoji, size, and flags

conditional Food Display if (!this.eaten) { textSize(this.size); textAlign(CENTER, CENTER); text(this.emoji, this.x, this.y); }

Draws the food emoji only if it hasn't been eaten yet

this.x = x;
Stores the food's x position
this.y = y;
Stores the food's y position
this.emoji = emoji;
Stores the emoji string (e.g., '🍕' or '🍰')
this.size = random(20, 36);
Picks a random text size between 20 and 36 pixels so each food item looks slightly different
this.eaten = false;
Initializes eaten to false—food starts uneaten
this.inKitchen = inKitchen;
Stores the inKitchen flag (defaults to false); kitchen food is treated differently when deleted
if (!this.eaten) {
Only draw the food if it hasn't been eaten yet (prevents invisible food from lingering)
textSize(this.size);
Sets the text size to this food's random size
textAlign(CENTER, CENTER);
Centers the text both horizontally and vertically so the emoji is drawn at (x, y) exactly
text(this.emoji, this.x, this.y);
Draws the emoji string at position (x, y)
return this.eaten;
Returns true if the food has been eaten, false otherwise
this.eaten = true;
Marks the food as eaten (called when a customer finishes)

Table (Class)

The Table class manages seating. Each table has an array of chair objects (not full objects, just plain objects with properties). The findEmptyChair() method is crucial—it returns the index of the first available seat, or -1 if full. Notice how chairs store references to customers and food, creating a web of connections that tracks the entire state of the restaurant.

🔬 This loop places the first chair on the left (i === 0) and all others on the right. What happens if you change it to place chairs in a circle around the table instead? Try changing chairX to use Math.cos and Math.sin with an angle based on i—can you arrange four chairs around a table?

    // Create chairs around the table
    for (let i = 0; i < numChairs; i++) {
      let chairX = this.x + (i === 0 ? -this.width / 2 - 20 : this.width / 2 + 20);
      let chairY = this.y;
      this.chairs.push({ x: chairX, y: chairY, occupied: false, customer: null, food: null });
    }
class Table {
  constructor(x, y, numChairs) {
    this.x = x;
    this.y = y;
    this.width = 120;
    this.height = 80;
    this.chairs = [];

    // Create chairs around the table
    for (let i = 0; i < numChairs; i++) {
      let chairX = this.x + (i === 0 ? -this.width / 2 - 20 : this.width / 2 + 20);
      let chairY = this.y;
      this.chairs.push({ x: chairX, y: chairY, occupied: false, customer: null, food: null });
    }
  }

  display() {
    // Table
    fill(139, 69, 19); // Brown table
    stroke(0);
    rect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height, 5);

    // Chairs
    for (let chair of this.chairs) {
      fill(100); // Gray chair
      noStroke();
      ellipse(chair.x, chair.y, 30, 30);
      if (chair.occupied) {
        fill(255, 0, 0, 100); // Red overlay for occupied chairs
        ellipse(chair.x, chair.y, 30, 30);
      }
    }
  }

  findEmptyChair() {
    for (let i = 0; i < this.chairs.length; i++) {
      if (!this.chairs[i].occupied) {
        return i; // Return the index of the empty chair
      }
    }
    return -1; // No empty chair found
  }
}
Line-by-line explanation (22 lines)

🔧 Subcomponents:

function-call Table Constructor constructor(x, y, numChairs) {

Creates a table at (x, y) with a specified number of chairs positioned to the left and right

for-loop Chair Creation Loop for (let i = 0; i < numChairs; i++) { let chairX = this.x + (i === 0 ? -this.width / 2 - 20 : this.width / 2 + 20); let chairY = this.y; this.chairs.push({ x: chairX, y: chairY, occupied: false, customer: null, food: null }); }

Creates numChairs chair objects: the first chair is on the left, the second on the right

block Table Drawing fill(139, 69, 19); stroke(0); rect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height, 5);

Draws the table as a brown rectangle with rounded corners

for-loop Chairs Drawing Loop for (let chair of this.chairs) { fill(100); noStroke(); ellipse(chair.x, chair.y, 30, 30); if (chair.occupied) { fill(255, 0, 0, 100); ellipse(chair.x, chair.y, 30, 30); } }

Draws each chair as a gray circle, with a red overlay if occupied

this.x = x;
Stores the table's center x position
this.y = y;
Stores the table's center y position
this.width = 120;
Sets the table width to 120 pixels (fixed for all tables)
this.height = 80;
Sets the table height to 80 pixels (fixed for all tables)
this.chairs = [];
Initializes an empty array to store chair objects
for (let i = 0; i < numChairs; i++) {
Loops once for each chair the table should have
let chairX = this.x + (i === 0 ? -this.width / 2 - 20 : this.width / 2 + 20);
Conditional expression: if i === 0 (first chair), place it on the left (x - width/2 - 20); otherwise place it on the right (x + width/2 + 20)
let chairY = this.y;
All chairs are at the same y position as the table center
this.chairs.push({ x: chairX, y: chairY, occupied: false, customer: null, food: null });
Creates a chair object with position, occupied flag (false initially), customer reference (null), and food reference (null), then adds it to the chairs array
fill(139, 69, 19);
Sets the fill color to brown (RGB: 139, 69, 19) for the table
stroke(0);
Sets the stroke color to black for the table outline
rect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height, 5);
Draws a rectangle centered at (this.x, this.y) with width and height, and 5-pixel rounded corners (the fifth parameter)
fill(100);
Sets fill color to gray (100, 100, 100) for unoccupied chairs
noStroke();
Removes the outline from chairs
ellipse(chair.x, chair.y, 30, 30);
Draws a gray circle 30 pixels in diameter at the chair's position
if (chair.occupied) {
If the chair is occupied, draw a red overlay
fill(255, 0, 0, 100);
Sets fill color to red with transparency: RGB(255, 0, 0) with alpha = 100 (semitransparent)
ellipse(chair.x, chair.y, 30, 30);
Draws another circle on top to create the occupied visual effect
for (let i = 0; i < this.chairs.length; i++) {
Loops through all chairs in this table
if (!this.chairs[i].occupied) {
If the chair is not occupied, it's empty
return i;
Returns the index of the empty chair immediately (first empty chair found)
return -1;
If all chairs are occupied, returns -1 to signal 'no empty chair found'

📦 Key Variables

customers array

Stores all Customer objects currently in the restaurant; allows draw() to update and display them

let customers = [];
foodItems array

Stores all Food objects (emojis) currently on display; allows draw() to render them and remove eaten food

let foodItems = [];
tables array

Stores all Table objects; allows draw() to display tables and customers to find chairs

let tables = [];
foodEmojis array

Array of food emoji strings that are randomly selected when food is created; includes an airplane cake

let foodEmojis = ["🥂", "🥤", "🍼", ...];
numInitialCustomers number

How many customers spawn at the very start (before the restaurant opens)

const numInitialCustomers = 6;
maxCustomers number

The maximum number of customers allowed on screen at once; limits crowding

const maxCustomers = 10;
customerArrivalInterval number

How many frames must pass between new customer arrivals; higher = slower arrival rate

const customerArrivalInterval = 180;
lastCustomerArrival number

Stores the frameCount value when the last customer arrived; used to measure time between arrivals

let lastCustomerArrival = 0;
kitchenPos object

Stores the kitchen area's position and size (x, y, width, height); used for drawing and spawning food on click

const kitchenPos = { x: 100, y: 150, width: 150, height: 100 };

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG Customer.update() SEEKING_TABLE state

If all tables are permanently full, customers wander forever and never leave. They consume memory indefinitely.

💡 Add a max wander timer; if a customer hasn't found a seat after 600 frames (10 seconds), move them to LEAVING state so they eventually exit.

PERFORMANCE draw() function

The customer loop and food loop iterate backwards, which is safe but less intuitive. More importantly, there's no limit to how long the customers array can grow if customers are added faster than they leave.

💡 Consider adding a hard cap or pool recycling for customers to prevent memory issues in long-running sketches.

STYLE Customer class constructor

Properties like this.size and this.speed are randomized, but there's no upper bound check. A customer could be extremely tall or fast by chance.

💡 Consider using constrain() to ensure speeds and sizes stay in a reasonable range, or document the ranges clearly.

FEATURE Table class

Tables always have exactly 2 chairs, even though the constructor accepts a numChairs parameter that is used but then ignored during chair positioning (all non-first chairs go to the right).

💡 Enhance the chair positioning logic to arrange multiple chairs in a circle or semicircle around the table for better realism and scalability.

BUG Food class and draw() loop

Kitchen-created food (inKitchen = true) is never removed from the foodItems array, it just gets eaten and hidden. Over thousands of clicks, kitchen food accumulates invisibly.

💡 Either modify the condition in draw() to also remove kitchen food after eating, or implement a separate kitchen inventory system with a max limit.

🔄 Code Flow

Code flow showing setup, draw, windowresized, mousepressed, touchstarted, customer, customerdisplay, customerupdate, customereat, food, table

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

graph TD start[Start] --> setup[setup] click setup href "#fn-setup" setup --> canvas-creation[Canvas Creation] click canvas-creation href "#sub-canvas-creation" setup --> table-creation[Table Layout] click table-creation href "#sub-table-creation" setup --> initial-customers[Initial Customer Spawn] click initial-customers href "#sub-initial-customers" setup --> draw[draw loop] click draw href "#fn-draw" draw --> background-clear[Background Clear] click background-clear href "#sub-background-clear" draw --> kitchen-drawing[Kitchen Drawing] click kitchen-drawing href "#sub-kitchen-drawing" draw --> table-display-loop[Table Display Loop] click table-display-loop href "#sub-table-display-loop" draw --> customer-update-loop[Customer Update Loop] click customer-update-loop href "#sub-customer-update-loop" draw --> new-customer-arrival[New Customer Arrival] click new-customer-arrival href "#sub-new-customer-arrival" draw --> food-display-loop[Food Display Loop] click food-display-loop href "#sub-food-display-loop" draw --> windowresized[windowResized] click windowresized href "#fn-windowresized" windowresized --> resize-canvas[Canvas Resize] click resize-canvas href "#sub-resize-canvas" windowresized --> table-repositioning[Table Repositioning] click table-repositioning href "#sub-table-repositioning" customer-update-loop --> customerupdate[customerupdate] click customerupdate href "#fn-customerupdate" customerupdate --> arriving-state[ARRIVING State] click arriving-state href "#sub-arriving-state" customerupdate --> seeking-state[SEEKING_TABLE State] click seeking-state href "#sub-seeking-state" customerupdate --> moving-state[MOVING_TO_CHAIR State] click moving-state href "#sub-moving-state" customerupdate --> eating-state[EATING State] click eating-state href "#sub-eating-state" customerupdate --> leaving-state[LEAVING State] click leaving-state href "#sub-leaving-state" customerupdate --> wander-fallback[Wandering Fallback] click wander-fallback href "#sub-wander-fallback" table-display-loop --> table[table] click table href "#fn-table" table --> table-constructor[Table Constructor] click table-constructor href "#sub-table-constructor" table --> chair-creation[Chair Creation Loop] click chair-creation href "#sub-chair-creation" chair-creation --> chairs-drawing[Chairs Drawing Loop] click chairs-drawing href "#sub-chairs-drawing" table --> table-drawing[Table Drawing] click table-drawing href "#sub-table-drawing" food-display-loop --> food[food] click food href "#fn-food" food --> food-constructor[Food Constructor] click food-constructor href "#sub-food-constructor" food --> food-display[Food Display] click food-display href "#sub-food-display" customerupdate --> constructor-init[Constructor Initialization] click constructor-init href "#sub-constructor-init" customerupdate --> push-pop[Push/Pop Transforms] click push-pop href "#sub-push-pop" customerupdate --> body-drawing[Body and Head Drawing] click body-drawing href "#sub-body-drawing" customerupdate --> eyes-drawing[Eyes Drawing] click eyes-drawing href "#sub-eyes-drawing" customerupdate --> mouth-animation[Mouth Animation Logic] click mouth-animation href "#sub-mouth-animation"

❓ Frequently Asked Questions

What visual elements are created in the p5.js sketch?

The sketch visually represents a restaurant scene featuring a kitchen, multiple tables, and animated customers using emojis to depict food items.

How can users interact with the creative coding sketch?

Currently, the sketch does not support user interaction; it primarily displays a simulation of customers arriving and interacting within a restaurant setting.

What creative coding concepts are demonstrated in this sketch?

This sketch showcases object-oriented programming through the use of Customer and Table classes, as well as dynamic array manipulation to manage customer behavior.

Preview

Sketch 2026-02-22 22:27 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2026-02-22 22:27 - Code flow showing setup, draw, windowresized, mousepressed, touchstarted, customer, customerdisplay, customerupdate, customereat, food, table
Code Flow Diagram