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.

Leave a Reply

Your email address will not be published. Required fields are marked *