【pixi.js入門】サクッと雪を降らせてみる

# 前書き
pixi.jsでパーティクル系の表現をしているサイトをよく見るなぁと思い、
よくある何か降らせる系のものを作ってみようと思ったのが作り始めたきっかけです。
同じような誰かの助けになれば良いなと思いつつ、自分用の備忘録も兼ねて。

# 完成イメージ
こんな感じのものが出来上がります。
![image.gif](https://qiita-image-store.s3.amazonaws.com/0/266579/8a592c50-024a-442e-85b0-bec28557a681.gif)


# 準備
とりあえずpixi.jsが必要なのでpixi.jsをインストール。（npmの説明は省かせていただきます。）

```sh
npm install pixi.js
```

あとはwebpack、gulpなどを必要に応じてインストールして下さい。
（[webpackについてはこちらを参考にすると良いかもしれません](https://qiita.com/soarflat/items/28bf799f7e0335b68186)）

とりあえず必要なものは以上です。実際にコードを書いていきます。

# 雪を降らせよう
## HTML
### ベースのHTML
```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>pixi.js - snow</title>
    <style>
      * {
        padding: 0;
        margin: 0;
      }
      #pixi-snow-particle {
        width: 100vw;
        height: 100vh;
      }
    </style>
  </head>
  <body>
    <div id="pixi-snow-particle"></div>
  </body>
  <script src="path/to/js/vendor.bundle.js"></script>
  <script src="path/to/js/snow.bundle.js"></script>
</html>
```

`#pixi-snow-particle`が雪を降らせる対象のエリアになります。
全画面に雪を降らせたかったのでこんな感じでCSSを設定しました。
JSのファイル名、ディレクトリ等に関してはご自分の環境に合わせて変更して下さい。

### data属性値等を追加
このまま進めても良いですが、雪の量とか速度とか使う画像とか…
頻繁に変更したり時々によって変わる可能性があるものはHTMLに値を持たせようと思います。

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>pixijs - snow</title>
    <style>
      * {
        padding: 0;
        margin: 0;
      }
      #pixi-snow-particle {
        width: 100vw;
        height: 100vh;
      }
      #pixi-snow-particle img {
        display: none;
      }
    </style>
  </head>
  <body>
    <!-- 
      data-quantity: 雪の量
      data-**-size: 雪のサイズ
      data-**-speed: 雪が落ちる速度
     -->
    <div id="pixi-snow-particle" data-quantity="100" data-min-size="20" data-max-size="50" data-min-speed="400" data-max-speed="600">
      <img src="path/to/images/snow.png" alt="">
      <img src="path/to/images/snow2.png" alt="">
      <img src="path/to/images/snow3.png" alt="">
    </div>
  </body>
  <script src="path/to/js/vendor.bundle.js"></script>
  <script src="path/to/js/snow.bundle.js"></script>
</html>
```

ひとまずHTMLができたので次はJSに行きます。

## JS
### 必要な情報の取得
まずはHTMLに色々値を持たせたので、それらを取得してあげます。

```js:snow.js
class Snow {
  constructor() {
    // 雪を降らせる要素
    const $WRAPPER = document.getElementById('pixi-snow-particle');

    // 雪の画像
    const $IMAGE_ELEMENTS = $WRAPPER.querySelectorAll('img');
    const IMAGES = [];
    for (let index = 0; index < $IMAGE_ELEMENTS.length; index++) {
      const IMAGE_PATH = $IMAGE_ELEMENTS[index].getAttribute('src');
      IMAGES.push(IMAGE_PATH);
    }

    // 降らせる雪の量
    const QUANTITY = parseInt($WRAPPER.dataset.quantity, 10);

    // 雪のサイズ
    const MIN_SIZE = parseInt($WRAPPER.dataset.minSize, 10);
    const MAX_SIZE = parseInt($WRAPPER.dataset.maxSize, 10);
    const SIZE = { min: MIN_SIZE, max: MAX_SIZE };

    // 雪が落ちる速度
    const MIN_SPEED = parseInt($WRAPPER.dataset.minSpeed, 10);
    const MAX_SPEED = parseInt($WRAPPER.dataset.maxSpeed, 10);
    const SPEED = { min: MIN_SPEED, max: MAX_SPEED };
  }
}

new Snow();
```

### SnowParticleモジュールのimport
実際に雪を降らせる処理はモジュールとして分けようと思うので、
取得した値をそちらに渡してあげます。

```js:snow.js
import SnowParticle from './modules/SnowParticle';

class Snow {
  constructor() {

    /*
      中略（値取得処理）
    */

    new SnowParticle(
      $WRAPPER,
      IMAGES,
      QUANTITY,
      SIZE,
      SPEED
    );
  }
}

new Snow();
```

### pixi.jsのimportとconstructorの定義
ここから雪を降らせる処理です。
`pixi.js`を`import`して、`constructor`の引数として渡された値を受け取ります。
**（執筆時のpixi.jsのバージョンは4.8.1です。）**

```js:SnowParticle.js
import * as PIXI from 'pixi.js';

export default class SnowParticle {
  constructor($WRAPPER, IMAGES, QUANTITY, SIZE, SPEED) {
   
  }
}
```

### 雪が降る領域のサイズを取得
まずは渡された`$WRAPPER`を元に雪を降らせる領域のサイズを取得します。

```js:SnowParticle.js
// 雪を降らせる領域の高さ・幅を取得
const CONTENT_WIDTH = $WRAPPER.offsetWidth;
const CONTENT_HEIGHT = $WRAPPER.offsetHeight;
```

### 画像の読み込み
次にPIXI.loaderを使って画像を読み込みます。

```js:SnowParticle.js
// 画像をロード
PIXI.loader.add(IMAGES).load(() => {
  // 画像の読み込みが完了した後の処理
});
```

### canvasの生成
画像の読み込みが完了したら`canvas`要素を生成します。

```js:SnowParticle.js
// 親要素と同じサイズでCanvasを生成
const APP = new PIXI.Application(CONTENT_WIDTH, CONTENT_HEIGHT, {
  backgroundColor: 0x1099bb
});
$WRAPPER.appendChild(APP.view);
```

サイズ指定の引数として、最初に取得した`$WRAPPER`の高さと幅を渡しています。
背景色も指定できるので今回は涼しげな青系の色にしました。

これで雪を降らせる場所の土台が完成したので、そこに雪を追加していきます。

###配列とfor文の準備

まずは一つ一つの雪の情報を格納しておく配列と、
表示する雪の数分ループする`for`文を用意しましょう。

```js:SnowParticle.js
const PARTICLES = [];

for (let index = 0; index < QUANTITY; index++) {
  // 表示する雪の数分ループ
}
```

ここから`for`文の中で雪の情報を設定していきます。

### テクスチャの設定
雪として使用する画像を設定しましょう。

```js:SnowParticle.js
// 使用する画像の設定
const IMAGE_PATH = this.getImagePath(IMAGES);
const PARTICLE = new PIXI.Sprite.fromImage(IMAGE_PATH);
```

`getImagePath()`で`IMAGES`配列の中からランダムに画像のパスを受け取り、
それを`PIXI.Sprite`オブジェクトのテクスチャとして使用するようにしています。
`getImagePath()`の処理はこんな感じです。

```js:SnowParticle.js
getImagePath(IMAGES) {
  const MAX = IMAGES.length;
  const INDEX = Math.floor(Math.random() * MAX);
  return IMAGES[INDEX];
}
```

### 原点の設定
次に雪の原点を設定してあげます。

```js:SnowParticle.js
// 原点を中心に設定
PARTICLE.anchor.set(0.5);
```

`anchor`が(0,0)で左上、(1,1)で右下が原点になります。デフォルトは(0,0)です。
ここの設定は人次第ですが、中心にしておいた方が何かと計算しやすいと思うので今回は中心の0.5にしています。

### サイズの設定
次は雪のサイズの設定です。

```js:SnowParticle.js
// サイズをランダムに設定
const PARTICLE_SIZE = this.getRandomInt(SIZE.min, SIZE.max);
PARTICLE.width = PARTICLE_SIZE;
PARTICLE.height = PARTICLE_SIZE;
```

雪が全部同じサイズなのも変なので、ランダムなサイズになるようにします。
ランダムな整数が欲しい場面は多そうなので、
最小値と最大値を渡したらその間でランダムな整数を返してくれる`getRandomInt()`を準備しました。
今回は使用しませんでしたが、[underscoreのrandom関数](https://underscorejs.org/#random)を使っても良いと思います。

一応`getRandomInt()`の処理はこんな感じです。

```js:SnowParticle.js
getRandomInt(MIN, MAX) {
  return Math.floor(Math.random() * (MAX + 1 - MIN)) + MIN;
}
```

### 初期位置の設定
次に雪の初期位置を設定します。
固定の値だと全ての雪が同じ場所から始まってしまうのでこれもランダムな位置に配置します。

```js:SnowParticle.js
// 雪のX座標、Y座標をランダムに配置
PARTICLE.x = Math.random() * APP.screen.width;
PARTICLE.y = Math.random() * APP.screen.height;
```

### 雪が落ちる速度の設定
次に雪が落ちる速度を設定します。
落ちる速度が同じなのもの変なので、これもランダムにします。

```js:SnowParticle.js
// 落ちていくスピードをランダムに設定
const PARTICLE_SPEED = this.getRandomInt(SPEED.min, SPEED.max);
PARTICLE.speed = (PARTICLE_SPEED + Math.random() * 0.5) * 0.5;
```

### 配列とcanvasへ雪を追加
ここまでの設定が完了したら、
用意してあげた配列とcanvas上に雪の情報を追加してあげましょう。

```js:SnowParticle.js
PARTICLES.push(PARTICLE);
APP.stage.addChild(PARTICLE);
```

これで`for`文の中で実行する雪の情報設定は終わりです。
この状態でページにアクセスすると、
静止した雪が指定の領域内に散らばって表示され、更新する度に位置やサイズがランダムに変わるはずです。

### 雪を落下させる
では止まっている雪を降らしていきましょう。
`for`文の後に雪を落下させる処理を追加します。

```js:SnowParticle.js
APP.ticker.add(() => {
  for (let index = 0; index < PARTICLES.length; index++) {
    // 配列からデータ取得
    const PARTICLE = PARTICLES[index];

    // 縦の位置を更新
    PARTICLE.y += (PARTICLE.height / 5000) * PARTICLE.speed;

    // 雪を回転
    PARTICLE.rotation += 0.01;

    // 画面の一番下に行った時縦の位置をリセット、横の位置をランダムに配置
    if (PARTICLE.y > CONTENT_HEIGHT + PARTICLE.height) {
      PARTICLE.y = -PARTICLE.height;
      PARTICLE.x = Math.random() * APP.screen.width;
    }
  }
});
```

`ticker`は`requestAnimationFrame`みたいなものです。
（というか内部的に`requestAnimationFrame`を使っている、はず。）

呼び出される度にそれぞれの雪の縦位置をspeedの値に応じて更新していき、
一番下に着いたら一番上に戻して横の位置をランダムに再配置しています。

これで雪を降らせることができました！

# もう少しリアルにしたい
確かに雪は降っていますが、今の状態だと垂直落下しているだけです。
もう少し雪っぽくしてあげたいので、一手間加えて揺れの動きを追加してあげましょう。

## 追加で必要なもの
`ticker`の中で計算して滑らかな動きをつけるのは骨が折れるので、`pixi-ease`をインストールします。
**（執筆時のpixi-easeのバージョンは1.1.1です。）**

```sh
npm install pixi-ease
```

## HTML
### 値の追加
雪が揺れる幅と揺れの速度の値を追加します。

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>pixijs - snow</title>
    <style>
      * {
        padding: 0;
        margin: 0;
      }
      #pixi-snow-particle {
        width: 100vw;
        height: 100vh;
      }
      #pixi-snow-particle img {
        display: none;
      }
    </style>
  </head>
  <body>
    <!--
      data-quantity: 雪の量
      data-**-size: 雪のサイズ
      data-**-speed: 雪が落ちる速度
      data-**-swing: 雪の揺れ幅
      data-**-swing-duration: 揺れの速度
     -->
    <div id="pixi-snow-particle" data-quantity="100" data-min-size="20" data-max-size="50" data-min-speed="400" data-max-speed="600" data-min-swing="-80" data-max-swing="80" data-min-swing-duration="3000" data-max-swing-duration="5000">
      <img src="path/to/images/snow.png" alt="">
      <img src="path/to/images/snow2.png" alt="">
      <img src="path/to/images/snow3.png" alt="">
    </div>
  </body>
  <script src="path/to/js/vendor.bundle.js"></script>
  <script src="path/to/js/snow.bundle.js" charset="utf-8"></script>
</html>
```

## JS
### 値取得処理の追加
HTMLに値を追加した分、値の取得処理を追加し、`SnowParticle`に渡してあげます。

```js:snow.js
import SnowParticle from './modules/SnowParticle';

class Snow {
  constructor() {

    /*
      中略（今までの値取得処理）
    */

    // 雪の揺れ幅
    const MIN_SWING = parseInt($WRAPPER.dataset.minSwing, 10);
    const MAX_SWING = parseInt($WRAPPER.dataset.maxSwing, 10);
    const SWING = { min: MIN_SWING, max: MAX_SWING };

    // 雪の揺れる速度
    const MIN_SWING_DURATION = parseInt($WRAPPER.dataset.minSwingDuration, 10);
    const MAX_SWING_DURATION = parseInt($WRAPPER.dataset.maxSwingDuration, 10);
    const SWING_DURATION = { min: MIN_SWING_DURATION, max: MAX_SWING_DURATION };

    new SnowParticle(
      $WRAPPER,
      IMAGES,
      QUANTITY,
      SIZE,
      SPEED,
      SWING,
      SWING_DURATION
    );
  }
}

new Snow();
```

### pixi-easeのimportと引数の追加
`SnowParticle`では、
まず`pixi-ease`を`import`し、値を追加した分の引数を追加してあげます。

```js:SnowParticle.js
import * as PIXI from 'pixi.js';
import Ease from 'pixi-ease';

export default class SnowParticle {
  constructor($WRAPPER, IMAGES, QUANTITY, SIZE, SPEED, SWING, SWING_DURATION) {
   
  }
}
```

### 揺れ幅と揺れの速度の設定
次は揺れのアニメーションに関する設定です。
雪の初期位置を設定した後に処理を書いていきます。

まずは揺れ幅と揺れの速度をランダムに決めます。

```js:SnowParticle.js
// 揺れ幅を設定
const SWING_WIDTH = PARTICLE.x - this.getRandomInt(SWING.min, SWING.max);

// 揺れの速度を設定
const DURATION = this.getRandomInt(
  SWING_DURATION.min,
  SWING_DURATION.max
);
```

### 揺れのアニメーションを付ける
あとはその値を元に、`pixi-ease`で揺れのアニメーションを付けてあげるだけです。

```js:SnowParticle.js
const EASE_LIST = new Ease.list();
EASE_LIST.to(PARTICLE, { x: SWING_WIDTH }, DURATION, {
  ease: 'easeInOutSine',
  repeat: true,
  reverse: true
});
```

`pixi-ease`の使い方については
[こちら（githubページ）](https://github.com/davidfig/pixi-ease#readme)か[こちら（デモページ）](https://davidfig.github.io/pixi-ease/)を参考にしてみてください。

これで雪が揺れながら落ちていくようになったと思います。

# 全体ソースコード
最後に全体のソースコードを載せておきます。

## HTML

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>pixijs - snow</title>
    <style>
      * {
        padding: 0;
        margin: 0;
      }
      #pixi-snow-particle {
        width: 100vw;
        height: 100vh;
      }
      #pixi-snow-particle img {
        display: none;
      }
    </style>
  </head>
  <body>
    <!--
      data-quantity: 雪の量
      data-**-size: 雪のサイズ
      data-**-speed: 雪が落ちる速度
      data-**-swing: 雪の揺れ幅
      data-**-swing-duration: 揺れの速度
     -->
    <div id="pixi-snow-particle" data-quantity="100" data-min-size="20" data-max-size="50" data-min-speed="400" data-max-speed="600" data-min-swing="-80" data-max-swing="80" data-min-swing-duration="3000" data-max-swing-duration="5000">
      <img src="path/to/images/snow.png" alt="">
      <img src="path/to/images/snow2.png" alt="">
      <img src="path/to/images/snow3.png" alt="">
    </div>
  </body>
  <script src="path/to/js/vendor.bundle.js"></script>
  <script src="path/to/js/snow.bundle.js"></script>
</html>
```

## JS

```js:snow.js
import SnowParticle from './modules/SnowParticle';

class Snow {
  constructor() {
    // 雪を降らせる要素
    const $WRAPPER = document.getElementById('pixi-snow-particle');

    // 雪の画像
    const $IMAGE_ELEMENTS = $WRAPPER.querySelectorAll('img');
    const IMAGES = [];
    for (let index = 0; index < $IMAGE_ELEMENTS.length; index++) {
      const IMAGE_PATH = $IMAGE_ELEMENTS[index].getAttribute('src');
      IMAGES.push(IMAGE_PATH);
    }

    // 降らせる雪の数
    const QUANTITY = parseInt($WRAPPER.dataset.quantity, 10);

    // 雪のサイズ
    const MIN_SIZE = parseInt($WRAPPER.dataset.minSize, 10);
    const MAX_SIZE = parseInt($WRAPPER.dataset.maxSize, 10);
    const SIZE = { min: MIN_SIZE, max: MAX_SIZE };

    // 雪が落ちる速度
    const MIN_SPEED = parseInt($WRAPPER.dataset.minSpeed, 10);
    const MAX_SPEED = parseInt($WRAPPER.dataset.maxSpeed, 10);
    const SPEED = { min: MIN_SPEED, max: MAX_SPEED };

    // 雪の揺れ幅
    const MIN_SWING = parseInt($WRAPPER.dataset.minSwing, 10);
    const MAX_SWING = parseInt($WRAPPER.dataset.maxSwing, 10);
    const SWING = { min: MIN_SWING, max: MAX_SWING };

    // 雪の揺れる速度
    const MIN_SWING_DURATION = parseInt($WRAPPER.dataset.minSwingDuration, 10);
    const MAX_SWING_DURATION = parseInt($WRAPPER.dataset.maxSwingDuration, 10);
    const SWING_DURATION = { min: MIN_SWING_DURATION, max: MAX_SWING_DURATION };

    new SnowParticle(
      $WRAPPER,
      IMAGES,
      QUANTITY,
      SIZE,
      SPEED,
      SWING,
      SWING_DURATION
    );
  }
}

new Snow();
```

```js:SnowParticle.js
import * as PIXI from 'pixi.js';
import Ease from 'pixi-ease';

export default class SnowParticle {
  constructor($WRAPPER, IMAGES, QUANTITY, SIZE, SPEED, SWING, SWING_DURATION) {
    // 雪を降らせる領域の高さ・幅を取得
    const CONTENT_WIDTH = $WRAPPER.offsetWidth;
    const CONTENT_HEIGHT = $WRAPPER.offsetHeight;

    // 画像をロード
    PIXI.loader.add(IMAGES).load(() => {
      // 親要素と同じサイズでCanvasを生成
      const APP = new PIXI.Application(CONTENT_WIDTH, CONTENT_HEIGHT, {
        backgroundColor: 0x1099bb
      });
      $WRAPPER.appendChild(APP.view);

      const PARTICLES = [];

      // 表示する雪の数分ループ
      for (let index = 0; index < QUANTITY; index++) {
        // 使用する画像の設定
        const IMAGE_PATH = this.getImagePath(IMAGES);
        const PARTICLE = new PIXI.Sprite.fromImage(IMAGE_PATH);

        // 原点を中心に設定
        PARTICLE.anchor.set(0.5);

        // サイズをランダムに設定
        const PARTICLE_SIZE = this.getRandomInt(SIZE.min, SIZE.max);
        PARTICLE.width = PARTICLE_SIZE;
        PARTICLE.height = PARTICLE_SIZE;

        // 雪のX座標、Y座標をランダムに配置
        PARTICLE.x = Math.random() * APP.screen.width;
        PARTICLE.y = Math.random() * APP.screen.height;

        // 揺れ幅を設定
        const SWING_WIDTH = PARTICLE.x - this.getRandomInt(SWING.min, SWING.max);

        // 揺れの速度を設定
        const DURATION = this.getRandomInt(
          SWING_DURATION.min,
          SWING_DURATION.max
        );

        const EASE_LIST = new Ease.list();
        EASE_LIST.to(PARTICLE, { x: SWING_WIDTH }, DURATION, {
          ease: 'easeInOutSine',
          repeat: true,
          reverse: true
        });

        // 落ちていくスピードをランダムに設定
        const PARTICLE_SPEED = this.getRandomInt(SPEED.min, SPEED.max);
        PARTICLE.speed = (PARTICLE_SPEED + Math.random() * 0.5) * 0.5;

        PARTICLES.push(PARTICLE);
        APP.stage.addChild(PARTICLE);
      }

      APP.ticker.add(() => {
        for (let index = 0; index < PARTICLES.length; index++) {
          // 配列からデータ取得
          const PARTICLE = PARTICLES[index];

          // 縦の位置を更新
          PARTICLE.y += (PARTICLE.height / 5000) * PARTICLE.speed;

          // 雪を回転
          PARTICLE.rotation += 0.01;

          // 画面の一番下に行った時縦の位置をリセット、横の位置をランダムに配置
          if (PARTICLE.y > CONTENT_HEIGHT + PARTICLE.height) {
            PARTICLE.y = -PARTICLE.height;
            PARTICLE.x = Math.random() * APP.screen.width;
          }
        }
      });
    });
  }

  /**
   * 画像のパスを返す
   * @param {Object} IMAGES
   * @returns {string} 画像のパス
   */
  getImagePath(IMAGES) {
    const MAX = IMAGES.length;
    const INDEX = Math.floor(Math.random() * MAX);
    return IMAGES[INDEX];
  }

  /**
   * ランダムな整数を返す
   * @param {number} MIN
   * @param {number} MAX
   * @returns {number} ランダムな整数
   */
  getRandomInt(MIN, MAX) {
    return Math.floor(Math.random() * (MAX + 1 - MIN)) + MIN;
  }
}
```

# あとがき
色々と参考になる記事はあってもバージョンが違って今と書き方が違う！
なんてことが多かったので、グーグル翻訳に頼りきりな自分としては少し辛い部分もありましたが
[公式ドキュメント](http://pixijs.download/dev/docs/index.html)も読みつつで何とかなりました。

画像やアニメーション等調整すれば、色々と使い回しが効きそうです。