Free Flowing Flocking System

The Concept

I wanted to create a system that goes beyond traditional boid flocking behavior. The goal was to craft a visual experience that tells a story through motion, tension, and release. The system transitions between free-flowing states and complex mathematical formations, all while maintaining the organic feel of natural flocking behavior.

The particles move through several distinct phases:

  1. Free-flowing exploration
  2. Lissajous curve formations
  3. Logarithmic spiral patterns
  4. Mandala-like circular arrangements
  5. A parametric heart shape with controlled chaos

Each phase has its own character, with carefully tuned parameters for alignment, cohesion, and separation that give it a unique feel while maintaining visual coherence with the whole piece.

Technical Highlight

One of the most challenging aspects was creating smooth transitions when particles wrap around screen edges. Traditional edge wrapping creates jarring visual artifacts – straight lines cutting across the screen. The solution involves calculating the closest position between two particles, considering wrapped positions around screen edges. This ensures smooth flocking behavior even when some particles are on opposite sides of the screen. When combined with history clearing during wrapping, it eliminates the visual artifacts that often plague edge-wrapping systems.

Color and Visual Design

The visual design draws inspiration from natural phenomena – deep sea bioluminescence, sunset plasma, and aurora borealis. Each color scheme transitions smoothly into the next, creating a constantly evolving palette that complements the movement patterns.

The particle trails add a sense of history to the motion, creating organic flowing lines that help viewers understand the system’s evolution. The trail length and opacity are carefully tuned to create a balance between showing motion history and maintaining visual clarity.

Reflections and Future Work

While I’m pleased with the current state of the project, there are several exciting directions for future development:

  1. 3D Extensions: Expanding the system into three dimensions would open up possibilities for more complex formations and viewpoint changes.
  2. Interactive Elements: Adding user interaction through mouse movement or touch could create interesting disruptions in the flocking patterns.
  3. Performance Optimization: While the current system handles 150 particles smoothly, optimizing the spatial partitioning could allow for thousands of particles, creating even more complex emergent patterns.

The most interesting aspect of this project has been finding the sweet spot between chaos and order – allowing enough randomness to keep the system feeling organic while maintaining the mathematical beauty of the formations. It’s a delicate balance that mirrors many natural systems, where simple rules give rise to complex, beautiful behaviors.

Sketch – Week 9

Concept

For this week’s assignment, I was inspired by Ryoichi Kurokawa’s “syn_mod.n” artwork. I liked how fluid the artwork looked, and the intricacy of the webs. I intended to create similar visual shapes and manipulate them through separation and alignment.

Code Snippet 

class Boid {
  constructor(x, y) {
    this.position = createVector(x, y);
    this.velocity = p5.Vector.random2D(); 
    this.acceleration = createVector(0, 0);

    // Define maximum speed and steering force for the boid
    this.maxSpeed = 3;
    this.maxForce = 0.05;

    // Set initial weights for flocking behaviors
    this.setBehaviorWeights(20, 1, 1);
  }

  run(boids) {
    // Control boid's behavior, movement, and visual display
    this.flock(boids);      
    this.update();         
    this.wrapAround();     
    this.showConnections(boids); // Draw connecting lines between boids
  }

  setBehaviorWeights(sepWeight, aliWeight, cohWeight) {
    // Set weights for separation, alignment, and cohesion behaviors
    this.sepWeight = sepWeight;
    this.aliWeight = aliWeight;
    this.cohWeight = cohWeight;
  }

  flock(boids) {
    // Calculate the three main flocking behaviors
    let sep = this.separate(boids).mult(this.sepWeight); // Separation: avoid crowding
    let ali = this.align(boids).mult(this.aliWeight);    // Alignment: steer towards average heading
    let coh = this.cohere(boids).mult(this.cohWeight);   // Cohesion: move towards group center

    // Apply the combined steering forces to the boid
    this.applyForce(sep.add(ali).add(coh));
  }

Embedded Sketch 

Reflections 

I enjoyed learning the flocking behaviours. To improve my code, I would add interaction to it. I wanted to add a mousePressed interaction but I was struggling to get the visual effect I wanted. I also wanted the web to look more organic, and I would change the code using perlin noise to achieve this effect.

Dynamic Flocking

Concept

Here I tried to simulate flocking behavior by creating a “school” of boids that move together in a coordinated, natural pattern. Each boid follows three core rules—separation, alignment, and cohesion—to avoid crowding, match direction, and stay close to neighbors. Together, these rules give rise to a lifelike group movement, where each boid is influenced by its surroundings to form a cohesive, dynamic flock.

Code Breakdown

Each Boid has properties for position, velocity, and acceleration, and the draw loop continuously applies flocking behaviors, updates the boid’s position, and renders it on the canvas with a trailing effect for smooth movement. The flock method combines the three behaviors by calculating forces based on nearby boids within defined distances. The separation force keeps boids from colliding, alignment adjusts direction to match neighboring boids, and cohesion steers toward the group center. The wrapEdgesmethod ensures that boids reappear on the opposite side if they move off-screen. An interactive feature, mouseDragged, adds new boids at the mouse location when dragged, adding flexibility to the simulation.

let boids = []; // Array to store boid instances
let w = 600, h = 600;

function setup() {
  createCanvas(w, h);
  background(0);

  // Initialize boids with random positions
  for (let i = 0; i < 50; i++) {
    boids.push(new Boid(random(width), random(height)));
  }
}

function draw() {
  background(255, 5); // Slight trail effect
  
  boids.forEach(boid => {
    boid.flock(boids);  // Apply flocking behavior
    boid.update();      // Update position and velocity
    boid.wrapEdges();   // Wrap around edges of the canvas
    boid.show();        // Draw the boid
  });
}

// Constants for boid behavior
let M = 2;        // Max speed
let F = 0.28;     // Max force
let sepDist = 25; // Desired separation distance
let aliDist = 50; // Alignment neighbor distance
let cohDist = 150; // Cohesion neighbor distance

// Class to represent a single boid
class Boid {
  constructor(x, y) {
    this.position = createVector(x, y);
    this.velocity = createVector(random(-1, 1), random(-1, 1));
    this.acceleration = createVector(0, 0);
  }

  // Method to apply flocking behavior
  flock(boids) {
    let separation = this.separate(boids); // Separation
    let alignment = this.align(boids);     // Alignment
    let cohesion = this.cohesion(boids);   // Cohesion

    // Adjust weights for the forces
    separation.mult(1.5);
    alignment.mult(0.99);
    cohesion.mult(0.99);

    // Apply forces to acceleration
    this.acceleration.add(separation);
    this.acceleration.add(alignment);
    this.acceleration.add(cohesion);
  }

  // Update position based on velocity and acceleration
  update() {
    this.velocity.add(this.acceleration);
    this.velocity.limit(M); // Limit speed
    this.position.add(this.velocity);
    this.acceleration.mult(0); // Reset acceleration
  }

  // Wrap boids around the screen edges
  wrapEdges() {
    this.position.x = (this.position.x + w) % w;
    this.position.y = (this.position.y + h) % h;
  }

  // Draw boid as a small circle
  show() {
    fill(255, 0, 0);
    ellipse(this.position.x, this.position.y, 10);
  }

  // Separation behavior: Avoid crowding neighbors
  separate(boids) {
    let steer = createVector(0, 0);
    let count = 0;
    boids.forEach(other => {
      let distance = p5.Vector.dist(this.position, other.position);
      if (distance > 0 && distance < sepDist) {
        let diff = p5.Vector.sub(this.position, other.position);
        diff.normalize();
        diff.div(distance);
        steer.add(diff);
        count++;
      }
    });
    if (count > 0) steer.div(count);
    if (steer.mag() > 0) {
      steer.normalize();
      steer.mult(M);
      steer.sub(this.velocity);
      steer.limit(F);
    }
    return steer;
  }

  // Alignment behavior: Steer towards the average heading of local flockmates
  align(boids) {
    let sum = createVector(0, 0);
    let count = 0;
    boids.forEach(other => {
      let distance = p5.Vector.dist(this.position, other.position);
      if (distance > 0 && distance < aliDist) {
        sum.add(other.velocity);
        count++;
      }
    });
    if (count > 0) {
      sum.div(count);
      sum.normalize();
      sum.mult(M);
      let steer = p5.Vector.sub(sum, this.velocity);
      steer.limit(F);
      return steer;
    }
    return createVector(0, 0);
  }

  // Cohesion behavior: Steer towards the average position of local flockmates
  cohesion(boids) {
    let sum = createVector(0, 0);
    let count = 0;
    boids.forEach(other => {
      let distance = p5.Vector.dist(this.position, other.position);
      if (distance > 0 && distance < cohDist) {
        sum.add(other.position);
        count++;
      }
    });
    if (count > 0) {
      sum.div(count);
      return this.seek(sum);
    }
    return createVector(0, 0);
  }

  // Seek method to steer boid towards a target position
  seek(target) {
    let desired = p5.Vector.sub(target, this.position);
    desired.normalize();
    desired.mult(M);
    let steer = p5.Vector.sub(desired, this.velocity);
    steer.limit(F);
    return steer;
  }
}

// Function to add a new boid on mouse drag
function mouseDragged() {
  boids.push(new Boid(mouseX, mouseY));
}

Future Improvements

While the current simulation focuses on realistic flocking dynamics, future enhancements will center around visual and auditory immersion. Adding a colorful background and subtle sound effects could heighten the simulation’s atmosphere, making the school of boids feel more alive and enhancing the viewer’s experience.

Week 9 | Webbings

Look here hehe
Concept

Initially, I wanted to make a simple tower defense game where players have to block the oncoming flocks of enemies. However, the execution was more difficult than I intended. My next idea then came from the behavior of birds themselves.

Enormous flocks of Snow Geese form during migration, yet thanks to almost a 6th sense individuals do not collide. Photo by Ray Hennessy via Birdshare.

birds in a flock pay close attention to the birds around them—particularly their closest neighbors.

But they might collide with birds from other species or different groups. Hence, my concept becomes as so: What if I make a flock of two or more different bird groups? A) They must group up with the same group B) Avoid different group.

Sketch Prototype

Here, what I did was to represent the boids in two colors by introducing a color property. This then allows me to modify the other behaviors and forces such that they interact with this color property. Particularly, the separation, alignment, and cohesion are affected by this change.

let emoji = this.color === "#C96868" ? "" : "";
separate(boids) {
  let desiredSeparation = 25;
  let steer = createVector(0, 0);
  let count = 0;
  for (let i = 0; i < boids.length; i++) {
    let other = boids[i];
    let d = p5.Vector.dist(this.position, other.position);
    if (d > 0 && d < desiredSeparation && other.color === this.color) {
      // Only same color
      let diff = p5.Vector.sub(this.position, other.position);
      diff.normalize();
      diff.div(d);
      steer.add(diff);
      count++;
    }
  }

But what if, and what happens when we connect a string between each boids? What visuals would it look like, and how would it behave?

I followed this intuition and attempted to add strings that attach to each boid. Of course, they are also vector which means these strings are attracted and affected by external forces.

//Connect
  //Function to connect a string between each boids
  connect(boids) {
    for (let other of boids) {
      let d = p5.Vector.dist(this.position, other.position);
      let maxDistance = 200; // Maximum distance for drawing a line

      // Only draw line if within maxDistance and the other boid is not itself
      if (d > 0 && d < maxDistance) {
        stroke(200, 50);
        strokeWeight(0.5); 
        line(
          this.position.x,
          this.position.y,
          other.position.x,
          other.position.y
        );
      }
    }

Alas, then it suddenly happened. But what a minute–what is this?


Aha! I have just visualized the connection between each boids. Instead of drawing lines on the current information, these lines draw from previous (a frame delay) information of the boids. In essence, they are the boids, birds, and flocks themselves, but represented in lines instead! It seems that the lines are following particularly the white boid but acts just like both red and white boid. I am unsure why.

Challenges & Improvements

➡️During the red-white test, I wanted the boids to return towards the screen again whenever it touches the edge of the canvas. I tried many implementations but found that by multiplying it to -1, or the negative vector does the job best.

➡️For the color checking between each boid, initially I used an if statement. While it worked, I also learned that ternary conditional statement worked better for this implementation where it just pairs up the color code and emoji.

🛠️Performance is quite an issue right now because the sketch is doing three calculations: One red, one white, and for the strings. In the future, I hope to bring more optimizations especially lifetimes for each vector to make sure that they don’t hog the machine.

Resources

All about birds – Cornellab

Ternary Operators – JavaScript

Week 9 Assignment

Concept

In this code, I merged a few ideas to create a cohesive set of actions I envisioned before executing it:

  • Flowers appearing at random places.
  • Flowers opening and closing.
  • A bee hive.
  • Bees follw to track the flowers (only opened ones and avoid the closed one)..
  • After passing all the open flowers bees return back to the hive.
  • Clicking a bee hive that follows the other swarm of bees (boid flock).
Highlight I’m proud of
class BeeBoid {
  constructor(x, y) {
    this.position = createVector(x, y);
    this.velocity = createVector(random(-1, 1), random(-1, 1));
    this.acceleration = createVector(0, 0);
    this.maxForce = 0.05; // Maximum steering force
    this.maxSpeed = 3; // Maximum speed
    this.size = 15; // Size of the bee body
    this.wingSize = 8; // Size of the wings
    this.xmove = random(-0.5, 0.5); // Horizontal movement speed
    this.ymove = random(-0.5, 0.5); // Vertical movement speed
  }

  applyForce(force) {
    this.acceleration.add(force);
  }

  seek(target) {
    let desired = p5.Vector.sub(target, this.position);
    desired.setMag(this.maxSpeed);
    let steer = p5.Vector.sub(desired, this.velocity);
    steer.limit(this.maxForce);
    this.applyForce(steer);
  }

  update() {
    this.velocity.add(this.acceleration);
    this.velocity.limit(this.maxSpeed);
    this.position.add(this.velocity);
    this.acceleration.mult(0); // Reset acceleration
  }

  borders() {
    if (this.position.x > width) this.position.x = 0;
    else if (this.position.x < 0) this.position.x = width;
    if (this.position.y > height) this.position.y = 0;
    else if (this.position.y < 0) this.position.y = height;
  }

  display() {
    // Draw wings
    ellipseMode(RADIUS);
    noStroke();
    fill(171, 240, 255);
    // Left wings
    ellipse(this.position.x - 10, this.position.y - 5, this.wingSize, this.wingSize);
    ellipse(this.position.x - 10, this.position.y + 5, this.wingSize, this.wingSize);
    // Right wings
    ellipse(this.position.x + 10, this.position.y - 5, this.wingSize, this.wingSize);
    ellipse(this.position.x + 10, this.position.y + 5, this.wingSize, this.wingSize);

    // Draw body
    fill(255, 244, 61);
    angleMode(DEGREES);
    ellipse(this.position.x, this.position.y, this.size, this.size * 1.5);
    stroke(0);
    strokeWeight(5);
    line(this.position.x - 10, this.position.y - 5, this.position.x + 10, this.position.y - 5);
    line(this.position.x - 10, this.position.y + 5, this.position.x + 10, this.position.y + 5);
  }

  move() {
    this.position.x += this.xmove;
    this.position.y += this.ymove;

    this.barrier();
  }

  barrier() {
    if (this.position.x >= width || this.position.x <= 0) {
      this.xmove = -this.xmove;
    }
    if (this.position.y >= height || this.position.y <= 0) {
      this.ymove = -this.ymove;
    }
  }
}
class Flock {
  constructor() {
    this.boids = [];
  }

  addBoid(boid) {
    this.boids.push(boid);
  }

  run() {
    for (let boid of this.boids) {
      let closestFlower = null;
      let closestDist = Infinity;

      // Check for the closest flower
      for (let flower of flowerArray) {
        if (flower.isAlive) {
          let d = p5.Vector.dist(boid.position, createVector(flower.centreX, flower.centreY));
          if (d < closestDist) {
            closestDist = d;
            closestFlower = flower;
          }
        }
      }

      if (closestFlower) {
        boid.seek(createVector(closestFlower.centreX, closestFlower.centreY));
      }
      
      boid.update();
      boid.borders();
boid.display();    }
  }
}
  • Arrays are used to store flowers, petals, and stem dots, allowing dynamic addition, deletion, and access to individual elements for updating their states.
  • For loops iterate through arrays and draw multiple flowers and petals, while if conditions handle flower removal, withering, and the periodic addition of new flowers.
  • The code uses trigonometric functions like Math.atan2 to calculate angles for petal positioning, creating uniqu-looking curved  flower shapes.
  • The frameCount variable is used to create periodic actions and smooth animations, such as blooming and movement of petals over time.
Embedded sketch

Edit Sketch: https://editor.p5js.org/mariamalkhoori/sketches/exMNOF-7n

Reflection and ideas for future work or improvements
    • I wanted to add sound to this sketch but did not have the time.
    • Playing around to develop some sort of story.
    • Work on better graphics; I wanted the bee wings to flutter and be animated and to create a better-shaped beehive.
    • Create my own unique-looking flowers using trigonometric functions.

Refrences:
https://editor.p5js.org/HelenaCui/sketches/qwgXBW6Da

Week 9: School of Fish

Concept:

For this week’s assignment, my dynamic flocking system is a school of fish. I added some oscillation to their weights by using sin(). I also randomized their maxSpeed and maxForce to make it seem more natural.

.

Code Snippet & Challenges:

function mousePressed() {
  let attractionPoint = createVector(mouseX, mouseY);
  for (let boid of flock.boids) {
    let attraction = p5.Vector.sub(attractionPoint, boid.position);
    attraction.setMag(0.6);
    boid.applyForce(attraction);
  }
  
  for (let i = 0; i < 20; i++) {
    particles.push(new Particle(mouseX, mouseY));
  }
}

In this part of the code, I added a mousePressed() function in which the fish get attracted to wherever the mouse is at the time when it is clicked on the canvas. I also added some food particles for the fish, which is what they attracted to. I would say the most challenging part was figuring out what the formula is for the attraction part.

Future Improvements:

  • I would definitely add a more colorful background and some sounds, for now I wanted to focus on more of the technical aspect of the school of fish.

Week #9 – Following uncertainty

Concept

I had a curious inspiration while I was going in a bus to campus. I was staring at the window, looking silently at the night, and this silent appreciation was then followed by the next music suggestion in my music app: DIIV – Taker.

It was a weirdly melancholic song, the type of beat I like: melancholic, but not to excessive levels to induce sadness, but rather, a peace calmness. And this is the type of feeling I got from listening to this music, calmness. Thus, for this week assignment, I wanted to share what I felt with a music visualization using flock systems.

Embedded sketch

Note: If the p5.js version is slow, it is suggested to try either this version or make a copy of the GitHub’s repo and run it locally on the computer.

Make sure to click on the Canva to start with the visualization!

Full-screen: Full-screen version

Brief explanation of the code

The flock system is divided on multiple parts, which are activated according to the time of the song; it is easily done with the value returned from the function song.currentTime(). Here is a quick example:

Let’s say that I want to generate the wave effect that appears at the beginning, for this, I create the following parameters inside a custom function called startPart(value):

The wave code is located in the second if condition (value == 2).

function startPart(value) {
  if (value == 1) {
    for (let boid of flock) {
      //Reset value.
      boid.velocity.mult(0);
      boid.maxForce = 0.0;
      //Avoid boids from going out from the borders of the screen.
      boid.transparency = 1;
      boid.acceleration.add(random(-2, 2), random(-0.5, 0.5));
      boid.update_values(1); //Updates the boundaries to the first phase values.
      part = 1;
    }
  }

  //THIS IS THE WAVE CODE
  if (value == 2) {
    for (let boid of flock) {
      //Reset value.
      boid.velocity.mult(0);

      //Assign maxForce
      boid.maxForce = 0.01;

      //Avoid boids from going out from the borders of the screen.
      boid.transparency = 10;
      boid.acceleration.add(random(1, 3), random(-1, 1));
      boid.update_values(2); //Updates the boundaries to the first phase values.
      part = 2;
    }
  }

And then, the specific part is called with the following custom function checkAudioTime() on the second 14 of the song:

function checkAudioTime() {
  if (round(audio.currentTime(), 0) == 6) {
    startPart(1);
  }

  if (round(audio.currentTime(), 0) == 14) {
    startPart(2);
  }

It is important to note that checkAudioTime()is called continuously under draw().

Highlight of some code that I am particularly proud of

I am mostly proud of the patterns I created for this visualization. In total, there are 11 variations which are divided between two flocking systems. I will not paste all the variations here, since it is almost 300 lines of code, but I will share three examples of it:

if (value == 9) {
  for (let boid of flock_outside) {
    //Reset value.
    boid.velocity.mult(0);

    //Assign maxForce
    boid.maxForce = 0.01;
    boid.maxSpeed = 1;
    boid.perception_radius = 50;

    //Avoid boids from going out from the borders of the screen.
    boid.transparency = 50;
    boid.acceleration.add(random(-1, 1), random(-1, 1));
    boid.update_values(6); //Updates the boundaries to the first phase values.
    part = 3;
  }
}

if (value == 10) {
  for (let boid of flock_outside) {
    //Reset value.
    boid.velocity.mult(0);
    boid.maxForce = 1;
    boid.transparency = 5;
    boid.acceleration.add(random(0), random(10));
  }

  for (let boid of flock) {
    //Reset value.
    boid.velocity.mult(0);
    boid.maxForce = 1;
    boid.transparency = 5;
    boid.acceleration.add(random(0), random(10));
  }
}

if (value == 11) {
  for (let boid of flock_outside) {
    //Reset value.
    boid.update_values(7);
  }

  for (let boid of flock) {
    //Reset value.
    boid.update_values(7);
  }
  part = 6;
}

Reflection and ideas for future work or improvements

I am mostly proud of this work. It is by far the assignment I have spent the most time on (or probably the second one). It was an enjoyable challenge, which I felt really curious about what kind of results I could get with the flocking systems. Although, I felt that some patterns in the visualization are rather simplistic and should have been reacting to the sound.

Not to imply that I did not try to implement more reactions that are based on the frequencies of the song, but after some attempts, it was best to disregard the idea since it would consume a lot of time.

Used resources

17.11: Sound Visualization: Frequency Analysis with FFT – p5.js Sound Tutorial

Coding Challenge 124: Flocking Simulation

DIIV // Taker (Official Audio)

Underwater Dance: A School of Fish in Motion – Week 9

Concept 

This week’s sketch is all about the movement of a school of fish. I wanted to show how they can swim in different patterns and scatter around, just like real fish do in the ocean. The main idea is to create a fun and lively underwater scene where the fish either swim together in formations or move freely, giving a sense of both community and individuality.

Code Highlight

One cool part of my code is the generateFormationPoints function. This function creates different shapes for the fish to follow, allowing for variety in their movement. Here’s a snippet of how it works:

function generateFormationPoints() {
  targetPoints = []; // Reset the target points array
  let shape = random(['doubleSpiral', 'wave', 'school', 'dna', 'vortex', 'shoal']); // Randomly select a formation shape
  let centerX = width / 2; // Calculate the center x position
  let centerY = height / 2; // Calculate the center y position
  let size = min(width, height) * 0.3; // Determine the size of the formation

  switch(shape) {
    case 'doubleSpiral':
      // Generate points for a double spiral formation
      // Logic for creating double spiral points...
      break;
    case 'wave':
      // Generate points for a wave formation
      // Logic for creating wave points...
      break;
    // Additional cases for other shapes...
  }
}

In this function, the fish can follow different patterns, like spirals, waves, or even formations resembling a DNA strand. Each time the function runs, it randomly picks a shape for the fish to follow. This keeps the scene dynamic and adds a layer of excitement to the simulation, as the fish move together in beautiful formations.

Embedded Sketch

Reflection and Future Ideas
I really like how this sketch turned out! The way the fish swim together creates a beautiful underwater scene that feels lively and realistic. The switch between swimming in formation and scattering around adds a fun element to the animation, making it engaging for viewers. I especially enjoyed seeing how the fish react to the different target points and how smoothly they transition between formations.

For the future, I see many ways to improve this project. I’d love to add more complex behaviors, like introducing a predator that chases the fish. This would add tension and excitement to the simulation, showcasing how the fish react to threats and change their behavior to survive.

Additionally, I could enhance the visuals by adding more details to the fish, creating different types and colors to represent various species. Improving the background with coral reefs or underwater plants could also make the scene more immersive. Sound effects, like bubbling water or gentle waves, could enhance the experience, making it feel like you’re truly underwater.

 

Week 9- Dynamic Firework Simulation

Concept:
The idea was to create a dynamic simulation where fireworks launch, explode, and slowly fade away, all while maintaining a sense of development over time. I wanted it to feel like a live performance in the sky, starting from calm darkness to sudden bursts of light and color and back to calm again.

Embedded Sketch:

Code Logic:

Here’s how the flocking concept is applied in my code:

Cohesion (Launching): The initial firework is like a single leader particle that starts from the bottom of the screen and moves upward. This particle influences the others once it bursts.
Separation (Explosion): When the main firework particle reaches its peak and “explodes,” it releases smaller particles that spread out in random directions. The particles act like a flock momentarily repelling from the center, creating that explosion effect.
Alignment (Falling and Fading): As the particles start to slow down and fall due to gravity, they align more with each other to create the visual of embers gently dispersing and fading away.

Code I’m Proud Of:

One part of the code I’m particularly proud of is how the flocking system’s logic was adapted to control the explosion and fading of particles:

explode() {
  for (let i = 0; i < 100; i++) {
    let p = new Particle(this.firework.pos.x, this.firework.pos.y, false);
    this.particles.push(p);
  }
}

When the firework reaches its highest point, this explode() function kicks in. It creates 100 new Particle objects, each with its own direction and speed. This mimics the behavior of a flock splitting apart. Instead of moving together like traditional flocks, these particles use a random2D() vector to scatter and simulate an explosion.

Additionally, each particle follows a simple rule:

Velocity Adjustment: Each particle’s velocity is affected by gravity, making them fall and spread out more realistically.
Lifespan Control: Particles gradually lose opacity (this.lifespan -= 4) to simulate them fading as they fall, creating that “embers in the sky” look.
This adaptation of flocking logic is what makes the explosion feel dynamic and natural, allowing the particles to behave independently yet still appear as part of a cohesive firework.

Challenges I Faced:

One of the main challenges was making the explosion look natural. Initially, the particles either looked too rigid or didn’t spread out enough. To solve this, I played around with p5.Vector.random2D() and tweaked the speed multipliers until it felt right. Another challenge was getting the fading effect correct. It took some trial and error to adjust the lifespan so that particles would fade smoothly and not disappear too abruptly.

Future Improvements:

While I’m happy with how the project turned out, there’s always room for more! Here are some ideas I’m thinking about:

Sound Integration: Adding sound effects would make the fireworks feel even more real. I’d like to use the p5.sound library to sync explosions with audio.
Mouse Interaction: Letting users click to create their own fireworks would add an interactive element to the project.
Different Shapes and Trails: Experimenting with different particle shapes and trail effects could make each firework more unique and visually interesting.
Color Themes: Adding color gradients or changing themes over time to simulate different types of fireworks shows would enhance the overall look.

Week 9 – Khalifa Alshamsi

Inspiration & Concept

The inspiration for this project came from Robert Hodgin and Aditya Anantharaman. Robert Hodgin inspired the concept of creating a tree and birds flocking around it, while Aditya Anantharaman provided insights on how to implement a flocking mechanism.

Sketch

Code

let seed;
let flock = [];
let centerTree;

function setup() {
  createCanvas(windowWidth, windowHeight);
  angleMode(DEGREES);
  seed = Date.now();
  randomSeed(seed);
  noStroke();

  // Initialize the tree
  centerTree = new Tree(width / 2, height - 100);

  // Initialize the flock
  for (let i = 0; i < 200; i++) {
    flock.push(new Boid(random(width), random(height)));
  }
}

function draw() {
  background(255);
  centerTree.display();

    // Update and display each boid in the flock
  for (let boid of flock) {
    boid.flock(flock);
    boid.update();
    boid.edges();
    boid.show();
  }
}

// Tree class
class Tree {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  display() {
    push();
    translate(this.x, this.y);
    // Start drawing the branches of the tree with an initial length of 25
    branch(25);
    pop();

  }
}
function branch(len) {
  push();
  // Recursive branching if the length is greater than 5
  if (len > 5) {
    let newLen = len * 0.8;
    // Set the stroke weight relative to the branch length to create a tapering effect
    strokeWeight(map(len, 10, 120, 1, 8));
    stroke(0);
    line(0, 0, 0, -len);
    // Move to the end of the current branch to create the next branch
    translate(0, -len);
    
    rotate(20); branch(newLen);
    rotate(-40); branch(newLen);
  } 
  pop();
}

// Boid class for flocking behavior
class Boid {
  constructor(x, y) {
    // Set the initial position of the boid
    this.position = createVector(x, y);
    // Assign a random initial velocity to each boid
    this.velocity = p5.Vector.random2D();
    // Set the magnitude of the velocity to control the initial speed
    this.velocity.setMag(random(2, 4));
    // Initialize acceleration with zero vector
    this.acceleration = createVector();
    // Maximum steering force to apply, controls agility
    this.maxForce = 0.2;
    // Maximum speed of the boid
    this.maxSpeed = 4;
    // Size of the boid, represented as a dot
    this.size = random(2, 6);
  }

  // Handles wrapping of boids across the edges of the canvas to keep them in view
  edges() {
    if (this.position.x > width) {
      this.position.x = 0;
    } else if (this.position.x < 0) {
      this.position.x = width;
    }
    if (this.position.y > height) {
      this.position.y = height;
    } else if (this.position.y < 0) {
      this.position.y = height;
    }
  }

  // Alignment behavior: steer towards the average heading of nearby boids
  align(boids) {
    let perceptionRadius = 75;
    let steering = createVector();
    let total = 0;
    for (let other of boids) {
      let d = dist(
        this.position.x,
        this.position.y,
        other.position.x,
        other.position.y
      );
      if (other != this && d < perceptionRadius) {
        steering.add(other.velocity);
        total++;
      }
    }
    if (total > 0) {
      steering.div(total);
      steering.setMag(this.maxSpeed);
      steering.sub(this.velocity);
      steering.limit(this.maxForce);
    }
    return steering;
  }

  // Cohesion behavior: steer towards the average position of nearby boids
  cohesion(boids) {
    let perceptionRadius = 75;
    let steering = createVector();
    let total = 0;
    for (let other of boids) {
      let d = dist(
        this.position.x,
        this.position.y,
        other.position.x,
        other.position.y
      );
      if (other != this && d < perceptionRadius) {
        steering.add(other.position);
        total++;
      }
    }
    if (total > 0) {
      steering.div(total);
      steering.sub(this.position);
      steering.setMag(this.maxSpeed);
      steering.sub(this.velocity);
      steering.limit(this.maxForce);
    }
    return steering;
  }

  // Separation behavior: steer away from nearby boids to avoid crowding
  separation(boids) {
    let perceptionRadius = 24;
    let steering = createVector();
    let total = 0;
    for (let other of boids) {
      let d = dist(
        this.position.x,
        this.position.y,
        other.position.x,
        other.position.y
      );
      if (other != this && d < perceptionRadius) {
        let diff = p5.Vector.sub(this.position, other.position);
        diff.div(d * d);
        steering.add(diff);
        total++;
      }
    }
    if (total > 0) {
      steering.div(total);
      steering.setMag(this.maxSpeed);
      steering.sub(this.velocity);
      steering.limit(this.maxForce);
    }
    return steering;
  }

  // Apply the three flocking rules to determine the boid's acceleration
  flock(boids) {
    let alignment = this.align(boids);
    let cohesion = this.cohesion(boids);
    let separation = this.separation(boids);

    // Apply the three simple flocking rules
    alignment.mult(1.0); // Alignment: move in the average direction of nearby boids
    cohesion.mult(1.0);  // Cohesion: move towards the average position of nearby boids and occasionally towards the tree
    separation.mult(1.5); // Repulsion: move away if too close to neighbors

    this.acceleration.add(alignment);
    this.acceleration.add(cohesion);
    this.acceleration.add(separation);

    // Dynamic change: Modify size to create tension and release effect
    this.size = map(sin(frameCount * 0.05), -1, 1, 1, 4);
  }

  update() {
    this.position.add(this.velocity);
    this.velocity.add(this.acceleration);
    this.velocity.limit(this.maxSpeed);
    // Reset acceleration to 0 for the next frame to avoid compounding effects
    this.acceleration.mult(0);
  }

  show() {
    fill(0);
    ellipse(this.position.x, this.position.y, this.size);
  }
}

Setting the Scene

The project begins by defining a canvas that spans the entire window. A central tree is positioned at the bottom center of the canvas, and a flock of 200 birds (represented as dots) is initialized at random positions. The code uses randomSeed(seed) to ensure the system’s behavior remains consistent across different runs by fixing the random values generated for the tree and flock.

function setup() {
  createCanvas(windowWidth, windowHeight);
  angleMode(DEGREES);
  seed = Date.now();
  randomSeed(seed);
  noStroke();

  centerTree = new Tree(width / 2, height - 100);

  for (let i = 0; i < 200; i++) {
    flock.push(new Boid(random(width), random(height)));
  }
}

The setup() function also initializes the tree and the flock. The tree will remain fixed, while the birds will exhibit a behavior similar to a flock moving around and reacting to each other and their surroundings.

Flocking Behavior of Birds

The flock of birds is represented by instances of the Boid class. Each bird follows three simple rules that are inspired by Aditya Anantharaman work:

  1. Alignment: Birds attempt to move in the average direction of their neighbors within a certain perception radius. This helps ensure the flock is cohesive.
  2. Cohesion: Birds steer towards the average position of nearby birds to maintain the flock’s unity.
  3. Separation: Birds steer away if they come too close to their neighbors, preventing clustering and collisions.

Each bird’s behavior is implemented using three methods: align(), cohesion(), and separation(). These methods collectively determine the bird’s acceleration.

flock(boids) {
  let alignment = this.align(boids);
  let cohesion = this.cohesion(boids);
  let separation = this.separation(boids);

  alignment.mult(1.0);
  cohesion.mult(1.0);
  separation.mult(1.5);

  this.acceleration.add(alignment);
  this.acceleration.add(cohesion);
  this.acceleration.add(separation);
}

In the flock() method, the calculated steering forces for alignment, cohesion, and separation are combined and applied to each bird’s acceleration. The mult() function is used to control the influence of each behavior, making separation slightly more influential to avoid overcrowding.

Dynamic Movement and Visual Appeal

One of the features that makes this flocking system visually appealing is the dynamic change in the size of each bird. The size varies sinusoidally, creating a rhythmic pulsation that adds a sense of tension and release to the movement of the flock:

this.size = map(sin(frameCount * 0.05), -1, 1, 1, 4);

The sin() function is used to create a smooth oscillation effect, making the birds appear to “breathe” as they move. This subtle addition contributes to the natural, lifelike quality of the simulation.

Future Improvements

Moving forward, there are several potential improvements to enhance this project further. One idea is to add environmental changes, such as wind or obstacles, that the flock must navigate around. Adding interactivity, like allowing users to click and influence the direction of the birds or the growth of the tree, would also make the experience more immersive.