Simulating Natural Systems in Creative Code: Butterflies;

P5.JS

Design Brief

Create a sketch that would simulate an existing natural system - look at physics, biology, and other natural sciences for good examples. Start with the environment - where is the system situated? What are the forces the environment might exert on the system? Examine the agents in the system, their relationships to each other and the environment they are in. Then look at how this system would develop over time. What are the rules that you are going to come up with, what are the parameters you are going to feed into them and what effect will the changes have on the development of the system.

  • create classes of entities to represent the components of the system

  • use vectors to represent forces existing in the system

  • use randomness or noise to generate at least one

  • add direct or indirect mouse or keyboard controls

Software:

The Concept

This simulation represents a simple ecosystem:

  • Flowers provide nourishment for butterflies.

  • Butterflies emerge from chrysalises, flutter around, and feed on flowers. Once they gather enough energy, they lay another chrysalis, extending their lifecycle.

  • Chrysalises transform into new butterflies after a set period.

  • Birds act as predators, hunting butterflies and creating a challenge to the system. Additional birds appear only after specific conditions are met—such as a butterfly population threshold—adding another element of complexity.

The system simulation starts with a butterfly chrysalis that after a certain amount of time becomes a butterfly, the butterfly is able to fly around and eat from flowers in the world and after eating a certain amount they are able to lay another chrysalis. There is a predator in the form of a bird that would fly at different altitudes, and if the butterfly or chrysalis are in their path they would get eaten, essentially being erased by their contact with the bird. If all butterflies or chrysalis in the system are eliminated, the system will re-introduce a chrysalis after a certain amount of time has passed, continuing the system.

The Process

The Core Mechanics:

The simulation revolves around key classes: Flower, ButterflyChrysalis, and Bird. Each has unique behaviors and interactions that drive the system:

  1. Flowers regenerate after being eaten by butterflies.

  2. Butterflies gain energy by feeding and can "lay eggs" (chrysalises) to propagate their species. They can also be eaten by birds.

  3. Chrysalises represent the metamorphosis stage, transforming into butterflies after a timed delay. They can also be eaten by birds.

  4. Birds hunt butterflies but appear only after specific conditions are met, such as the passage of time or the number of butterflies alive in the system.

Skeleton Code - 1st Attemp:

let chrysalises = [];
let butterflies = [];
let flowers = [];
let bird;

function setup() {
  createCanvas(800, 600);
  // Start with one chrysalis
  chrysalises.push(new Chrysalis(width / 2, height / 2));
  // Create some flowers for the butterflies to eat from
  for (let i = 0; i < 5; i++) {
    flowers.push(new Flower(random(width), random(height)));
  }
  // Create the bird predator
  bird = new Bird();
}

function draw() {
  background(135, 206, 235);  // Sky blue
  
  // Display and update all flowers
  for (let flower of flowers) {
    flower.show();
  }
  
  // Update and display all chrysalises
  for (let i = chrysalises.length - 1; i >= 0; i--) {
    let chrysalis = chrysalises[i];
    chrysalis.update();
    chrysalis.show();
    
    if (chrysalis.isButterfly()) {
      butterflies.push(new Butterfly(chrysalis.x, chrysalis.y));
      chrysalises.splice(i, 1);  // Remove chrysalis after it becomes a butterfly
    }
  }
  
  // Update and display all butterflies
  for (let i = butterflies.length - 1; i >= 0; i--) {
    let butterfly = butterflies[i];
    butterfly.update();
    butterfly.show();
    butterfly.eat(flowers);
    
    if (butterfly.eatenBy(bird)) {
      butterflies.splice(i, 1);  // Remove butterfly if eaten by bird
    }
  }
  
  // Update and display the bird (predator)
  bird.update();
  bird.show();
}

class Chrysalis {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.timeToTransform = 300;  // Frames until transformation
  }
  
  update() {
    this.timeToTransform--;
  }
  
  show() {
    fill(150, 100, 50);
    ellipse(this.x, this.y, 20, 40);  // Simple chrysalis shape
  }
  
  isButterfly() {
    return this.timeToTransform <= 0;
  }
}

class Butterfly {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.energy = 0;  // Energy level increases when eating
  }
  
  update() {
    // Simple random flying movement
    this.x += random(-2, 2);
    this.y += random(-2, 2);
  }
  
  show() {
    fill(255, 200, 0);
    ellipse(this.x, this.y, 20, 10);  // Simple butterfly shape
  }
  
  eat(flowers) {
    for (let flower of flowers) {
      let d = dist(this.x, this.y, flower.x, flower.y);
      if (d < 20) {
        this.energy++;
        flower.replenish();  // Replenish the flower when eaten
        if (this.energy >= 5) {
          chrysalises.push(new Chrysalis(this.x, this.y));  // Lay a new chrysalis
          this.energy = 0;
        }
      }
    }
  }
  
  eatenBy(bird) {
    let d = dist(this.x, this.y, bird.x, bird.y);
    return d < 30;  // If butterfly is close enough to the bird, it gets eaten
  }
}

class Flower {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  
  show() {
    fill(255, 0, 0);
    ellipse(this.x, this.y, 10, 10);  // Simple flower shape
  }
  
  replenish() {
    // Optionally add code to regenerate flower after being eaten
  }
}

class Bird {
  constructor() {
    this.x = random(width);
    this.y = random(height);
    this.speed = random(1, 3);
  }
  
  update() {
    this.x += this.speed;
    if (this.x > width) {
      this.x = 0;
      this.y = random(height);  // Bird changes altitude randomly
    }
  }
  
  show() {
    fill(50);
    ellipse(this.x, this.y, 30, 15);  // Simple bird shape
  }
}

Update 1:

I noticed that after 4 butterflies are born, the odds of the bird eating all of the butterflies get slimmer as more of them come. In order to introduce population control, I added a second bird that appears in the world after 4 butterflies are born  and remain alive. This way the bird will only come when 4 butterflies are alive  in the sketch, rather than after 4 chrysalis turn into butterflies.

Here’s what I did:

  1. Add a aliveButterflyCount variable: This variable will keep track of how many butterflies are alive in the sketch.

  2. Create a second bird: Add a condition that spawns the second bird when aliveButterflyCount reaches 4 live butterflies

  3. Handle both birds: Ensure both birds update and check for collisions with butterflies.

Key Changes:

  1. aliveButterflyCount: This variable tracks how many butterflies are currently alive (not eaten by birds). It increments when a butterfly is born and decrements when a butterfly is eaten.

  2. Bird array: Birds are stored in an array (birds) to handle multiple birds easily. Each bird is updated and checked for collisions with butterflies.

  3. Second bird appearance: A second bird will appear only when there are at least 4 butterflies alive in the simulation. Butterflies that have been eaten will not count towards this number.

With these changes, a second bird will only be introduced once there are 4 butterflies flying around the sketch at the same time.

Update 2:

At first, I tried to reset the sketch after all butterflies and chrysalises were eaten, but this made the entire world change and essentially created a new world. Instead, I just wanted a new chrysalis to appear to restart the cycle. So to make a new chrysalis appear instead of resetting the entire sketch after all the butterflies and chrysalises are eaten, I modified the logic. Instead of calling resetSimulation(), I can simply push a new chrysalis into the chrysalises array when both arrays (butterflies and chrysalises) are empty.

Here’s the logic was updated  in the draw() function:

  1. Check if all butterflies and chrysalises are eaten: If both arrays are empty, push a new chrysalis into the chrysalises array.

  2. Remove the resetSimulation() function: You no longer need it since we’re not resetting the whole simulation.

Key Changes:

  • Instead of resetting the entire simulation when no butterflies or chrysalises are left, a new chrysalis is spawned at a random location.

  • The check happens within the main loop (draw()), and if both the butterflies and chrysalises arrays are empty, a new chrysalis is added.

This keeps the simulation going continuously, as a new chrysalis will always appear after all existing butterflies and chrysalises are gone.

Update 3:

After testing the simulation a few times, I decided I wanted the second bird to be removed from the sketch if the count of live butterflies dropped below 4, to give the remaining butterflies a higher chance of survival. 

Here’s what the updated logic looks like:

  • We check if the number of live butterflies drops below 4, and if the second bird exists, we remove it.

Key Changes:

  1. Dynamic bird management: The second bird is added when the number of currently alive butterflies is 4 or more and removed if the count goes below 4.

  2. birds.pop(): This removes the second bird from the array when the live butterfly count drops below 4.

This ensures the system responds dynamically to the number of living butterflies!

Update 4:

I wanted to add a delay for the first bird's appearance after the first butterfly is born, using  a timer that starts when the first butterfly is created. Here's how it was implement in the code:

  1. Introduce a new variable to keep track of the time since the first butterfly is born.

  2. Modify the draw function to check if the first butterfly has been created and increment the timer.

  3. Only add the first bird once the timer exceeds a specified delay.

Key Changes:

  • Timer Initialization: firstButterflyBornTime is set to 0 initially and updated when the first butterfly is born.

  • Delay Logic: A birdDelay variable specifies the number of frames to wait before the first bird appears.

  • Bird Addition Condition: The first bird is added only when the delay time has passed after the first butterfly is born.

The birdDelay value can be adjusted to suit the timing desired.

Update 5:

I wanted to edit the code so that: 

  • The  color mode was set to HSB 

  • Each new butterfly born is a different color 

  • There is a delay for a chrysalis to appear after all butterflies and chrysalis have been eaten

Key Changes:

  • Color Mode: The color mode is set to HSB in the setup function using 0colorMode(HSB).

  • Random Colors for Butterflies: Each butterfly is assigned a random color (hue) upon birth by passing a random value to the Butterfly constructor.

  • Chrysalis Delay: After all butterflies and chrysalises are eaten, a delay (controlled by chrysalisDelay) is applied before a new chrysalis appears.

The chrysalisDelay variable can be adjusted to set the timing that feels right for your simulation.

Update 6:

I wanted the butterflies to resemble a butterfly, rather than being a single ellipse. To change the appearance of the butterflies so that they resemble an "X" shape made with two ellipses, I modified the show() method in the Butterfly class. Instead of drawing a single ellipse, it will draw two overlapping ellipses rotated to form an "X."

Key Changes:

  1. Shape of the Butterfly: The show() method now draws two ellipses at 45-degree angles to form an "X". The translate() function moves the origin to the butterfly's position, and the rotate() function adjusts the orientation of each ellipse.

  2. Size Parameter: Added a size property to control the dimensions of the ellipses easily. You can adjust the value of this.size to change the overall size of the butterfly.

The Final Result

The result is a beautifully dynamic system where butterflies flit between flowers, lay chrysalises, and attempt to survive the looming threat of birds. Timing controls, such as the delayed appearance of the second bird, ensure that the simulation remains engaging and avoids overcrowding.

This project deepened my appreciation for timing and interaction in generative art. By simulating dependencies, life cycles, and delays, I was able to recreate some of the complexities of natural ecosystems while keeping the system intuitive and visually appealing.

Key takeaways include:

  • Modular design: Building independent, self-contained classes (e.g., ButterflyBird) made it easier to adjust behaviors without disrupting the entire simulation.

  • Timing in simulations: Using frame-based delays brought realism to the system, reflecting how events in nature don't occur all at once.

  • Emergent behaviors: The interplay between different agents (e.g., butterflies and birds) creates a system that evolves and feels alive.

FINAL CODE

let chrysalises = [];
let butterflies = [];
let flowers = [];
let birds = [];  // Array for birds
let aliveButterflyCount = 0;  // Track how many butterflies are alive
let firstButterflyBornTime = 0; // Time the first butterfly is born
let lastButterflyTime = 0; // Time the last butterfly is eaten
const birdDelay = 60; // Delay in frames for the first bird
const chrysalisDelay = 120; // Delay in frames for the new chrysalis

function setup() {
  createCanvas(600, 400);
  colorMode(HSB, TWO_PI, 1, 1); // Set color mode to HSB with specified ranges
  noStroke();
  
  // Start with one chrysalis
  chrysalises.push(new Chrysalis(width / 2, height / 2));
  
  // Create more flowers
  for (let i = 0; i < 20; i++) {
    flowers.push(new Flower(random(width), random(height)));
  }
}

function draw() {
  background(3.5, 0.37, 1);  // Sky blue in HSB
  
  // Display and update all flowers
  for (let flower of flowers) {
    flower.update(); // Update the regrowth timer
    flower.show();
  }
  
  // Update and display all chrysalises
  for (let i = chrysalises.length - 1; i >= 0; i--) {
    let chrysalis = chrysalises[i];
    chrysalis.update();
    chrysalis.show();
    
    if (chrysalis.isButterfly()) {
      let butterflyColor = color(random(TWO_PI), 0.5, 1); // HSB color for butterfly
      butterflies.push(new Butterfly(chrysalis.x, chrysalis.y, butterflyColor));
      chrysalises.splice(i, 1);  // Remove chrysalis after it becomes a butterfly
      aliveButterflyCount++;  // Increment the count of currently alive butterflies
      // Start the timer when the first butterfly is born
      if (firstButterflyBornTime === 0) {
        firstButterflyBornTime = frameCount; // Record the frame count
      }
    }
  }
  
  // Update and display all butterflies
  for (let i = butterflies.length - 1; i >= 0; i--) {
    let butterfly = butterflies[i];
    butterfly.update();
    butterfly.show();
    butterfly.eat(flowers);
    
    // Check if butterfly is eaten by any bird
    for (let bird of birds) {
      if (butterfly.eatenBy(bird)) {
        butterflies.splice(i, 1);  // Remove butterfly if eaten by bird
        aliveButterflyCount--;  // Decrement the count of currently alive butterflies
        break;
      }
    }
  }
  
  // Update and display all birds (predators)
  for (let bird of birds) {
    bird.update();
    bird.show();
  }
  
  // Check if the first bird should be added
  if (firstButterflyBornTime > 0 && frameCount - firstButterflyBornTime >= birdDelay && birds.length === 0) {
    birds.push(new Bird());  // Add the first bird after the delay
  }
  
  // Check if a second bird should be added or removed
  if (aliveButterflyCount >= 4 && birds.length === 1) {
    birds.push(new Bird());  // Add second bird
  } else if (aliveButterflyCount < 4 && birds.length === 2) {
    birds.pop();  // Remove second bird
  }

  // Handle the case when all butterflies and chrysalises are gone
  if (butterflies.length === 0 && chrysalises.length === 0) {
    if (lastButterflyTime === 0) {
      lastButterflyTime = frameCount; // Record the frame count when the last butterfly is eaten
    } else if (frameCount - lastButterflyTime >= chrysalisDelay) {
      chrysalises.push(new Chrysalis(random(width), random(height)));  // Add a new chrysalis after delay
      lastButterflyTime = 0; // Reset the timer
    }
  }
}

class Chrysalis {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.timeToTransform = 100;  // Frames until transformation
  }
  
  update() {
    this.timeToTransform--;
  }
  
  show() {
    fill(0.08 * TWO_PI, 1, 0.59);  // Simple chrysalis color in HSB
    ellipse(this.x, this.y, 10, 20);  // Simple chrysalis shape
  }
  
  isButterfly() {
    return this.timeToTransform <= 0;
  }
}

class Butterfly {
  constructor(x, y, color) {
    this.position = createVector(x, y);
    this.velocity = createVector(random(-3, 3), random(-3, 3));
    this.energy = 0;  // Energy level increases when eating
    this.color = color; // Store butterfly color
    this.size = 15; // Size of the butterflies
  }
  
  update() {
    // Update position based on velocity
    this.position.add(this.velocity);
    
    // Bounce off edges of the canvas
    if (this.position.x < 0 || this.position.x > width) {
      this.velocity.x *= -1;  // Reverse direction
    }
    if (this.position.y < 0 || this.position.y > height) {
      this.velocity.y *= -1;  // Reverse direction
    }
  }
  
  show() {
    fill(this.color); // Use the stored color
    noStroke();

    // Draw the two ellipses to form an "X"
    // First ellipse (rotated)
    push();
    translate(this.position.x, this.position.y);
    rotate(PI / 4); // Rotate by 45 degrees
    ellipse(0, 0, this.size * 1.5, this.size / 3); // Draw first ellipse (longer and skinnier)
    pop();
    
    // Second ellipse (rotated)
    push();
    translate(this.position.x, this.position.y);
    rotate(-PI / 4); // Rotate by -45 degrees
    ellipse(0, 0, this.size * 1.5, this.size / 3); // Draw second ellipse (longer and skinnier)
    pop();
  }
  
  eat(flowers) {
    for (let flower of flowers) {
      let d = dist(this.position.x, this.position.y, flower.x, flower.y);
      if (d < 20 && flower.isAlive) {
        this.energy++;
        flower.eaten();  // Mark flower as eaten
        if (this.energy >= 5) {
          chrysalises.push(new Chrysalis(this.position.x, this.position.y));  // Lay a new chrysalis
          this.energy = 0;
        }
      }
    }
  }
  
  eatenBy(bird) {
    let d = dist(this.position.x, this.position.y, bird.x, bird.y);
    return d < 30;  // If butterfly is close enough to the bird, it gets eaten
  }
}

class Flower {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.isAlive = true; // Flower is initially alive
    this.regrowthTime = 200; // Time in frames to regrow after being eaten
    this.currentTime = 0; // Timer for regrowth
  }
  
  show() {
    if (this.isAlive) {
      fill(0, 1, 1); // Flower color in HSB (Red)
      // fill(random(TWO_PI), 1, 1); Flower color is random, but glitches
      ellipse(this.x, this.y, 10, 10);  // Simple flower shape
    }
  }
  
  eaten() {
    this.isAlive = false; // Mark flower as eaten
    this.currentTime = this.regrowthTime; // Reset timer
  }
  
  update() {
    if (!this.isAlive) {
      this.currentTime--;
      if (this.currentTime <= 0) {
        this.isAlive = true; // Flower regrows
      }
    }
  }
}

class Bird {
  constructor() {
    this.x = random(width);
    this.y = random(height);
    this.speed = random(1, 3);
  }
  
  update() {
    this.x += this.speed;
    if (this.x > width) {
      this.x = 0;
      this.y = random(height);  // Bird changes altitude randomly
    }
  }
  
  show() {
    fill(0, 0, 0); // Bird color in HSB (Black)
    ellipse(this.x, this.y, 30, 15);  // Simple bird shape
  }
}