Source: AfterImage.js

AfterImage.js

/* global phina:false */

const DEFAULT_PARAMS = {
  interval: 4,
  poolNum: 22,
  effectProps: { alpha: 0 },
  effectDuration: 800,
  effectEasing: 'easeOutCirc',
}

/**
 * 対象のSpriteもしくはShapeオブジェクトに残像エフェクトを付与します。(ただしShapeは不完全)<br>
 * 残像にfilterをかけることもできます。
 * @class phina.accessory.AfterImage
 * @memberOf phina.accessory
 * @extends phina.accessory.Accessory
 *
 * @example
 * const player = phina.display.Sprite('player').addChildTo(this);
 * const filterFunc = (pixel, i, x, y, imageData)=> {
 *   if (pixel[3] === 0) return; // pixel[3]はalpha値、0の場合は透明ピクセルなので無視
 *   imageData.data[i] *= 0.2; // r
 *   imageData.data[i + 1] *= 0; // g
 *   imageData.data[i + 2] *= 0.8; // b
 * }
 * phina.accessory.AfterImage({
 *   interval: 8
 * }, filterFunc)
 * .attachTo(player);
 *
 * @param {object} [options]
 *   @param  {number} [options.interval=4] - 残像を表示する間隔
 *   @param  {number} [options.poolNum=22] - プールする残像オブジェクト数
 *   @param  {object} [options.effectProps={ alpha: 0 }] - 消え方のパラメータ
 *   @param  {number} [options.effectDuration=800] - 消える時間
 *   @param  {number} [options.effectEasing=easeOutCirc] - 消える際のイージング
 * @param {function} [filterFunc] - 残像にかけるフィルター関数:cross-originによるエラーに注意
 *
 */
export default phina.createClass({
  superClass: phina.accessory.Accessory,

  init: function (options, filterFunc) {
    this.superInit();

    options = ({}).$extend(DEFAULT_PARAMS, options);
    this._interval = options.interval;
    this._effectProps = options.effectProps;
    this._effectDuration = options.effectDuration;
    this._effectEasing = options.effectEasing;
    this._showing = true;
    this._pool = [];
    this._afterImageTexture = null;

    this.on('attached', function() {
      if (this.target instanceof phina.display.Sprite) {
        this._afterImageTexture = this.target._image.clone();
      } else if (this.target instanceof phina.display.Shape) {
        // Shape系クラスは最初の形状から編集できない
        // console.warn("AfterImage: Shape class is not fully supported!")
        this._afterImageTexture = this._toTexture(this.target);
      }

      if (filterFunc && typeof filterFunc === 'function') {
        this._afterImageTexture.filter(filterFunc)
      }

      for (let i = 0; i < options.poolNum; i++) this.poolImage();
    });
  },

  update: function(app) {
    if (!this.target || !this._showing) return;
    if (app.frame % this._interval === 0) this._emitAfterImage();
  },

  /**
   * @private
   * @instance
   * @memberof phina.accessory.AfterImage
   */
  _emitAfterImage: function() {
    let s = this._pool.find(function(el) { return !el.parent; });
    if (!s) return;
    // if (!s) s = this.poolImage(); // プールに無かったら補充

    // reset AfterImage sprite
    s.setPosition(this.target.x, this.target.y)
      .setRotation(this.target.rotation)
      .setSize(this.target.width, this.target.height)
      .setScale(this.target.scaleX, this.target.scaleY)
    ;
    if (this.target.srcRect) {
      const frame = this.target.srcRect;
      s.srcRect.set(frame.x, frame.y, frame.width, frame.height)
    }
    s.alpha = 1;

    // start tweener animation
    s.tweener.clear()
      .to(this._effectProps, this._effectDuration, this._effectEasing)
      .call(function () { s.remove(); })
    ;

    // 本体より手前に描画されるよう、addChildする
    this.target.parent.children.unshift(s);
    s.parent = this.target.parent;
  },

  /**
   * Shapeをcanvasに変換
   * @protected
   * @instance
   * @memberof phina.accessory.AfterImage
   *
   * @param {phina.display.Shape} shape
   * @return {phina.asset.Texture}
   */
  _toTexture: function(shape) {
    var t = phina.asset.Texture();
    shape.render(shape.canvas); // 一旦描画しておく
    var size = shape.calcCanvasSize();
    var canvas = phina.graphics.Canvas().setSize(size.width, size.height);
    canvas.context.drawImage(shape.canvas.domElement, 0, 0);
    t.domElement = canvas.domElement;
    return t;
  },

  /**
   * 表示フラグを変更
   * @instance
   * @memberof phina.accessory.AfterImage
   *
   * @param {boolean} flag
   * @return {this}
   */
  setVisible: function(flag) {
    this._showing = Boolean(flag);
    return this;
  },

  /**
   * 残像用Spriteをプール
   * @instance
   * @memberof phina.accessory.AfterImage
   *
   * @return {phina.display.Sprite} 生成したSpriteクラス
   */
  poolImage: function() {
    var s = phina.display.Sprite(this._afterImageTexture)
    this._pool.push(s);
    return s;
  },

  /**
   * add pool-releasing process to original remove method
   * @override
   * @instance
   * @memberof phina.accessory.AfterImage
   *
   * @return {void}
   */
  remove: function() {
    this.target.detach(this);
    this.target = null;
    this._pool.length = 0; // プール開放
  },

  _static: {
    defaults: DEFAULT_PARAMS,
  },

});