Source: tiny/core/display/Container.js

import { removeItems } from '../../../utils';
import DisplayObject from './DisplayObject';
import TWEEN from '../../../libs/tween.js';

/**
 * A Container represents a collection of display objects.
 * It is the base class of all display objects that act as a container for other objects.
 *
 *```js
 * let container = new Tiny.Container();
 * container.addChild(sprite);
 * ```
 *
 * @class
 * @extends Tiny.DisplayObject
 * @memberof Tiny
 */
export default class Container extends DisplayObject {
  /**
   *
   */
  constructor() {
    super();

    /**
     * The array of children of this container.
     *
     * @member {array<Tiny.DisplayObject>}
     * @readonly
     */
    this.children = [];

    /**
     * 该对象上绑定的所有 Action
     *
     * @member {array<Tiny.Action>}
     */
    this.actions = [];

    this.on('removed', () => {
      this.removeActionsTrace();
    });
  }

  /**
   * 移除该对象上绑定的某个(些) Action
   *
   * @example
   * sprite.removeAction(action1, action2);
   *
   * @param  {...Tiny.Action} actions - 要移除的某个(些)动作
   * @version 1.6.0
   */
  removeAction(...actions) {
    if (actions && actions.length !== 0) {
      actions.forEach(action => {
        const index = this.actions.indexOf(action.tween);

        if (~index) {
          this.actions.splice(index, 1);
        }
      });
    }
  };

  /**
   * 移除该对象上绑定的所有 Action
   */
  removeActions() {
    if (this.actions.length !== 0) {
      this.actions.forEach(function(action) {
        TWEEN.remove(action);
      });
      this.actions = [];
    }
  }

  /**
   * 移除该对象及其子集上绑定的所有 Action
   */
  removeActionsTrace() {
    if (this.children.length !== 0) {
      this.children.forEach((item) => {
        item.removeActionsTrace();
      });
    }
    this.removeActions();
  }

  /**
   * Overridable method that can be used by Container subclasses whenever the children array is modified
   *
   * @private
   */
  onChildrenChange() {
    /* empty */
  }

  /**
   * Adds one or more children to the container.
   *
   * Multiple items can be added like so: `myContainer.addChild(thingOne, thingTwo, thingThree)`
   *
   * @param {...Tiny.DisplayObject} child - The DisplayObject(s) to add to the container
   * @return {Tiny.DisplayObject} The first child that was added.
   */
  addChild(child) {
    const argumentsLength = arguments.length;

    // if there is only one argument we can bypass looping through the them
    if (argumentsLength > 1) {
      // loop through the arguments property and add all children
      // use it the right way (.length and [i]) so that this function can still be optimised by JS runtimes
      for (let i = 0; i < argumentsLength; i++) {
        this.addChild(arguments[i]);
      }
    } else {
      // if the child has a parent then lets remove it as Tiny objects can only exist in one place
      if (child.parent) {
        child.parent.removeChild(child);
      }

      child.parent = this;

      // ensure a transform will be recalculated..
      child.transform._parentID = -1;

      this.children.push(child);

      // ensure bounds will be recalculated
      this._boundsID++;

      // TODO - lets either do all callbacks or all events.. not both!
      this.onChildrenChange(this.children.length - 1);
      child.emit('added', this);
    }

    return child;
  }

  /**
   * Adds a child to the container at a specified index. If the index is out of bounds an error will be thrown
   *
   * @param {Tiny.DisplayObject} child - The child to add
   * @param {number} index - The index to place the child in
   * @return {Tiny.DisplayObject} The child that was added.
   */
  addChildAt(child, index) {
    if (index < 0 || index > this.children.length) {
      throw new Error(`${child}addChildAt: The index ${index} supplied is out of bounds ${this.children.length}`);
    }

    if (child.parent) {
      child.parent.removeChild(child);
    }

    child.parent = this;
    // ensure child transform will be recalculated
    child.transform._parentID = -1;

    this.children.splice(index, 0, child);

    // ensure bounds will be recalculated
    this._boundsID++;

    // TODO - lets either do all callbacks or all events.. not both!
    this.onChildrenChange(index);
    child.emit('added', this);

    return child;
  }

  /**
   * Swaps the position of 2 Display Objects within this container.
   *
   * @param {Tiny.DisplayObject} child - First display object to swap
   * @param {Tiny.DisplayObject} child2 - Second display object to swap
   */
  swapChildren(child, child2) {
    if (child === child2) {
      return;
    }

    const index1 = this.getChildIndex(child);
    const index2 = this.getChildIndex(child2);

    this.children[index1] = child2;
    this.children[index2] = child;
    this.onChildrenChange(index1 < index2 ? index1 : index2);
  }

  /**
   * Returns the index position of a child DisplayObject instance
   *
   * @param {Tiny.DisplayObject} child - The DisplayObject instance to identify
   * @return {number} The index position of the child display object to identify
   */
  getChildIndex(child) {
    const index = this.children.indexOf(child);

    if (index === -1) {
      throw new Error('The supplied DisplayObject must be a child of the caller');
    }

    return index;
  }

  /**
   * Changes the position of an existing child in the display object container
   *
   * @param {Tiny.DisplayObject} child - The child DisplayObject instance for which you want to change the index number
   * @param {number} index - The resulting index number for the child display object
   */
  setChildIndex(child, index) {
    if (index < 0 || index >= this.children.length) {
      throw new Error(`The index ${index} supplied is out of bounds ${this.children.length}`);
    }

    const currentIndex = this.getChildIndex(child);

    removeItems(this.children, currentIndex, 1); // remove from old position
    this.children.splice(index, 0, child); // add at new position
    this.onChildrenChange(index);
  }

  /**
   * Returns the child at the specified index
   *
   * @param {number} index - The index to get the child at
   * @return {Tiny.DisplayObject} The child at the given index, if any.
   */
  getChildAt(index) {
    if (index < 0 || index >= this.children.length) {
      throw new Error(`getChildAt: Index (${index}) does not exist.`);
    }

    return this.children[index];
  }

  /**
   * Removes one or more children from the container.
   *
   * @param {...Tiny.DisplayObject} child - The DisplayObject(s) to remove
   * @return {Tiny.DisplayObject} The first child that was removed.
   */
  removeChild(child) {
    const argumentsLength = arguments.length;

    // if there is only one argument we can bypass looping through the them
    if (argumentsLength > 1) {
      // loop through the arguments property and add all children
      // use it the right way (.length and [i]) so that this function can still be optimised by JS runtimes
      for (let i = 0; i < argumentsLength; i++) {
        this.removeChild(arguments[i]);
      }
    } else {
      const index = this.children.indexOf(child);

      if (index === -1) return null;

      child.parent = null;
      // ensure child transform will be recalculated
      child.transform._parentID = -1;
      removeItems(this.children, index, 1);

      // ensure bounds will be recalculated
      this._boundsID++;

      // TODO - lets either do all callbacks or all events.. not both!
      this.onChildrenChange(index);
      child.emit('removed', this);
    }

    return child;
  }

  /**
   * Removes a child from the specified index position.
   *
   * @param {number} index - The index to get the child from
   * @return {Tiny.DisplayObject} The child that was removed.
   */
  removeChildAt(index) {
    const child = this.getChildAt(index);

    // ensure child transform will be recalculated..
    child.parent = null;
    child.transform._parentID = -1;
    removeItems(this.children, index, 1);

    // ensure bounds will be recalculated
    this._boundsID++;

    // TODO - lets either do all callbacks or all events.. not both!
    this.onChildrenChange(index);
    child.emit('removed', this);

    return child;
  }

  /**
   * Removes all children from this container that are within the begin and end indexes.
   *
   * @param {number} [beginIndex=0] - The beginning position.
   * @param {number} [endIndex=this.children.length] - The ending position. Default value is size of the container.
   * @return {DisplayObject[]} List of removed children
   */
  removeChildren(beginIndex = 0, endIndex = null) {
    const begin = beginIndex;
    const end = typeof endIndex === 'number' ? endIndex : this.children.length;
    const range = end - begin;
    let removed;

    if (range > 0 && range <= end) {
      removed = this.children.splice(begin, range);

      for (let i = 0; i < removed.length; ++i) {
        removed[i].parent = null;
        if (removed[i].transform) {
          removed[i].transform._parentID = -1;
        }
      }

      this._boundsID++;

      this.onChildrenChange(beginIndex);

      for (let i = 0; i < removed.length; ++i) {
        removed[i].emit('removed', this);
      }

      return removed;
    } else if (range === 0 && this.children.length === 0) {
      return [];
    }

    throw new RangeError('removeChildren: numeric values are outside the acceptable range.');
  }

  /**
   * Updates the transform on all children of this container for rendering
   */
  updateTransform() {
    this._boundsID++;

    this.transform.updateTransform(this.parent.transform);

    // TODO: check render flags, how to process stuff here
    this.worldAlpha = this.alpha * this.parent.worldAlpha;

    for (let i = 0, j = this.children.length; i < j; ++i) {
      const child = this.children[i];

      if (child.visible) {
        child.updateTransform();
      }
    }
  }

  /**
   * Recalculates the bounds of the container.
   *
   */
  calculateBounds() {
    this._bounds.clear();

    this._calculateBounds();

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

      if (!child.visible || !child.renderable) {
        continue;
      }

      child.calculateBounds();

      // TODO: filter+mask, need to mask both somehow
      if (child._mask) {
        child._mask.calculateBounds();
        this._bounds.addBoundsMask(child._bounds, child._mask._bounds);
      } else if (child.filterArea) {
        this._bounds.addBoundsArea(child._bounds, child.filterArea);
      } else {
        this._bounds.addBounds(child._bounds);
      }
    }

    this._bounds.updateID = this._boundsID;
  }

  /**
   * Retrieves the local bounds of the displayObject as a rectangle object.
   *
   * @param {Tiny.Rectangle} [rect] - Optional rectangle to store the result of the bounds calculation.
   * @param {boolean} [skipChildrenUpdate=false] - Setting to `true` will stop re-calculation of children transforms.
   * @return {Tiny.Rectangle} The rectangular bounding area.
   */
  getLocalBounds(rect, skipChildrenUpdate = false) {
    const result = super.getLocalBounds(rect);

    if (!skipChildrenUpdate) {
      for (let i = 0, j = this.children.length; i < j; ++i) {
        const child = this.children[i];

        if (child.visible) {
          child.updateTransform();
        }
      }
    }

    return result;
  }

  /**
   * Recalculates the bounds of the object. Override this to
   * calculate the bounds of the specific object (not including children).
   *
   * @private
   */
  _calculateBounds() {
    // FILL IN//
  }

  /**
   * Renders the object using the WebGL renderer
   *
   * @param {Tiny.WebGLRenderer} renderer - The renderer
   */
  renderWebGL(renderer) {
    // if the object is not visible or the alpha is 0 then no need to render this element
    if (!this.visible || this.worldAlpha <= 0 || !this.renderable) {
      return;
    }

    // do a quick check to see if this element has a mask or a filter.
    if (this._mask || (this.filters && this.filters.length)) {
      this.renderAdvancedWebGL(renderer);
    } else {
      this._renderWebGL(renderer);

      // simple render children!
      for (let i = 0, j = this.children.length; i < j; ++i) {
        this.children[i].renderWebGL(renderer);
      }
    }
  }

  /**
   * Render the object using the WebGL renderer and advanced features.
   *
   * @private
   * @param {Tiny.WebGLRenderer} renderer - The renderer
   */
  renderAdvancedWebGL(renderer) {
    renderer.flush();

    const filters = this._filters;
    const mask = this._mask;

    // push filter first as we need to ensure the stencil buffer is correct for any masking
    if (filters) {
      if (!this._enabledFilters) {
        this._enabledFilters = [];
      }

      this._enabledFilters.length = 0;

      for (let i = 0; i < filters.length; i++) {
        if (filters[i].enabled) {
          this._enabledFilters.push(filters[i]);
        }
      }

      if (this._enabledFilters.length) {
        renderer.filterManager.pushFilter(this, this._enabledFilters);
      }
    }

    if (mask) {
      renderer.maskManager.pushMask(this, this._mask);
    }

    // add this object to the batch, only rendered if it has a texture.
    this._renderWebGL(renderer);

    // now loop through the children and make sure they get rendered
    for (let i = 0, j = this.children.length; i < j; i++) {
      this.children[i].renderWebGL(renderer);
    }

    renderer.flush();

    if (mask) {
      renderer.maskManager.popMask(this, this._mask);
    }

    if (filters && this._enabledFilters && this._enabledFilters.length) {
      renderer.filterManager.popFilter();
    }
  }

  /**
   * To be overridden by the subclasses.
   *
   * @private
   * @param {Tiny.WebGLRenderer} renderer - The renderer
   */
  _renderWebGL(renderer) { // eslint-disable-line no-unused-vars
    // this is where content itself gets rendered...
  }

  /**
   * To be overridden by the subclass
   *
   * @private
   * @param {Tiny.CanvasRenderer} renderer - The renderer
   */
  _renderCanvas(renderer) { // eslint-disable-line no-unused-vars
    // this is where content itself gets rendered...
  }

  /**
   * Renders the object using the Canvas renderer
   *
   * @param {Tiny.CanvasRenderer} renderer - The renderer
   */
  renderCanvas(renderer) {
    // if not visible or the alpha is 0 then no need to render this
    if (!this.visible || this.worldAlpha <= 0 || !this.renderable) {
      return;
    }

    if (this._mask) {
      renderer.maskManager.pushMask(this._mask);
    }

    this._renderCanvas(renderer);
    for (let i = 0, j = this.children.length; i < j; ++i) {
      this.children[i].renderCanvas(renderer);
    }

    if (this._mask) {
      renderer.maskManager.popMask(renderer);
    }
  }

  /**
   * Removes all internal references and listeners as well as removes children from the display list.
   * Do not use a Container after calling `destroy`.
   *
   * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options have been set to that value
   * @param {boolean} [options.children=false] - if set to true, all the children will have their destroy method called as well. 'options' will be passed on to those calls.
   * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true Should it destroy the texture of the child sprite
   * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true Should it destroy the base texture of the child sprite
   */
  destroy(options) {
    super.destroy();

    const destroyChildren = typeof options === 'boolean' ? options : options && options.children;

    const oldChildren = this.removeChildren(0, this.children.length);

    if (destroyChildren) {
      for (let i = 0; i < oldChildren.length; ++i) {
        oldChildren[i].destroy(options);
      }
    }
  }

  /**
   * The width of the Container, setting this will actually modify the scale to achieve the value set
   *
   * @member {number}
   */
  get width() {
    return this.scale.x * this.getLocalBounds().width;
  }

  set width(value) { // eslint-disable-line require-jsdoc
    const width = this.getLocalBounds().width;

    if (width !== 0) {
      this.scale.x = value / width;
    } else {
      this.scale.x = 1;
    }

    this._width = value;
  }

  /**
   * The height of the Container, setting this will actually modify the scale to achieve the value set
   *
   * @member {number}
   */
  get height() {
    return this.scale.y * this.getLocalBounds().height;
  }

  set height(value) { // eslint-disable-line require-jsdoc
    const height = this.getLocalBounds().height;

    if (height !== 0) {
      this.scale.y = value / height;
    } else {
      this.scale.y = 1;
    }

    this._height = value;
  }
}

// performance increase to avoid using call.. (10x faster)
Container.prototype.containerUpdateTransform = Container.prototype.updateTransform;
Documentation generated by JSDoc 3.4.3 on Fri Jul 09 2021 19:32:25 GMT+0800 (CST)