Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel is required to process package imports. If you need a different preprocessor remove all packages first.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div id="flock"></div>
              
            
!

CSS

              
                html, body {
  margin: 0;
  padding: 0;
  background: #f5f5f5;
  color: #333;
  text-align: center;
  font-family: Arial, Helvetica, sans-serif;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

h1, h2, h3, h4 {
  font-weight: normal;
}

#flock {
  width: 100%;
  height: 100%;
  display: inline-block;
  margin: 0;
}

footer {
  position: fixed;
  bottom: 0;
  left: 0;
  width: 100%;
  text-align: center;
  color: #666;
  padding: 0.5em 1em;
}

a, a:link, a:focus, a:visited {
  color: #ccc;
  text-decoration: none;
}

a:hover {
  color: #fff;
}

              
            
!

JS

              
                declare var PIXI: any;
declare var Stats: any;

type Options = {
  containerId: string,
  boidLength: number,
  boidHeight: number,
  number: number,

  speed: number,

  cohesionRadius: number,
  separationRadius: number,
  alignmentRadius: number,
  predatorRadius: number,

  cohesionForce: number,
  separationForce: number,
  alignmentForce: number,
  predatorForce: number,
  obstacleForce: number,
};

type Position = {
  x: number,
  y: number,
};

class Renderer {
  private app: PIXI.Application;
  private container: PIXI.ParticleContainer;
  private boidTexture: PIXI.Texture;
  private stats: Stats;

  private boids: PIXI.Sprite[] = [];

  constructor(private options: Options) {
    this.app = new PIXI.Application({
      resizeTo: window,
      resolution: devicePixelRatio,
      autoDensity: true,
      backgroundColor: 0xf5f5f5,
    });

    this.container = new PIXI.ParticleContainer(this.options.number, {
      position: true,
      rotation: true,
      tint: true,
    });
    this.app.stage.addChild(this.container);

    // Prepare the boid texture for sprites
    const graphics = new PIXI.Graphics();
    graphics.beginFill(0xcccccc);
    graphics.lineStyle(0);
    graphics.drawPolygon([
      new PIXI.Point(this.options.boidLength / 2, this.options.boidHeight),
      new PIXI.Point(0, 0),
      new PIXI.Point(this.options.boidLength, 0),
    ]);
    graphics.endFill();
    const region = new PIXI.Rectangle(0, 0, options.boidLength, options.boidHeight);
    this.boidTexture = this.app.renderer.generateTexture(graphics, 1, 1, region);
    this.boidTexture.defaultAnchor.set(0.5, 0.5);

    // Render the app
    document.getElementById(this.options.containerId).appendChild(this.app.view);
  }

  public start() {
    const maxX = this.app.screen.width;
    const maxY = this.app.screen.height;
    for (let i = 0; i < this.options.number; i++) {
      const boid = new PIXI.Sprite(this.boidTexture);
      boid.x = Math.floor(Math.random() * maxX);
      boid.y = Math.floor(Math.random() * maxY);
      boid.pivot.set(this.options.boidLength / 2, this.options.boidHeight)
      boid.anchor.set(0.5, 0.5)
      boid.rotation = Math.random() * Math.PI * 2;
      this.container.addChild(boid);
      this.boids.push(boid);
    }

    // Listen for animate update
    this.app.ticker.add((delta) => {
      this.updateBoids(delta);
    });
  }

  private updateBoids(delta: number) {
    const maxX = this.app.screen.width;
    const maxY = this.app.screen.height;
    const children = this.boids.length;

    for (let i = 0; i < children; i++) {
      const boid = this.boids[i];

      // Forces that determine flocking
      let f_cohesion: number = 0;   // steer towards average position of neighbours (long range attraction)
      let f_separation: number = 0; // avoid crowding neighbours (short range repulsion)
      let f_alignment: number = 0;  // steer towards average heading of neighbours
      let f_predators: number = 0;  // avoid predators
      let f_obstacles: number = 0;  // avoid obstacles (same as predators but with less margin)

      // Find important neighbours
      const cohesionNeighbours: PIXI.Sprite[] = [];
      const separationNeighbours: PIXI.Sprite[] = [];
      const alignmentNeighbours: PIXI.Sprite[] = [];
      // const enemiesNear = [];

      // Iterate over the rest of the boids
      for (let a = 0; a < children; a++) {
        if (a === i) {
          continue;
        }
        const neighbour = this.boids[a];
        const d = Renderer.distance(boid, neighbour);

        if (d < this.options.separationRadius) {
          separationNeighbours.push(neighbour);
        }
        if (d < this.options.alignmentRadius) {
          alignmentNeighbours.push(neighbour);
        }
        if (d < this.options.cohesionRadius) {
          cohesionNeighbours.push(neighbour);
        }  
      }

      boid.tint = 0x333333;

      // Calculate forces
      if (separationNeighbours.length > 0) {
        f_separation = Renderer.getNeighboursRotation(separationNeighbours, boid) + Math.PI;
      }

      if (alignmentNeighbours.length > 0) {
        boid.tint = 0x9dd60b;
      }

      if (cohesionNeighbours.length + separationNeighbours.length + alignmentNeighbours.length < 1) {
        boid.tint = 0xcccccc;
      }

      if (alignmentNeighbours.length > 0) {
        f_alignment = Renderer.getNeighboursRotation(alignmentNeighbours, boid);
      }

      if (cohesionNeighbours.length > 0) {
        f_cohesion = Renderer.getNeighboursRotation(cohesionNeighbours, boid);
      }

      // set the mouse as an enemy
      const mouseCoords = this.app.renderer.plugins.interaction.mouse.global;
      const mouseDistance = Renderer.distance(mouseCoords, boid);
      if (mouseDistance < this.options.predatorRadius) {
        boid.tint = 0xeb0000;
        f_predators = Renderer.getRotation(mouseCoords.x, mouseCoords.y, boid) + Math.PI;
      }

      // REF:
      // https://github.com/rafinskipg/birds/blob/master/app/scripts/models/birdsGenerator.js

      // REF2
      // Reynolds, Craig (1987). "Flocks, herds and schools: A distributed behavioral model.". SIGGRAPH '87: Proceedings of the 14th annual conference on Computer graphics and interactive techniques. Association for Computing Machinery

      // Calculate the new direction of flight
      boid.rotation = boid.rotation + 
                      this.options.cohesionForce * f_cohesion / 100 + 
                      this.options.separationForce * f_separation / 100 + 
                      this.options.alignmentForce * f_alignment / 100 +
                      this.options.predatorForce * f_predators / 100 +
                      this.options.obstacleForce * f_obstacles / 100;

      // Now use the angle and the speed to calculate dx and dy
      const dx = Math.sin(boid.rotation) * this.options.speed;
      const dy = Math.cos(boid.rotation) * this.options.speed;

      boid.x -= dx * delta;
      boid.y += dy * delta;

      // Wrap around
      if (boid.x <= 0) {
        boid.x = maxX - 1;
      } else if (boid.x >= maxX) {
        boid.x = 1;
      }

      if (boid.y <= 0) {
        boid.y = maxY - 1;
      } else if (boid.y >= maxY) {
        boid.y = 1;
      }
    }
  }

  private static random(min: number, max: number) {
    return min + Math.random() * (max - min);
  }

  private static getNeighboursRotation(neighbours: Array<PIXI.Sprite>, boid: PIXI.Sprite) {
    if (neighbours.length < 1) {
      return 0;
    }

    // [meanX, meanY] is the center of mass of the neighbours
    const meanX = Renderer.arrayMean(neighbours, (boid: PIXI.Sprite) => boid.x);
    const meanY = Renderer.arrayMean(neighbours, (boid: PIXI.Sprite) => boid.y);

    return Renderer.getRotation(meanX, meanY, boid);
  }

  private static getRotation(meanX: number, meanY: number, boid: PIXI.Sprite) {
    // Vector from boid to mean neighbours
    const mean_dx = meanX - boid.x;
    const mean_dy = meanY - boid.y;

    // Diff between angle of the vector from boid to the mean neighbours and current direction
    return Math.atan2(mean_dy, mean_dx) - boid.rotation;
  }

  private static distance(p1: Position, p2: Position) {
    // Approximation by using octagons approach
  	const dx = Math.abs(p2.x - p1.x);
  	const dy = Math.abs(p2.y - p1.y);
  	return 1.426776695 * Math.min(0.7071067812 * (dx + dy), Math.max(dx, dy));	
  }

  private static arrayMean(arr: Array<any>, getKey: Function) {
    let result = 0;
    for (let i = 0; i < arr.length; i++) {
        result += getKey(arr[i]);
    }
    result /= arr.length;
    return result;
  }
}

const options: Options = {
  containerId: 'flock',
  boidLength: 5,
  boidHeight: 10,
  number: 75,

  speed: 3,

  cohesionRadius: 130,
  alignmentRadius: 25,
  separationRadius: 10,
  predatorRadius: 100,

  cohesionForce: 90,
  separationForce: 5,
  alignmentForce: 10,
  predatorForce: 60,
  obstacleForce: 20,
};

// Setup the Renderer
const renderer = new Renderer(options);

renderer.start();
              
            
!
999px

Console