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
, Butterfly
, Chrysalis
, and Bird
. Each has unique behaviors and interactions that drive the system:
Flowers regenerate after being eaten by butterflies.
Butterflies gain energy by feeding and can "lay eggs" (chrysalises) to propagate their species. They can also be eaten by birds.
Chrysalises represent the metamorphosis stage, transforming into butterflies after a timed delay. They can also be eaten by birds.
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:
Add a
aliveButterflyCount
variable: This variable will keep track of how many butterflies are alive in the sketch.Create a second bird: Add a condition that spawns the second bird when
aliveButterflyCount
reaches 4 live butterfliesHandle both birds: Ensure both birds update and check for collisions with butterflies.
Key Changes:
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.Bird array: Birds are stored in an array (birds) to handle multiple birds easily. Each bird is updated and checked for collisions with butterflies.
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:
Check if all butterflies and chrysalises are eaten: If both arrays are empty, push a new chrysalis into the
chrysalises
array.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 thebutterflies
andchrysalises
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:
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.
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:
Introduce a new variable to keep track of the time since the first butterfly is born.
Modify the
draw
function to check if the first butterfly has been created and increment the timer.Only add the first bird once the timer exceeds a specified delay.
Key Changes:
Timer Initialization:
firstButterflyBornTime
is set to0
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 using0colorMode(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:
Shape of the Butterfly: The
show()
method now draws two ellipses at 45-degree angles to form an "X". Thetranslate()
function moves the origin to the butterfly's position, and therotate()
function adjusts the orientation of each ellipse.Size Parameter: Added a
size
property to control the dimensions of the ellipses easily. You can adjust the value ofthis.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.,
Butterfly
,Bird
) 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 } }