Source: tiny/core/display/DisplayObject.js

import EventEmitter from 'eventemitter3';
import { TRANSFORM_MODE } from '../const';
import settings from '../settings';
import TransformStatic from './TransformStatic';
import Transform from './Transform';
import Bounds from './Bounds';
import { Rectangle, Point } from '../math';
import { hex2rgb } from '../../../utils';
import { pluginTarget } from '../utils';

/**
 * `The base class for all objects that are rendered on the screen.`
 * This is an abstract class and should not be used on its own rather it should be extended.
 *
 * @class
 * @extends EventEmitter
 * @memberof Tiny
 */
export default class DisplayObject extends EventEmitter {
  /**
   *
   */
  constructor() {
    super();

    const TransformClass = settings.TRANSFORM_MODE === TRANSFORM_MODE.STATIC ? TransformStatic : Transform;

    this.tempDisplayObjectParent = null;

    /**
     * World transform and local transform of this object.
     * This will become read-only later, please do not assign anything there unless you know what are you doing
     *
     * @member {Tiny.TransformBase}
     */
    this.transform = new TransformClass();

    /**
     * The opacity of the object.
     *
     * @member {number}
     */
    this.alpha = 1;

    /**
     * The visibility of the object. If false the object will not be drawn, and the updateTransform function will not be called.
     *
     * Only affects recursive calls from parent. You can ask for bounds or call updateTransform manually
     *
     * @member {boolean}
     */
    this.visible = true;

    /**
     * Can this object be rendered, if false the object will not be drawn but the updateTransform methods will still be called.
     *
     * Only affects recursive calls from parent. You can ask for bounds manually
     *
     * @member {boolean}
     */
    this.renderable = true;

    /**
     * The display object container that contains this display object.
     *
     * @member {Tiny.Container}
     * @readonly
     */
    this.parent = null;

    /**
     * The multiplied alpha of the displayObject
     *
     * @member {number}
     * @readonly
     */
    this.worldAlpha = 1;

    /**
     * The area the filter is applied to. This is used as more of an optimisation rather than figuring out the dimensions of the displayObject each frame you can set this rectangle
     *
     * Also works as an interaction mask
     *
     * @member {Tiny.Rectangle}
     */
    this.filterArea = null;

    this._filters = null;
    this._enabledFilters = null;

    /**
     * The bounds object, this is used to calculate and store the bounds of the displayObject
     *
     * @member {Tiny.Rectangle}
     * @private
     */
    this._bounds = new Bounds();
    this._boundsID = 0;
    this._boundsRect = null;
    this._localBoundsRect = null;

    /**
     * Local bounds object, swapped with `_bounds` when using `getLocalBounds()`.
     *
     * @member {Tiny.Bounds}
     * @private
     */
    this._$localBounds = null;

    /**
     * The original, cached mask of the object
     *
     * @member {Tiny.Graphics|Tiny.Sprite}
     * @private
     */
    this._mask = null;

    /**
     * If the object has been destroyed via destroy(). If true, it should not be used.
     *
     * @member {boolean}
     * @private
     * @readonly
     */
    this._destroyed = false;

    /**
     * The instance name of the object.
     *
     * @member {string}
     */
    this.name = null;

    /**
     * Fired when this DisplayObject is added to a Container.
     *
     * @event Tiny.DisplayObject#added
     * @param {Tiny.Container} container - The container added to.
     */

    /**
     * Fired when this DisplayObject is removed from a Container.
     *
     * @event Tiny.DisplayObject#removed
     * @param {Tiny.Container} container - The container removed from.
     */

    this.initPlugins();
  }

  /**
   * @member {Tiny.DisplayObject}
   * @private
   */
  get _tempDisplayObjectParent() {
    if (this.tempDisplayObjectParent === null) {
      this.tempDisplayObjectParent = new DisplayObject();
    }

    return this.tempDisplayObjectParent;
  }

  /**
   * Updates the object transform for rendering
   */
  updateTransform() {
    this._boundsID++;

    this.transform.updateTransform(this.parent.transform);
    // multiply the alphas..
    this.worldAlpha = this.alpha * this.parent.worldAlpha;
  }

  /**
   * Recursively updates transform of all objects from the root to this one internal function for toLocal()
   *
   * @private
   */
  _recursivePostUpdateTransform() {
    if (this.parent) {
      this.parent._recursivePostUpdateTransform();
      this.transform.updateTransform(this.parent.transform);
    } else {
      this.transform.updateTransform(this._tempDisplayObjectParent.transform);
    }
  }

  /**
   * Retrieves the bounds of the displayObject as a rectangle object.
   *
   * @param {boolean} skipUpdate - setting to true will stop the transforms of the scene graph from being updated. This means the calculation returned MAY be out of date BUT will give you nice performance boost
   * @param {Tiny.Rectangle} rect - Optional rectangle to store the result of the bounds calculation
   * @return {Tiny.Rectangle} the rectangular bounding area
   */
  getBounds(skipUpdate, rect) {
    if (!skipUpdate) {
      if (!this.parent) {
        this.parent = this._tempDisplayObjectParent;
        this.updateTransform();
        this.parent = null;
      } else {
        this._recursivePostUpdateTransform();
        this.updateTransform();
      }
    }

    if (this._bounds.updateID !== this._boundsID) {
      this.calculateBounds();
      this._bounds.updateID = this._boundsID;
    }

    if (!rect) {
      if (!this._boundsRect) {
        this._boundsRect = new Rectangle();
      }

      rect = this._boundsRect;
    }

    return this._bounds.getRectangle(rect);
  }

  /**
   * 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
   * @return {Tiny.Rectangle} the rectangular bounding area
   */
  getLocalBounds(rect) {
    if (!rect) {
      if (!this._localBoundsRect) {
        this._localBoundsRect = new Rectangle();
      }

      rect = this._localBoundsRect;
    }

    if (!this._$localBounds) {
      this._$localBounds = new Bounds();
    }

    const transformRef = this.transform;
    const parentRef = this.parent;

    this.parent = null;
    this.transform = this._tempDisplayObjectParent.transform;

    const worldBounds = this._bounds;
    const worldBoundsID = this._boundsID;

    this._bounds = this._$localBounds;

    const bounds = this.getBounds(false, rect);

    this.parent = parentRef;
    this.transform = transformRef;

    this._bounds = worldBounds;
    this._bounds.updateID += this._boundsID - worldBoundsID; // reflect side-effects

    return bounds;
  }

  /**
   * Calculates the global position of the display object
   *
   * @param {Tiny.Point} position - The world origin to calculate from
   * @param {Tiny.Point} [point] - A Point object in which to store the value, optional (otherwise will create a new Point)
   * @param {boolean} [skipUpdate=false] - Should we skip the update transform.
   * @return {Tiny.Point} A point object representing the position of this object
   */
  toGlobal(position, point, skipUpdate = false) {
    if (!skipUpdate) {
      this._recursivePostUpdateTransform();

      // this parent check is for just in case the item is a root object.
      // If it is we need to give it a temporary parent so that displayObjectUpdateTransform works correctly
      // this is mainly to avoid a parent check in the main loop. Every little helps for performance :)
      if (!this.parent) {
        this.parent = this._tempDisplayObjectParent;
        this.displayObjectUpdateTransform();
        this.parent = null;
      } else {
        this.displayObjectUpdateTransform();
      }
    }

    // don't need to update the lot
    return this.worldTransform.apply(position, point);
  }

  /**
   * Calculates the local position of the display object relative to another point
   *
   * @param {Tiny.Point} position - The world origin to calculate from
   * @param {Tiny.DisplayObject} [from] - The DisplayObject to calculate the global position from
   * @param {Tiny.Point} [point] - A Point object in which to store the value, optional (otherwise will create a new Point)
   * @param {boolean} [skipUpdate=false] - Should we skip the update transform
   * @return {Tiny.Point} A point object representing the position of this object
   */
  toLocal(position, from, point, skipUpdate) {
    if (from) {
      position = from.toGlobal(position, point, skipUpdate);
    }

    if (!skipUpdate) {
      this._recursivePostUpdateTransform();

      // this parent check is for just in case the item is a root object.
      // If it is we need to give it a temporary parent so that displayObjectUpdateTransform works correctly
      // this is mainly to avoid a parent check in the main loop. Every little helps for performance :)
      if (!this.parent) {
        this.parent = this._tempDisplayObjectParent;
        this.displayObjectUpdateTransform();
        this.parent = null;
      } else {
        this.displayObjectUpdateTransform();
      }
    }

    // simply apply the matrix..
    return this.worldTransform.applyInverse(position, point);
  }

  /**
   * Returns the global position of the displayObject. Does not depend on object scale, rotation and pivot.
   *
   * @param {Tiny.Point} point - the point to write the global value to. If null a new point will be returned
   * @param {boolean} skipUpdate - setting to true will stop the transforms of the scene graph from being updated. This means the calculation returned MAY be out of date BUT will give you a nice performance boost
   * @return {Tiny.Point} The updated point
   */
  getGlobalPosition(point = new Point(), skipUpdate = false) {
    if (this.parent) {
      this.parent.toGlobal(this.position, point, skipUpdate);
    } else {
      point.x = this.position.x;
      point.y = this.position.y;
    }
    return point;
  }

  /**
   * Returns the display object in the container
   *
   * @param {string} name - instance name
   * @return {Tiny.DisplayObject} The child with the specified name.
   */
  getChildByName(name) {
    for (let i = 0; i < this.children.length; i++) {
      if (this.children[i].name === name) {
        return this.children[i];
      }
    }

    return null;
  }

  /**
   * Renders the object using the WebGL renderer
   *
   * @param {Tiny.WebGLRenderer} renderer - The renderer
   */
  renderWebGL(renderer) { // eslint-disable-line no-unused-vars
    // OVERWRITE;
  }

  /**
   * Renders the object using the Canvas renderer
   *
   * @param {Tiny.CanvasRenderer} renderer - The renderer
   */
  renderCanvas(renderer) { // eslint-disable-line no-unused-vars
    // OVERWRITE;
  }

  /**
   * Set the parent Container of this DisplayObject
   *
   * @param {Tiny.Container} container - The Container to add this DisplayObject to
   * @return {Tiny.Container} The Container that this DisplayObject was added to
   */
  setParent(container) {
    if (!container || !container.addChild) {
      throw new Error('setParent: Argument must be a Container');
    }

    container.addChild(this);

    return container;
  }

  /**
   * Convenience function to set the position, scale, skew and pivot at once.
   *
   * @param {number} [x=0] - The X position
   * @param {number} [y=0] - The Y position
   * @param {number} [scaleX=1] - The X scale value
   * @param {number} [scaleY=1] - The Y scale value
   * @param {number} [rotation=0] - The rotation
   * @param {number} [skewX=0] - The X skew value
   * @param {number} [skewY=0] - The Y skew value
   * @param {number} [pivotX=0] - The X pivot value
   * @param {number} [pivotY=0] - The Y pivot value
   * @return {Tiny.DisplayObject} The DisplayObject instance
   */
  setTransform(x = 0, y = 0, scaleX = 1, scaleY = 1, rotation = 0, skewX = 0, skewY = 0, pivotX = 0, pivotY = 0) {
    this.position.x = x;
    this.position.y = y;
    this.scale.x = !scaleX ? 1 : scaleX;
    this.scale.y = !scaleY ? 1 : scaleY;
    this.rotation = rotation;
    this.skew.x = skewX;
    this.skew.y = skewY;
    this.pivot.x = pivotX;
    this.pivot.y = pivotY;

    return this;
  }

  /**
   * 设置是否可交互
   *
   * @param {boolean} b
   */
  setEventEnabled(b) {
    this.interactive = !!b;
  }

  /**
   *
   * @param {number} x
   * @param {number} y
   */
  setPosition(x, y) {
    this.position.set(x, y === void 0 ? x : y);
  }

  /**
   *
   * @return {object}
   */
  getPosition() {
    return this.position;
  }

  /**
   *
   * @param {number} x
   */
  setPositionX(x) {
    this.x = x;
  }

  /**
   *
   * @return {number}
   */
  getPositionX() {
    return this.x;
  }

  /**
   *
   * @param {number} y
   */
  setPositionY(y) {
    this.y = y;
  }

  /**
   *
   * @return {number}
   */
  getPositionY() {
    return this.y;
  }

  /**
   *
   * @param {number} x
   * @param {number} y
   */
  setPivot(x, y) {
    this.pivot.set(x, y === void 0 ? x : y);
  }

  /**
   *
   * @return {number}
   */
  getPivot() {
    return this.pivot;
  }

  /**
   *
   * @param {number} rotation
   */
  setRotation(rotation) {
    this.rotation = rotation;
  }

  /**
   *
   * @return {number}
   */
  getRotation() {
    return this.rotation;
  }

  /**
   *
   * @param {number} alpha
   */
  setOpacity(alpha) {
    this.alpha = alpha;
  }

  /**
   *
   * @return {number}
   */
  getOpacity() {
    return this.alpha;
  }

  /**
   *
   * @param {boolean} visible
   */
  setVisible(visible) {
    this.visible = visible;
  }

  /**
   *
   * @return {boolean}
   */
  getVisible() {
    return this.visible;
  }

  /**
   *
   * @param {number} x
   * @param {number} y
   */
  setScale(x, y) {
    this.scale.set(x, (y === void 0 ? x : y));
  }

  /**
   *
   * @return {object}
   */
  getScale() {
    return this.scale;
  }

  /**
   *
   * @param {number} x
   * @param {number} y
   * @version 1.1.0
   */
  setSkew(x, y) {
    this.skew.set(x, (y === void 0 ? x : y));
  }

  /**
   *
   * @return {object}
   * @version 1.1.0
   */
  getSkew() {
    return this.skew;
  }

  /**
   * 获取对象的属性:`x`, `y`, `angle`, `rotation`, `visible`, `alpha`, `scaleX`, `scaleY`, `skewX`, `skewY`, `colorR`, `colorG`, `colorB`
   *
   * @return {object}
   */
  getNature() {
    const tint = hex2rgb(this.tint);
    return {
      x: this.x,
      y: this.y,
      angle: this.angle || 0,
      rotation: this.rotation,
      visible: this.visible,
      alpha: this.alpha,
      scaleX: this.scale.x,
      scaleY: this.scale.y,
      skewX: this.skew.x,
      skewY: this.skew.y,
      colorR: tint[0] * 255,
      colorG: tint[1] * 255,
      colorB: tint[2] * 255,
    };
  }

  /**
   * Base destroy method for generic display objects. This will automatically remove the display object from its parent Container as well as remove all current event listeners and internal references. Do not use a DisplayObject after calling `destroy`.
   *
   */
  destroy() {
    if (this.parent) {
      this.parent.removeChild(this);
    }
    this.removeAllListeners();
    this.transform = null;

    this.parent = null;

    this._bounds = null;
    this._currentBounds = null;
    this._mask = null;

    this.filterArea = null;

    this.interactive = false;
    this.interactiveChildren = false;

    this._destroyed = true;
  }

  /**
   * The position of the displayObject on the x axis relative to the local coordinates of the parent.
   * An alias to position.x
   *
   * @member {number}
   */
  get x() {
    return this.position.x;
  }

  set x(value) { // eslint-disable-line require-jsdoc
    this.transform.position.x = value;
  }

  /**
   * The position of the displayObject on the y axis relative to the local coordinates of the parent.
   * An alias to position.y
   *
   * @member {number}
   */
  get y() {
    return this.position.y;
  }

  set y(value) { // eslint-disable-line require-jsdoc
    this.transform.position.y = value;
  }

  /**
   * Current transform of the object based on world (parent) factors
   *
   * @member {Tiny.Matrix}
   * @readonly
   */
  get worldTransform() {
    return this.transform.worldTransform;
  }

  /**
   * Current transform of the object based on local factors: position, scale, other stuff
   *
   * @member {Tiny.Matrix}
   * @readonly
   */
  get localTransform() {
    return this.transform.localTransform;
  }

  /**
   * The coordinate of the object relative to the local coordinates of the parent.
   *
   * @member {Tiny.Point|Tiny.ObservablePoint}
   */
  get position() {
    return this.transform.position;
  }

  set position(value) { // eslint-disable-line require-jsdoc
    this.transform.position.copy(value);
  }

  /**
   * The scale factor of the object.
   *
   * @member {Tiny.Point|Tiny.ObservablePoint}
   */
  get scale() {
    return this.transform.scale;
  }

  set scale(value) { // eslint-disable-line require-jsdoc
    this.transform.scale.copy(value);
  }

  /**
   * The pivot point of the displayObject that it rotates around
   *
   * @member {Tiny.Point|Tiny.ObservablePoint}
   */
  get pivot() {
    return this.transform.pivot;
  }

  set pivot(value) { // eslint-disable-line require-jsdoc
    this.transform.pivot.copy(value);
  }

  /**
   * The skew factor for the object in radians.
   *
   * @member {Tiny.ObservablePoint}
   */
  get skew() {
    return this.transform.skew;
  }

  set skew(value) { // eslint-disable-line require-jsdoc
    this.transform.skew.copy(value);
  }

  /**
   * The rotation of the object in radians.
   *
   * @member {number}
   */
  get rotation() {
    return this.transform.rotation;
  }

  set rotation(value) {
    this.transform.rotation = value;
  }

  /**
   * Indicates if the object is globally visible.
   *
   * @member {boolean}
   * @readonly
   */
  get worldVisible() {
    let item = this;

    do {
      if (!item.visible) {
        return false;
      }

      item = item.parent;
    } while (item);

    return true;
  }

  /**
   * Sets a mask for the displayObject. A mask is an object that limits the visibility of an object to the shape of the mask applied to it. In Tiny a regular mask must be a Tiny.Graphics or a Tiny.Sprite object. This allows for much faster masking in canvas as it utilises shape clipping. To remove a mask, set this property to null.
   *
   * @member {Tiny.Graphics|Tiny.Sprite}
   */
  get mask() {
    return this._mask;
  }

  set mask(value) {
    if (this._mask) {
      this._mask.renderable = true;
      this._mask.isMask = false;
    }

    this._mask = value;

    if (this._mask) {
      this._mask.renderable = false;
      this._mask.isMask = true;
    }
  }

  /**
   * Sets the filters for the displayObject.
   * * IMPORTANT: This is a webGL only feature and will be ignored by the canvas renderer.
   * To remove filters simply set this property to 'null'
   *
   * @member {array<Tiny.Filter>}
   */
  get filters() {
    return this._filters && this._filters.slice();
  }

  set filters(value) {
    this._filters = value && value.slice();
  }
}

// performance increase to avoid using call.. (10x faster)
DisplayObject.prototype.displayObjectUpdateTransform = DisplayObject.prototype.updateTransform;

pluginTarget.mixin(DisplayObject);
Documentation generated by JSDoc 3.4.3 on Fri Jul 09 2021 19:32:25 GMT+0800 (CST)