p5.js の複雑めのアニメーション公式サンプル Flocking を obniz ディスプレイで動かすメモ

p5.js の複雑めのアニメーション公式サンプル Flocking を obniz ディスプレイで動かすメモです。

このように動きます

動作環境

  • Windows 10
  • Chrome ブラウザから obniz クラウドで実行

プログラム

p5.js の canvas 要素を渡して obniz ディスプレイでアニメーションを表示させるメモ

p5.js の複雑めのアニメーション公式サンプル Flocking をベースに、p5.js の canvas の動きをobniz ディスプレイで動かしたこちらのナレッジを活用していきます。

実際に出来上がったプログラムがこちらです。

<html>

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" />
  <script src="https://obniz.io/js/jquery-3.2.1.min.js"></script>
  <script src="https://unpkg.com/obniz@3.25.0/obniz.js" crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.min.js" integrity="sha512-3RlxD1bW34eFKPwj9gUXEWtdSMC59QqIqHnD8O/NoTwSJhgxRizdcFVQhUMFyTp5RwLTDL0Lbcqtl8b7bFAzog==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>

<body>
  <script>

    let cnv;

    // https://p5js.org/examples/hello-p5-flocking.html
    // ラインがループするサンプルを改造

    let boids = [];

    function setup() {
      cnv = createCanvas(128, 64);

      frameRate(10);

      // Add an initial set of boids into the system
      for (let i = 0; i < 50; i++) {
        boids[i] = new Boid(random(width), random(height));
      }
    }

    function draw() {
      background(0);
      // Run all the boids
      for (let i = 0; i < boids.length; i++) {
        boids[i].run(boids);
      }
    }

    // Boid class
    // Methods for Separation, Cohesion, Alignment added
    class Boid {
      constructor(x, y) {
        this.acceleration = createVector(0, 0);
        this.velocity = p5.Vector.random2D();
        this.position = createVector(x, y);
        this.r = 0.5;
        this.maxspeed = 5;    // Maximum speed
        this.maxforce = 0.05; // Maximum steering force
      }

      run(boids) {
        this.flock(boids);
        this.update();
        this.borders();
        this.render();
      }
      
      // Forces go into acceleration
      applyForce(force) {
        this.acceleration.add(force);
      }
      
      // We accumulate a new acceleration each time based on three rules
      flock(boids) {
        let sep = this.separate(boids); // Separation
        let ali = this.align(boids);    // Alignment
        let coh = this.cohesion(boids); // Cohesion
        // Arbitrarily weight these forces
        sep.mult(2.5);
        ali.mult(1.0);
        coh.mult(1.0);
        // Add the force vectors to acceleration
        this.applyForce(sep);
        this.applyForce(ali);
        this.applyForce(coh);
      }
      
      // Method to update location
      update() {
        // Update velocity
        this.velocity.add(this.acceleration);
        // Limit speed
        this.velocity.limit(this.maxspeed);
        this.position.add(this.velocity);
        // Reset acceleration to 0 each cycle
        this.acceleration.mult(0);
      }
      
      // A method that calculates and applies a steering force towards a target
      // STEER = DESIRED MINUS VELOCITY
      seek(target) {
        let desired = p5.Vector.sub(target, this.position); // A vector pointing from the location to the target
        // Normalize desired and scale to maximum speed
        desired.normalize();
        desired.mult(this.maxspeed);
        // Steering = Desired minus Velocity
        let steer = p5.Vector.sub(desired, this.velocity);
        steer.limit(this.maxforce); // Limit to maximum steering force
        return steer;
      }
      
      // Draw boid as a circle
      render() {
        fill(0, 0);
        stroke(500);
        const size = 10 + Math.floor(Math.random() * 6);
        ellipse(this.position.x, this.position.y, size, size);
      }
      
      // Wraparound
      borders() {
        if (this.position.x < -this.r) this.position.x = width + this.r;
        if (this.position.y < -this.r) this.position.y = height + this.r;
        if (this.position.x > width + this.r) this.position.x = -this.r;
        if (this.position.y > height + this.r) this.position.y = -this.r;
      }
      
      // Separation
      // Method checks for nearby boids and steers away
      separate(boids) {
        let desiredseparation = 25.0;
        let steer = createVector(0, 0);
        let count = 0;
        // For every boid in the system, check if it's too close
        for (let i = 0; i < boids.length; i++) {
          let d = p5.Vector.dist(this.position, boids[i].position);
          // If the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself)
          if ((d > 0) && (d < desiredseparation)) {
            // Calculate vector pointing away from neighbor
            let diff = p5.Vector.sub(this.position, boids[i].position);
            diff.normalize();
            diff.div(d); // Weight by distance
            steer.add(diff);
            count++; // Keep track of how many
          }
        }
        // Average -- divide by how many
        if (count > 0) {
          steer.div(count);
        }
      
        // As long as the vector is greater than 0
        if (steer.mag() > 0) {
          // Implement Reynolds: Steering = Desired - Velocity
          steer.normalize();
          steer.mult(this.maxspeed);
          steer.sub(this.velocity);
          steer.limit(this.maxforce);
        }
        return steer;
      }
      
      // Alignment
      // For every nearby boid in the system, calculate the average velocity
      align(boids) {
        let neighbordist = 50;
        let sum = createVector(0, 0);
        let count = 0;
        for (let i = 0; i < boids.length; i++) {
          let d = p5.Vector.dist(this.position, boids[i].position);
          if ((d > 0) && (d < neighbordist)) {
            sum.add(boids[i].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;
        } else {
          return createVector(0, 0);
        }
      }
      
      // Cohesion
      // For the average location (i.e. center) of all nearby boids, calculate steering vector towards that location
      cohesion(boids) {
        let neighbordist = 50;
        let sum = createVector(0, 0); // Start with empty vector to accumulate all locations
        let count = 0;
        for (let i = 0; i < boids.length; i++) {
          let d = p5.Vector.dist(this.position, boids[i].position);
          if ((d > 0) && (d < neighbordist)) {
            sum.add(boids[i].position); // Add location
            count++;
          }
        }
        if (count > 0) {
          sum.div(count);
          return this.seek(sum); // Steer towards the location
        } else {
          return createVector(0, 0);
        }
      }  
    }




    // 今回の obniz を指示するための設定
    const obniz = new Obniz("OBNIZ_ID_HERE");

    obniz.onconnect = async function () {
      obniz.display.clear();
      obniz.display.print("p5.js");

      console.log(cnv);

      obniz.onloop = async function(){
        // この形で canvas の getContext('2d') 的な値を渡せる
        const ctx = cnv.drawingContext;
        obniz.display.draw(ctx);
      }
      
    }
  </script>
</body>

</html>

対応としては、そんなに多くなく以下の通りです。

    function setup() {
      cnv = createCanvas(128, 64);

      frameRate(10);

setup のところで、fps 10 、obniz Board の画面サイズ 128 * 64 の対応をしています。

      // Add an initial set of boids into the system
      for (let i = 0; i < 50; i++) {
        boids[i] = new Boid(random(width), random(height));
      }

泡の出る量を 100 から 50 に変更しました。

    function draw() {
      background(0);

背景をグレーから真っ黒にして白黒を鮮明に。

      constructor(x, y) {
        this.acceleration = createVector(0, 0);
        this.velocity = p5.Vector.random2D();
        this.position = createVector(x, y);
        this.r = 0.5;
        this.maxspeed = 5;    // Maximum speed
        this.maxforce = 0.05; // Maximum steering force
      }

this.r で円の大きさを 3.0 から 0.5 にしてディスプレイに収まりやすいよう小さめに。this.maxspeed を 3 から 5 にして少し早めに動くように。

      // Draw boid as a circle
      render() {
        fill(0, 0);
        stroke(500);
        const size = 10 + Math.floor(Math.random() * 6);

render のところで fill を調整して127,127 から 0,0 にしてより白黒に調整。stroke は 200 から 500 にして線を太めに出しました。const size のところで、少しサイズランダムをいれて泡っぽくしています。

fps10 くらいでも、PC で描画されている雰囲気とかなり近い形で動きますね。ありがたいです。