Art
Feb 25, 2025
Polar Radiance
Using the Boids Algorithm in a different way.

Demo

Press [Space Bar] to restart, [S] to save image.

Here's a link to my sketch: https://editor.p5js.org/interfinity/sketches/93iPmAb2s

Overview

This project resulted very differenly from what I first tried. Exploring the movement of particles, I came across the Boids Algorithm, which was first used for simulating the behavior of birds flying in groups: they follow the average direction of the others, but keeps a minimum distance so that they don't collide into each other. But instead of simply drawing out the particles moving in this behavior, I wanted to try something different. I first try to connect particles that are close to each other with lines, creating a mesh pattern, and I experimented with drawing some static particles which will repel the moving ones.

But eventrually, I discovered that the particles moving by the Boids Algorithm have a smooth but random curve path, so I tried to draw a thin white line from each particle to the center of the canvas, creating this radiant pattern, which gives a sense of stars, flowers, aurora, etc. Since I gave the lines an opacity, I have to limit the time in which they are drawn. After some testing, I set the time limit to be 2500ms (2.5s), which gives the best results.

Process

I first implimented the Boids Algorithm in JavaScript, creating a Boid class.

class Boid {
  constructor(x, y, ratio) {
    this.position = createVector(x, y);
    this.ratio = ratio
    this.velocity = createVector(random(-2 * ratio, 2 * ratio), random(-2 * ratio, 2 * ratio));
    this.acceleration = createVector(0, 0);
    this.maxSpeed = 3 * ratio;
    this.maxForce = 0.05 * ratio;
    this.neighborDist = 50 * ratio;
    this.desiredSeparation = 25 * ratio;
  }

  flock(boids) {
    let sep = this.separate(boids);
    let ali = this.align(boids);
    let coh = this.cohesion(boids);

    sep.mult(1.5);
    ali.mult(1.0);
    coh.mult(1.0);

    this.acceleration.add(sep);
    this.acceleration.add(ali);
    this.acceleration.add(coh);
    
    this.centerConnect()
  }

  separate(boids) {
    let steer = createVector(0, 0);
    let count = 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 > 0 && d < this.desiredSeparation) {
        let diff = p5.Vector.sub(this.position, other.position);
        diff.normalize();
        diff.div(d);
        steer.add(diff);
        count++;
      }
    }

    if (count > 0) {
      steer.div(count);
    }

    if (steer.mag() > 0) {
      steer.normalize();
      steer.mult(this.maxSpeed);
      steer.sub(this.velocity);
      steer.limit(this.maxForce);
    }

    return steer;
  }

  align(boids) {
    let sum = createVector(0, 0);
    let count = 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 > 0 && d < this.neighborDist) {
        sum.add(other.velocity);
        count++;
      }
    }

    if (count > 0) {
      sum.div(count);
      sum.normalize();
      sum.mult(this.maxSpeed);
      let steer = p5.Vector.sub(sum, this.velocity);
      steer.limit(this.maxForce);
      return steer;
    }

    return createVector(0, 0);
  }

  cohesion(boids) {
    let sum = createVector(0, 0);
    let count = 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 > 0 && d < this.neighborDist) {
        sum.add(other.position);
        count++;
      }
    }

    if (count > 0) {
      sum.div(count);
      return this.steerTowards(sum);
    }

    return createVector(0, 0);
  }

  steerTowards(target) {
    let desired = p5.Vector.sub(target, this.position);
    desired.normalize();
    desired.mult(this.maxSpeed);

    let steer = p5.Vector.sub(desired, this.velocity);
    steer.limit(this.maxForce);

    return steer;
  }

  centerConnect() {
    stroke(255, 25);
    strokeWeight(1.5 * this.ratio);
    line(this.position.x, this.position.y, width / 2, height / 2);
  }

  update() {
    this.prevPosition = this.position;

    this.velocity.add(this.acceleration);
    this.velocity.limit(this.maxSpeed);
    this.position.add(this.velocity);
    this.acceleration.mult(0);
  }
}

This class represents a Boid particle which draws a thin line between itself and the center of canvas every time the flock() method is called. Then, in the sketch.js file, I simply wrote a function reset() which creates 100 Boid particles and use the setTimeout() function to stop drawing in 2.5 seconds.

let boids = [];
let isDrawing = false;

function setup() {
  let canvas = createCanvas(
    Math.min(windowWidth, windowHeight),
    Math.min(windowWidth, windowHeight)
  );

  reset();
}

function draw() {
  for (let boid of boids) {
    boid.flock(boids);
    boid.update();

    moveOver(boid);
  }
}

function moveOver(b) {
  if (b.position.x > width) b.position.x -= width;
  if (b.position.x < 0) b.position.x += width;
  if (b.position.y > height) b.position.y -= height;
  if (b.position.y < 0) b.position.y += height;
}

function reset() {
  isDrawing = true;
  for (let i = 0; i < 100; i++) {
    boids.push(
      new Boid(random(width), random(height), Math.min(width / 800, 1))
    );
  }

  setTimeout(() => {
    boids = [];
    isDrawing = false;
  }, 2500);

  background(15);
}

function keyPressed() {
  if (keyCode == 83) {
    saveCanvas("polar_radiance.png");
  } else if (keyCode == 32 && !isDrawing) {
    reset();
  }
}

Also, the moveOver() function makes sure that when a Boid particles moves beyond an edge of the canvas, it returns to the opposite edge. And I used the keyPressed() function to allow users to press the [Space Bar] to restart the program and press [S] to save the canvas as an image.

Conclusion

Although I don't think this is the perfect use case for the Boids Algorithm, it is certainly an interesting experiment with it. Here are some results I got: