Source: gfx/core/particles/ParticleContainer.js

const Node = require('../Node');
const CONST = require('../../const');

/**
 * The ParticleContainer class is a really fast version of the Node built solely for speed,
 * so use when you need a lot of sprites or particles. The tradeoff of the ParticleContainer is that advanced
 * functionality will not work. ParticleContainer implements only the basic object transform (position, scale, rotation).
 * Any other functionality like tinting, masking, etc will not work on sprites in this batch.
 *
 * It's extremely easy to use :
 *
 * ```js
 * var container = new ParticleContainer();
 *
 * for (var i = 0; i < 100; ++i) {
 *   var sprite = new Sprite("myImage.png");
 *   container.addChild(sprite);
 * }
 * ```
 *
 * And here you have a hundred sprites that will be renderer at the speed of light.
 *
 * @class
 * @extends Node
 */
class ParticleContainer extends Node {
  /**
   * @constructor
   * @param {number} [maxSize=15000] The maximum number of particles that can be renderer by the container.
   * @param {object} [properties] The properties of children that should be uploaded to the gpu and applied.
   * @param {boolean} [properties.scale=false] When true, scale be uploaded and applied.
   * @param {boolean} [properties.position=true] When true, position be uploaded and applied.
   * @param {boolean} [properties.rotation=false] When true, rotation be uploaded and applied.
   * @param {boolean} [properties.uvs=false] When true, uvs be uploaded and applied.
   * @param {boolean} [properties.alpha=false] When true, alpha be uploaded and applied.
   * @param {number} [batchSize=15000] Number of particles per batch.
   */
  constructor(maxSize, properties, batchSize) {
    super();

    batchSize = batchSize || 15000; // CONST.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop
    maxSize = maxSize || 15000;

    // Making sure the batch size is valid
    // 65535 is max vertex index in the index buffer (see ParticleRenderer)
    // so max number of particles is 65536 / 4 = 16384
    var maxBatchSize = 16384;
    if (batchSize > maxBatchSize) {
      batchSize = maxBatchSize;
    }

    if (batchSize > maxSize) {
      batchSize = maxSize;
    }

    /**
     * Set properties to be dynamic (true) / static (false)
     *
     * @member {boolean[]}
     * @private
     */
    this._properties = [false, true, false, false, false];

    /**
     * @member {number}
     * @private
     */
    this._maxSize = maxSize;

    /**
     * @member {number}
     * @private
     */
    this._batchSize = batchSize;

    /**
     * @member {WebGLBuffer}
     * @private
     */
    this._buffers = null;

    /**
     * @member {number}
     * @private
     */
    this._bufferToUpdate = 0;

    /**
     * @member {boolean}
     *
     */
    this.interactiveChildren = false;

    /**
     * The blend mode to be applied to the sprite. Apply a value of `BLEND_MODES.NORMAL` to reset the blend mode.
     *
     * @member {number}
     * @default BLEND_MODES.NORMAL
     * @see BLEND_MODES
     */
    this.blendMode = CONST.BLEND_MODES.NORMAL;

    /**
     * Used for canvas renderering. If true then the elements will be positioned at the nearest pixel. This provides a nice speed boost.
     *
     * @member {boolean}
     * @default true;
     */
    this.roundPixels = true;

    this.setProperties(properties);
  }
}

/**
 * Sets the private properties array to dynamic / static based on the passed properties object
 *
 * @param {object} properties The properties to be uploaded
 */
ParticleContainer.prototype.setProperties = function(properties) {
  if (properties) {
    this._properties[0] = 'scale' in properties ? !!properties.scale : this._properties[0];
    this._properties[1] = 'position' in properties ? !!properties.position : this._properties[1];
    this._properties[2] = 'rotation' in properties ? !!properties.rotation : this._properties[2];
    this._properties[3] = 'uvs' in properties ? !!properties.uvs : this._properties[3];
    this._properties[4] = 'alpha' in properties ? !!properties.alpha : this._properties[4];
  }
};

/**
 * Updates the object transform for rendering
 *
 * @private
 */
ParticleContainer.prototype.updateTransform = function() {
  // TODO don't need to!
  this.displayObjectUpdateTransform();
};

/**
 * Renders the container using the WebGL renderer
 *
 * @param {WebGLRenderer} renderer The webgl renderer
 * @private
 */
ParticleContainer.prototype.renderWebGL = function(renderer) {
  if (!this.visible || this.worldAlpha <= 0 || !this.children.length || !this.renderable) {
    return;
  }

  renderer.setObjectRenderer(renderer.plugins.particle);
  renderer.plugins.particle.render(this);
};

/**
 * Set the flag that static data should be updated to true
 * @private
 * @param {Number} smallestChildIndex The very first child index
 */
ParticleContainer.prototype.onChildrenChange = function(smallestChildIndex) {
  var bufferIndex = Math.floor(smallestChildIndex / this._batchSize);
  if (bufferIndex < this._bufferToUpdate) {
    this._bufferToUpdate = bufferIndex;
  }
};

/**
 * Renders the object using the Canvas renderer
 *
 * @param {CanvasRenderer} renderer The canvas renderer
 * @private
 */
ParticleContainer.prototype.renderCanvas = function(renderer) {
  if (!this.visible || this.worldAlpha <= 0 || !this.children.length || !this.renderable) {
    return;
  }

  var context = renderer.context;
  var transform = this.worldTransform;
  var isRotated = true;

  var positionX = 0;
  var positionY = 0;

  var finalWidth = 0;
  var finalHeight = 0;

  var compositeOperation = renderer.blendModes[this.blendMode];
  if (compositeOperation !== context.globalCompositeOperation) {
    context.globalCompositeOperation = compositeOperation;
  }

  context.globalAlpha = this.worldAlpha;

  this.displayObjectUpdateTransform();

  for (var i = 0; i < this.children.length; ++i) {
    var child = this.children[i];

    if (!child.visible) {
      continue;
    }

    var frame = child.texture.frame;

    context.globalAlpha = this.worldAlpha * child.alpha;

    if (child.rotation % (Math.PI * 2) === 0) {
            // this is the fastest  way to optimise! - if rotation is 0 then we can avoid any kind of setTransform call
      if (isRotated) {
        context.setTransform(
          transform.a,
          transform.b,
          transform.c,
          transform.d,
          transform.tx,
          transform.ty
        );

        isRotated = false;
      }

      positionX = ((child.anchor.x) * (-frame.width * child.scale.x) + child.position.x + 0.5);
      positionY = ((child.anchor.y) * (-frame.height * child.scale.y) + child.position.y + 0.5);

      finalWidth = frame.width * child.scale.x;
      finalHeight = frame.height * child.scale.y;

    }
    else {
      if (!isRotated) {
        isRotated = true;
      }

      child.displayObjectUpdateTransform();

      var childTransform = child.worldTransform;

      if (renderer.roundPixels) {
        context.setTransform(
          childTransform.a,
          childTransform.b,
          childTransform.c,
          childTransform.d,
          childTransform.tx | 0,
          childTransform.ty | 0
        );
      }
      else {
        context.setTransform(
          childTransform.a,
          childTransform.b,
          childTransform.c,
          childTransform.d,
          childTransform.tx,
          childTransform.ty
        );
      }

      positionX = ((child.anchor.x) * (-frame.width) + 0.5);
      positionY = ((child.anchor.y) * (-frame.height) + 0.5);

      finalWidth = frame.width;
      finalHeight = frame.height;
    }

    context.drawImage(
      child.texture.baseTexture.source,
      frame.x,
      frame.y,
      frame.width,
      frame.height,
      positionX,
      positionY,
      finalWidth,
      finalHeight
    );
  }
};

/**
 * Destroys the container
 *
 * @param {boolean} [destroyChildren=false] if set to true, all the children will have their destroy method called as well
 */
ParticleContainer.prototype.destroy = function() {
  Node.prototype.destroy.apply(this, arguments);

  if (this._buffers) {
    for (var i = 0; i < this._buffers.length; ++i) {
      this._buffers[i].destroy();
    }
  }

  this._properties = null;
  this._buffers = null;
};

module.exports = ParticleContainer;