phina.jsとProtonでパーティクルしてみる。その2「phina.jsと連携する」

前回の[『phina.jsとProtonでパーティクルしてみる。その1「Protonの使い方」』](https://qiita.com/matsu7089/items/dcb7d326e4ec1340eba6)の続きになります。
この記事を書いた後、[Proton](https://github.com/a-jie/Proton)のREADMEにリンクが貼られていました！少し貢献できた気がして嬉しいです。(どうやってこの記事みつけたのかな…)
Protonの開発者であるa-jieさん、ありがとうございます！

さて、今回はphina.jsでProtonを簡単に扱えるようなプラグインを作ってみました。
プラグインの使い方、オリジナルエミッターの作り方をまとめたいと思います。

# プラグインを読み込む

```html
<script src="https://rawgit.com/matsu7089/phina-proton/master/phina.proton.js"></script>
```

htmlに上の一行を追加するか、[ここ](https://rawgit.com/matsu7089/phina-proton/master/phina.proton.js)を右クリックして「リンク先を保存」して、お好きなように読み込んでください。

# プラグインの使い方
```javascript:main.js
var SCREEN_W = 320;
var SCREEN_H = 320;

phina.globalize();

phina.define('MainScene', {
  superClass: 'DisplayScene',
  init: function(options) {
    this.superInit(options);
    this.backgroundColor = 'black';

    var protonLayer = ProtonLayer({
      width: SCREEN_W,
      height: SCREEN_H
    }).setPosition(SCREEN_W/2, SCREEN_H/2).addChildTo(this);

    var emitter = ProtonEmitter({
      rate: new Proton.Rate(Proton.getSpan(10, 20), 0.1),
      initialize: [
        new Proton.Radius(1, 22),
        new Proton.Life(2, 4),
        new Proton.Velocity(2, Proton.getSpan(0, 360), 'polar')
      ],
      behaviour: [
        new Proton.Color('ff0000', 'random'),
        new Proton.Alpha(1, 0)
      ]
    }).setPosition(SCREEN_W/2, SCREEN_H/2);

    emitter.addChildTo(protonLayer).emit();
  }
});

phina.main(function() {
  var app = GameApp({
    startLabel: 'main',
    width: SCREEN_W,
    height: SCREEN_H,
    fps: 60
  });
  app.run();
});
```
プラグインを読み込むと`ProtonLayer`と`ProtonEmitter`が使えるようになります。(`phina.globalize()`してない時は、`phina.proton.`を前に付け加えてください)
では、使い方を簡単に説明していきます。

## ProtonLayer
```javascript
var protonLayer = ProtonLayer({
  width: SCREEN_W,
  height: SCREEN_H
}).setPosition(SCREEN_W/2, SCREEN_H/2).addChildTo(this);
```
`ProtonLayer`はProton描画専用のレイヤーです。
特にこだわりが無ければ、サンプルのように画面サイズと同じ`width`と`height`をオプションで渡して、画面の中心に移動させてあげると上手く動きます。

Proton本体が用意されていて、`ProtonEmitter`を子要素に追加するだけで表示してくれます。

## ProtonEmitter
```javascript
var emitter = ProtonEmitter({
  rate: new Proton.Rate(Proton.getSpan(10, 20), 0.1),
  initialize: [
    new Proton.Radius(1, 22),
    new Proton.Life(2, 4),
    new Proton.Velocity(2, Proton.getSpan(0, 360), 'polar')
  ],
  behaviour: [
    new Proton.Color('ff0000', 'random'),
    new Proton.Alpha(1, 0)
  ]
}).setPosition(SCREEN_W/2, SCREEN_H/2);
```
phina.jsっぽくエミッターを作れるようにしました。前回のプログラムと比べると、見やすくなりましたかね？
ご覧の通り`rate`、`initialize`、`behaviour`をオプションで渡す事が出来ます。

```javascript
var emitter = ProtonEmitter().setPosition(SCREEN_W/2, SCREEN_H/2);

emitter.rate = new Proton.Rate(Proton.getSpan(10, 20), 0.1);

var initializes = [
  new Proton.Radius(1, 22),
  new Proton.Life(2, 4),
  new Proton.Velocity(2, Proton.getSpan(0, 360), 'polar')
];

emitter
  .addInitialize(initializes)
  .addBehaviour(new Proton.Alpha(1, 0))
  .addBehaviour(new Proton.Color('ff0000', 'random'));
```
もちろん、オプションに渡さず後から追加することもできます。
`rate`は直接rateプロパティに渡してあげてください。
`initialize`、`behaviour`は`addInitialize()`、`addBehaviour()`を使って追加してあげてください。
どちらも上記のプログラムのinitializeのように配列でまとめて追加するか、behaviourのように1つずつ追加することができます。

他にもいくつかのメソッドが利用出来ます。

|method                      |description                 |
|:---------------------------|:---------------------------|
|emit()                      |パーティクルを放射      |
|stop()                      |放射をストップ          |
|removeAllParticles()        |全てのパーティクルを削除|
|removeInitialize(initialize)|指定のinitializeを削除  |
|removeAllInitializers()     |全てのinitializeを削除  |
|removeBehaviour(behaviour)  |指定のbehaviourを削除   |
|removeAllBehaviours()       |全てのbehaviourを削除   |

説明も書いてみましたが、メソッドの名前だけで大体分かりますね！w

# オリジナルエミッターを作る
さて、ここからは`ProtonEmitter`を拡張してオリジナルエミッターを作ってみましょう:sparkles:

### クリックでbehaviourを切り替える
```javascript:main.js
var SCREEN_W = 320;
var SCREEN_H = 320;

phina.globalize();

// MyEmitterを定義する
phina.define('MyEmitter', {
  superClass: 'ProtonEmitter',
  init: function() {
    this.superInit({
      rate: new Proton.Rate(Proton.getSpan(10, 20), 0.2),
      initialize: [
        new Proton.Radius(1, 10),
        new Proton.Life(2, 4),
        new Proton.Velocity(1, Proton.getSpan(0, 360), 'polar')
      ],
      behaviour: [
        new Proton.Alpha(1, 0),
        new Proton.Color(['e91e63', '2196f3', 'ffeb3b']),
        new Proton.CrossZone(new Proton.RectZone(0, 0, SCREEN_W, SCREEN_H), 'bound')
      ]
    });
    this.setPosition(SCREEN_W/2, SCREEN_H/3).emit();

    var center = new Proton.Vector2D(SCREEN_W/2, SCREEN_H/2);

    // 切り替えに使うbehaviourの配列
    this.behaviours = [
      new Proton.Attraction(center, 25, 150),
      new Proton.Collision(this.protonEmitter),
      new Proton.Force(30, 0),
      new Proton.Gravity(1),
      new Proton.GravityWell(center, 200),
      new Proton.RandomDrift(20, 0, .035),
      new Proton.Repulsion(center, 10, 150),
      new Proton.Scale(1, 3)
    ];

    this.index = 0;
    this.addBehaviour(this.behaviours[this.index]);
  },

  /**
   * behaviourを切り替える
   */ 
  changeBehaviour: function() {
    // 前のbehaviourを取り除く
    this.removeBehaviour(this.behaviours[this.index]);

    // 新しいbehaviourを追加する
    this.index = ++this.index % 8;
    this.addBehaviour(this.behaviours[this.index]);
  }
});

phina.define('MainScene', {
  superClass: 'DisplayScene',
  init: function(options) {
    this.superInit(options);
    this.backgroundColor = '#f0f0f0';

    var protonLayer = ProtonLayer({
      width: SCREEN_W,
      height: SCREEN_H
    }).setPosition(SCREEN_W/2, SCREEN_H/2).addChildTo(this);

    // MyEmitterをprotonLayerに追加する
    var emitter = MyEmitter().addChildTo(protonLayer);

    this.setInteractive(true);
    this.on('pointstart', function() {
      // 画面がクリックされた時にMyEmitterのchangeBehaviour()を呼ぶ
      emitter.changeBehaviour();
    });
  }
});

phina.main(function() {
  var app = GameApp({
    startLabel: 'main',
    width: SCREEN_W,
    height: SCREEN_H,
    fps: 60
  });
  app.run();
});
```

<script async src="https://static.codepen.io/assets/embed/ei.js"></script>
<p data-height="400" data-theme-id="0" data-slug-hash="pVqLPY" data-default-tab="result" data-user="matsu7089" data-embed-version="2" data-pen-title="my-emitter" data-preview="true" class="codepen">See the Pen <a href="https://codepen.io/matsu7089/pen/pVqLPY/">my-emitter</a> by matsu (<a href="https://codepen.io/matsu7089">@matsu7089</a>) on <a href="https://codepen.io">CodePen</a>.</p>

どうでしょう！簡単に実装することができました:sparkles:

エミッターの動きを切り替える必要がある時は、superInitのオプションには渡さず、後から追加する方が便利です。
ただ、今回は変える必要のないbehaviour(alpha, color, crosszone)があったので、それだけオプションに渡しています。

### 花火っぽい何か
```javascript:main.js
var SCREEN_W = 320;
var SCREEN_H = 320;

phina.globalize();

// FireworksEmitterを定義する
phina.define('FireworksEmitter', {
  superClass: 'ProtonEmitter',
  init: function() {
    this.superInit({
      rate: new Proton.Rate(Proton.getSpan(20, 30)),
      initialize: [
        new Proton.Radius(10, 15),
        new Proton.Life(2, 4),
      ],
      behaviour: [
        new Proton.Gravity(1.5),
        new Proton.Scale(1, 0)
      ]
    });

    // 使用する色のリスト
    this.colors = [
      '1abc9c', '2ecc71', '3498db', '9b59b6',
      'f1c40f', 'e67e22', 'e74c3c'
    ];

    // velocityとcolorをエミッターに追加する
    this.velocity = new Proton.Velocity();
    this.color = new Proton.Color();
    this.addInitialize(this.velocity).addBehaviour(this.color);

    // tweenerを利用して一定のタイミングで_emit()を呼ぶ
    this.tweener
      .call(function() { this._emit(); }, this).wait(100)
      .call(function() { this._emit(); }, this).wait(100)
      .call(function() { this._emit(); }, this).wait(1300)
      .setLoop(true);
  },

  _emit: function() {
    // 放射速度を変える
    this.velocity.reset(Math.random()+0.5, Proton.getSpan(0, 360), 'p');
    // 色を変える
    this.color.reset(this.colors.pickup());
    // 位置を変える
    this.setPosition(Math.random()*SCREEN_W, Math.random()*SCREEN_H/2);
    // 放射する間隔に関係なく、一度だけ放射する
    this.emit('once');
  }
});

phina.define('MainScene', {
  superClass: 'DisplayScene',
  init: function(options) {
    this.superInit(options);
    this.backgroundColor = '#f0f0f0';

    var protonLayer = ProtonLayer({
      width: SCREEN_W,
      height: SCREEN_H
    }).setPosition(SCREEN_W/2, SCREEN_H/2).addChildTo(this);

    // FireworksEmitterをprotonLayerに追加する
    var emitter = FireworksEmitter().addChildTo(protonLayer);
  }
});

phina.main(function() {
  var app = GameApp({
    startLabel: 'main',
    width: SCREEN_W,
    height: SCREEN_H,
    fps: 60
  });
  app.run();
});
```

<p data-height="400" data-theme-id="0" data-slug-hash="NMezxd" data-default-tab="result" data-user="matsu7089" data-embed-version="2" data-pen-title="fireworks-emitter" data-preview="true" class="codepen">See the Pen <a href="https://codepen.io/matsu7089/pen/NMezxd/">fireworks-emitter</a> by matsu (<a href="https://codepen.io/matsu7089">@matsu7089</a>) on <a href="https://codepen.io">CodePen</a>.</p>

どうでしょう！花火っぽい何かを作れました:sparkles:

phina.jsのtweenerを利用して一定のタイミングでパーティクルを放射しています。

```javascript
_emit: function() {
  // 放射速度を変える
  this.velocity.reset(Math.random()+0.5, Proton.getSpan(0, 360), 'p');
  // 色を変える
  this.color.reset(this.colors.pickup());
  // 位置を変える
  this.setPosition(Math.random()*SCREEN_W, Math.random()*SCREEN_H/2);
  // 放射する間隔に関係なく、一度だけ放射する
  this.emit('once');
}
```

この部分を少し説明します。
initializeとbehaviourには`reset()`が用意されていて、値を再設定することができます。
今回はこれを利用して放射毎に速度と色を変更しています。
ちなみに、色を変える所で`pickup()`というのがありますが、これはphina.jsが用意している便利メソッドで、配列の中からランダムに値を取り出してくれます。

`this.emit('once')`の所はコメント文にある通り、rateの放射間隔に関係無く一度だけ放射する事ができます。
また、`emit(秒数)`で「何秒間だけ放射する」ということができます。

オリジナルエミッター作りはここまで！

# さいごに
最後まで閲覧頂きありがとうございます。
phina.jsとProton、少しでも面白そうと思って頂けたら嬉しいです。

今のところ「その3」の予定はありませんが、エミッターのレシピが貯まったら記事にまとめるかもしれません。

では皆さん、楽しいphina.js&Protonライフを！:wave: