【Pixi連載#2】Pixi.jsでパーティクルアニメーションを作る

Pixiシリーズ2回目です！
前回の[Pixi製スライダー](https://qiita.com/AVELWP/items/a15eaabf03baec9cbe19)に引き続き、今回はPixi.jsを使ってパーティクルアニメーションを作っていきます。

## Pixi.jsでパーティクルアニメーションを作る

### 今回作るもの（完成版）
![particle.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101111/f3265d81-e899-db5e-e11a-0cb3fe135d8d.gif)


今回作るパーティクルアニメーションの完成版です。
各パーティクルがうようよランダムに浮遊しているような動きをしつつ、マウスを近づけるとマウスに追従してくるような動きを目指していきます。
また、それ以外にも色や大きさ・スピードなどもそれぞれのパーティクルで別々の動きをするように実装します。

- 完成版ソースは以下  
[https://github.com/horizun-horizon/pixi-particle/blob/master/index.html](https://github.com/horizun-horizon/pixi-particle/blob/master/index.html)

### 0. ライブラリ・プラグインの読み込み
まずは以下のライブラリ・プラグインを読み込みます。
今回はPixi.js本体だけの読み込みです。

```html
<!-- Pixi.js本体 -->
<script src="//cdnjs.cloudflare.com/ajax/libs/pixi.js/5.0.3/pixi.min.js"></script>
```

### 1. canvas追加
次に、前回と同様にcanvasを指定の要素に追加するために`new PIXI.Application`を宣言します。

```javascript
const Particle = {
  init: function(options) {
    this.options = options;
    this.colors = ["0xE5493F","0x55C1FF","0x26B9A0", "0x5A52FF"]
    this.particles = [];
    this.maxTime = this.options.frequency * this.options.totalParticles;
    this.timeToRecreate = false;
    this.mouse = {
      x: undefined,
      y: undefined,
    };

    this.canvas = document.getElementById(this.options.targetId);
    this.app = new PIXI.Application({
      width: this.options.width,
      height: this.options.height,
      backgroundColor: '#111111',
      antialias: true,
    });
    this.canvas.appendChild(this.app.view);

    this.container = new PIXI.Container();
    this.container.interactive = true;
    this.app.stage.addChild(this.container);
  },
};

Particle.init({
  targetId: 'canvas', // canvasを追加するid
  width: 900, // canvasの幅
  height: 540, // canvasの高さ
  totalParticles: 150, // パーティクルの総数
  frequency: 100, // パーティクルを追加する頻度
});
```

### 2. 一定時間経過後にパーティクルを描画して生成
今回作るパーティクルは、完成版の画像ではわかりませんがcanvas中央あたりから一つづつパーティクルが現れる作りにしていきます。
そのため、`setTimeout`を使って描画と生成をしていきます。
また、今回パーティクルを作るために`new PIXI.ParticleContainer`を使っていますが、`new PIXI.Graphics`をそのまま追加しようとするとエラーが返ってきてしまいます。内容を見るとどうやらPixiの仕様でSpriteに変換してくないといけないみたいなので、そのような処理を`draw`関数内で記述しています。

```javascript
const Particle = {
  init: function(options) {
    // 〜省略
    this.generate(this.options.totalParticles);
  },
  generate: function(num) {
    for (let i = 0; i < num; i++) {
      setTimeout(() => {
        this.draw(); // 一定時間後に描画処理を走らせる
      }, this.options.frequency * i);
    }
  },
  draw: function() {
    // 引数で色情報をランダムで渡し、別々の色情報を持ったパーティクルを貰います。
    const graphic = this.particleGraphic(this.colors[this.randomIntFromInterval(0, this.colors.length - 1)]);
    // 以下2行はSpriteに変換する処理
    const texture = this.app.renderer.generateTexture(graphic);
    const spriteParticle = new PIXI.Sprite(texture);
    // 以下オプション
    spriteParticle.anchor.set(0.5);
    const size = 0.05 + Math.random() * 0.1;
    spriteParticle.alpha = Math.random();
    spriteParticle.scale.x = size;
    spriteParticle.scale.y = size;
    // パーティクル出現位置を画面中央付近に
    spriteParticle.x = (this.options.width / 2) - ((Math.random() * 200) -  (Math.random() * 200));
    spriteParticle.y = (this.options.height / 2) - ((Math.random() * 200) -  (Math.random() * 200));
    spriteParticle.a = 0;
    spriteParticle.s = ((1 + (8 * Math.random())) + (Math.random() * 1))/10;
    // サイズが小さいパーティクルのスピードを速く、大きいパーティクルのスピードを遅く
    spriteParticle.speed = (Math.floor( Math.random() * 4 ) + 3) / Math.floor((size * 100));
    this.particles.push(spriteParticle);

    // パーティクルコンテナの追加
    this.particleContainer = new PIXI.ParticleContainer(
      this.options.totalParticle,
      {
        scale: true,
        position: true,
      }
    );
    this.particleContainer.addChild(spriteParticle);
    this.container.addChild(this.particleContainer);
  },
  particleGraphic: function(type) {
    // パーティクル描画
    const graphic = new PIXI.Graphics();
    graphic.lineStyle(20, type, 10);
    graphic.beginFill(0x111111, 0.7);
    graphic.drawCircle(0, 0, 50);
    graphic.endFill();
    return graphic;
  },
  randomIntFromInterval(min,max){
    return Math.floor(Math.random() * (max - min + 1) + min);
  },
};
```

### 3. パーティクルの動きづけ
パーティクルの生成ができたら、いよいよ動きをつけていきます。
マウスに反応する動きもここで指定します。

```javascript
const Particle = {
  init: function(options) {
    // 〜省略
    // コンテナにマウスイベントを追加
    this.container.on('mousemove', e => this.mouseAction(e));

    setTimeout(() => {
      this.timeToRecreate = true;
    }, this.maxTime);
    this.generate(this.options.totalParticles);
    // 更新関数
    this.update();
  },
  move: function() {
    for(let i = 0; i < this.particles.length; i++) {
      const particle = this.particles[i];
      // 三角関数を使ってランダムな動きをさせる
      particle.x += Math.cos(particle.a) * particle.s * particle.speed;
      particle.y += Math.sin(particle.a) * particle.s * particle.speed;
      particle.a += Math.random() * 0.4 - 0.2;
      // 画面外に出た時の処理
      if(particle.x > this.options.width + 10) {
        particle.x = -5;
      } else if(particle.x < 0 - 10) {
        particle.x = this.options.width + 5;
      } else if(particle.y > this.options.height + 10) {
        particle.y = -5;
      } else if(particle.y < 0 - 10) {
        particle.y = this.options.height + 5;
      }
      // マウスの近くにあるパーティクルはマウスに近寄ってくる
      if (this.mouse.x - particle.x < 80 && this.mouse.x - particle.x > -80
      && this.mouse.y - particle.y < 80 && this.mouse.y - particle.y > -80) {
        particle.x += (this.mouse.x - particle.x) * particle.speed * 0.02;
        particle.y += (this.mouse.y - particle.y) * particle.speed * 0.02;
      }
    }
  },
  update: function() {
    this.move();
    if (this.timeToRecreate) {
      if (this.particles.length < this.options.totalParticles) {
        // パーティクルが全て生成し終わったら、一定時間ごとにまた生成する
        this.generate(1);
      }
    }
    requestAnimationFrame(this.update.bind(this));
  },
  mouseAction: function(e) {
    // マウスの位置を取得
    this.mouse.x = e.data.global.x;
    this.mouse.y = e.data.global.y;
  },
};
```

## 終わりに
いかがでしたか？意外と簡単にパーティクルが作れました。
パーティクルは一見動きが複雑に見えるため実装も難しく感じてしまいますが、仕組みさえ理解してしまえばそこまで量も多くない記述で実装できてしまいます。
また、一番のポイントは色や大きさ・スピードなどにどんな値を持たせるかで、どんな見栄え・印象になるかが変わってきますので色んな値を入れて試してみてください。

次はPixiを使ってどんな実装をしていこうかしら。
では、また〜

- 完成版ソースは以下  
[https://github.com/horizun-horizon/pixi-particle/blob/master/index.html](https://github.com/horizun-horizon/pixi-particle/blob/master/index.html)