block by harrystevens a82fc63f8bed339d64ccdcc49a635a05

Bad Boids

Full Screen

This is my first attempt at implementing Craig Reynolds’s boids flocking simulation. Click and drag to add boids.

If you compare this to successful implementations, such as this, this and this, you’ll see that adjusting the parameters should have a more significant affect on the behavior of the boids. I don’t know how to implement the separation parameter correctly; I don’t know what factors should affect the boids’ velocity; and I don’t understand why the boids have a tendency to move to the right. Finally, this implementation uses no spatial index, so it runs in quadratic time.

Even though it is very flawed, I thought it’d be fun to publish a notebook of this early version to track my progress. And the boids still exhibit some pretty cool emergent behavior.

index.html

<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      margin: 0;
    }

    #controls {
      font-family: sans-serif;
      padding: 5px;
      position: absolute;
      text-align: center;
      width: 100%;
      bottom: 0px;
    }

    #controls .control {
      background: rgba(255, 255, 255, .95);
      display: inline-block;
      padding: 10px;
      text-align: left;
      width: 200px;
    }

    #controls .control .range input, #controls .control .range .value {
      display: inline-block;
    }

    #controls .control .range .value {
      font-size: 12px;
      margin-top: -5px;
      vertical-align: middle;
    }

    #controls .control .description {
      font-size: 12px;
    }
  </style>
</head>
<div id="controls">
  <div class="control">
    <div class="title">Alignment</div>
    <div class="range">
      <input data-parameter="alignment" type="range" min="0" max="1" value=".5" step=".1" />
      <div class="value">0.5</div>
    </div>
    <div class="description">Steer towards the average heading of local flockmates</div>
  </div>
  <div class="control">
    <div class="title">Cohesion</div>
    <div class="range">
      <input data-parameter="cohesion" type="range" min="0" max="1" value=".5" step=".1" />
      <div class="value">0.5</div>
    </div>
    <div class="description">Steer to move toward the average position of local flockmates</div>
  </div>
  <div class="control">
    <div class="title">Separation</div>
    <div class="range">
      <input data-parameter="separation" type="range" min="0" max="1" value=".5" step=".1" />
      <div class="value">0.5</div>
    </div>
    <div class="description">Steer to avoid crowding local flockmates</div>
  </div>
  <div class="control">
    <div class="title">Distance</div>
    <div class="range">
      <input data-parameter="distance" type="range" min="1" max="200" value="30" step="1" />
      <div class="value">30</div>
    </div>
    <div class="description">Maximum distance of other boids to consider</div>
  </div>
</div>
<div id="simulation"></div>
<body>
  <script src="https://unpkg.com/geometric@2.2.3/build/geometric.min.js"></script>
  <script src="https://d3js.org/d3-array.v2.min.js"></script>
  <script src="https://d3js.org/d3-random.v2.min.js"></script>
  <script>
  class Boids {
    init(opts){
      this.width = opts && opts.width ? opts.width : innerWidth;
      this.height = opts && opts.height ? opts.height : innerHeight;
      this.center = [this.width / 2, this.height / 2];

      this.data = [];
      
      this.separation = opts && isFinite(opts.separation) ? opts.separation : .5;
      this.alignment = opts && isFinite(opts.alignment) ? opts.alignment : 1;
      this.cohesion = opts && isFinite(opts.cohesion) ? opts.cohesion :  1;
      
      this.distance = opts && opts.distance ? opts.distance : 30;

      return this;
    }
    
    add(datum){
      const d = datum || {};
      d.angle = d.angle || 0;
      d.startAngle = d.angle;
      d.pos = d.pos || this.center;
      d.speed = d.speed || 1;

      this.data.push(d);
      
      return this;
    }

    tick(){
      // Check if any of alignment, cohesion, or separation are greater than 0
      const hasValue = this.alignment || this.cohesion || this.separation;
      
      if (hasValue){
        // Loop through the boids to find the neighborhood of each
        for (let i = 0, l = this.data.length; i < l; i++){
          const d = this.data[i];

          d.neighborhood = [];

          // Find all boids within this.distance
          for (let i0 = 0, l0 = this.data.length; i0 < l0; i0++){
            const d0 = this.data[i0];

            if (geometric.lineLength([d.pos, d0.pos]) < this.distance) d.neighborhood.push(d0);
          }
        }      
      }

      // Loop through the boids to calculate the new position
      for (let i = 0, l = this.data.length; i < l; i++){
        const d = this.data[i];

        if (d.neighborhood.length && hasValue){
          const alignment = d3.mean(d.neighborhood, d0 => d0.angle),
                cohesion = geometric.lineAngle([
                  d.pos,
                  geometric.polygonMean(d.neighborhood.map(d0 => d0.pos))
                ]),
                separation = d.startAngle;

          // The new angle. Alignment needs to be boosted by some coefficient
          d.angle = (cohesion * this.cohesion +
                    separation * this.separation +
                    alignment * this.alignment * 40) / 
                    (this.cohesion + this.separation + this.alignment * 40);
        }
        
        const [x, y] = geometric.pointTranslate(d.pos, d.angle, d.speed);
        d.pos = [x < 0 ? this.width : x > this.width ? 0 : x, y < 0 ? this.height : y > this.height ? 0 : y];
      }
      
      return this;
    }
  }

  const myBoids = (_ => {
    const simulation = new Boids;
    simulation.init({
      cohesion: .5,
      alignment: .5,
      separation: .5
    });

    // Add 500 boids
    for (let i = 0; i < 500; i++){
      simulation.add({
        angle: d3.randomUniform(-360, 360)(),
        pos: [
          d3.randomUniform(0, simulation.width)(),
          d3.randomUniform(0, simulation.height)()
        ]
      });
    }

    return simulation;
  })();

  // Draw the simulation
  const wrapper = document.getElementById("simulation");
  const canvas = document.createElement("canvas");
  canvas.width = myBoids.width;
  canvas.height = myBoids.height;
  wrapper.appendChild(canvas);
  const context = canvas.getContext("2d");
  context.strokeStyle = "red";
  context.fillStyle = "pink";

  function tick(){
    requestAnimationFrame(tick);
    context.clearRect(0, 0, myBoids.width, myBoids.height);

    // The simulation.tick method advances the simulation one tick
    myBoids.tick();
    for (let i = 0, l = myBoids.data.length; i < l; i++){
      const boid = myBoids.data[i],
            a = geometric.pointTranslate(boid.pos, boid.angle - 90, 3),
            b = geometric.pointTranslate(boid.pos, boid.angle, 9),
            c = geometric.pointTranslate(boid.pos, boid.angle + 90, 3);
      context.beginPath();
      context.moveTo(...a);
      context.lineTo(...b);
      context.lineTo(...c);
      context.lineTo(...a);
      context.fill();
      context.stroke();
    }
  }
  tick();

  let holding = false;
  canvas.addEventListener("mousedown", e => { holding = true; addBoidOnEvent(e); });
  canvas.addEventListener("mouseup", e => { holding = false });
  canvas.addEventListener("mousemove", e => { if (holding) addBoidOnEvent(e); });

  function addBoidOnEvent(e){
    myBoids.add({
      angle: d3.randomUniform(-360, 360)(),
      pos: [e.pageX, e.pageY]
    });
  }

  addEventListener("resize", _ => {
    myBoids.width = innerWidth;
    myBoids.height = innerHeight;
    canvas.width = myBoids.width;
    canvas.height = myBoids.height;
    context.strokeStyle = "red";
    context.fillStyle = "pink";
  });

  const controls = document.querySelectorAll(".control");
  controls.forEach(control => {
    control.addEventListener("input", _ => {
      _.target.nextElementSibling.innerHTML = +_.target.value
      myBoids[_.target.dataset.parameter] = +_.target.value;
    });
  });

  </script>
</body>
</html>