Source: gfx/core/Node.js

const math = require('./math');
const { removeItems } = require('./utils');
const EventEmitter = require('engine/EventEmitter');
const RenderTexture = require('./textures/RenderTexture');
const CONST = require('../const');
const Vector = require('engine/Vector');

const _tempMatrix = new math.Matrix();
const _tempDisplayObjectParent = {
  worldTransform: new math.Matrix(),
  worldAlpha: 1,
  children: [],
};

const EMPTY_ARRAY = [];

/**
 * Node is the basic graphic element, that can be added to other nodes as children.
 * It is also the base class of all display objects.
 *
 *```js
 * var container = new Node();
 * container.addChild(some_sprite);
 * ```
 * @class
 * @extends EventEmitter
 */
class Node extends EventEmitter {
  /**
   * @constructor
   */
  constructor() {
    super();

    /**
     * The coordinate of the node relative to the local coordinates of the parent.
     *
     * @type {Vector}
     */
    this.position = new Vector();

    /**
     * The scale factor of the node.
     *
     * @type {Vector}
     */
    this.scale = new Vector(1, 1);

    /**
     * The pivot point of the node that it rotates around
     *
     * @type {Vector}
     */
    this.pivot = new Vector(0, 0);


    /**
     * The skew factor for the node in radians.
     *
     * @type {Vector}
     */
    this.skew = new Vector(0, 0);

    /**
     * The rotation of the node in radians.
     *
     * @type {Number}
     */
    this.rotation = 0;

    /**
     * The opacity of the node.
     *
     * @type {Number}
     */
    this.alpha = 1;

    /**
     * The visibility of the node. If false the node will not be drawn, and
     * the updateTransform function will not be called.
     *
     * @type {Boolean}
     */
    this.visible = true;

    /**
     * Can this node be rendered, if false the node will not be drawn but the updateTransform
     * methods will still be called.
     *
     * @type {Boolean}
     */
    this.renderable = true;

    /**
     * The node that contains this one.
     *
     * @type {Node}
     * @readonly
     */
    this.parent = null;

    /**
     * The list of children added to this node.
     *
     * @member {Node[]}
     * @readonly
     */
    this.children = [];

    /**
     * The multiplied alpha of the node
     *
     * @type {Number}
     * @readonly
     */
    this.worldAlpha = 1;

    /**
     * Current transform of the node based on world (parent) factors
     *
     * @type {Matrix}
     * @readonly
     */
    this.worldTransform = new math.Matrix();

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

    /**
     * cached sin rotation
     *
     * @type {Number}
     * @private
     */
    this._sr = 0;

    /**
     * cached cos rotation
     *
     * @type {Number}
     * @private
     */
    this._cr = 1;

    /**
     * The original, cached bounds of the node
     *
     * @type {Rectangle}
     * @private
     */
    this._bounds = new math.Rectangle(0, 0, 1, 1);

    /**
     * The most up-to-date bounds of the node
     *
     * @type {Rectangle}
     * @private
     */
    this._currentBounds = null;

    /**
     * The original, cached mask of the node
     *
     * @type {Rectangle}
     * @private
     */
    this._mask = null;

    /**
     * Reference to the gfx system this node is renderer by
     * @type {SystemGfx}
     * @private
     */
    this._system = null;
  }

  /**
   * Overridable method that can be used by `Node` subclasses whenever the children list is modified
   * @memberof Node#
   * @private
   */
  onChildrenChange() {}

  /**
   * Adds a child to this node.
   * You can also add multple items like so: myContainer.addChild(thinkOne, thingTwo, thingThree)
   * @memberof Node#
   * @param {Node} child The Node to add to the container
   * @return {Node} The child that was added
   */
  addChild(child) {
    var 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 (var i = 0; i < argumentsLength; i++) {
        this.addChild(arguments[i]);
      }
    }
    else {
      // if the child has a parent then lets remove it as nodes can only exist in one place
      if (child.parent) {
        child.parent.removeChild(child);
      }

      child.parent = this;
      child.system = this.system;

      this.children.push(child);

      // 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 this node at a specified index. If the index is out of bounds an error will be thrown
   * @memberof Node#
   * @param {Node} child      The child to add
   * @param {Number} index    The index to place the child at
   * @return {Node} The child that was just added
   */
  addChildAt(child, index) {
    if (index >= 0 && index <= this.children.length) {
      if (child.parent) {
        child.parent.removeChild(child);
      }

      child.parent = this;
      child.system = this.system;

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

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

      return child;
    }
    else {
      throw new Error(child + 'addChildAt: The index ' + index + ' supplied is out of bounds ' + this.children.length);
    }
  }

  /**
   * Swaps the position of 2 children within this node.
   * @memberof Node#
   * @param {Node} child   Child to swap
   * @param {Node} child2  Child to swap
   */
  swapChildren(child, child2) {
    if (child === child2) {
      return;
    }

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

    if (index1 < 0 || index2 < 0) {
      throw new Error('swapChildren: Both the supplied Node must be children of the caller.');
    }

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

  /**
   * Returns the index position of a child Node instance
   * @memberof Node#
   * @param {Node} child The node instance to identify
   * @return {Number} The index position of the child node to identify
   */
  getChildIndex(child) {
    var index = this.children.indexOf(child);

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

    return index;
  }

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

    var 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
   * @memberof Node#
   * @param {Number} index The index to get the child at
   * @return {Node} The child at the given index, if any.
   */
  getChildAt(index) {
    if (index < 0 || index >= this.children.length) {
      throw new Error('getChildAt: Supplied index ' + index + ' does not exist in the child list, or the supplied Node is not a child of the caller');
    }

    return this.children[index];
  }

  /**
   * Removes a child from this node.
   * @memberof Node#
   * @param {Node} child The Node to remove
   * @return {Node} The child that was removed.
   */
  removeChild(child) {
    var 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 (var i = 0; i < argumentsLength; i++) {
        this.removeChild(arguments[i]);
      }
    }
    else {
      var index = this.children.indexOf(child);

      if (index === -1) {
        return;
      }

      child.parent = null;
      child.system = null;
      removeItems(this.children, index, 1);

      // 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.
   * @memberof Node#
   * @param {Number} index The index to get the child from
   * @return {Node} The child that was removed.
   */
  removeChildAt(index) {
    var child = this.getChildAt(index);

    child.parent = null;
    child.system = null;
    removeItems(this.children, index, 1);

    // 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 node that are within the begin and end indexes.
   * @memberof Node#
   * @param {Number} beginIndex The beginning position. Default value is 0.
   * @param {Number} endIndex   The ending position. Default value is size of this node.
   * @return {Array} Children removed from this node.
   */
  removeChildren(beginIndex, endIndex) {
    var begin = beginIndex || 0;
    var end = typeof endIndex === 'number' ? endIndex : this.children.length;
    var range = end - begin;
    var removed, i;

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

      for (i = 0; i < removed.length; ++i) {
        removed[i].parent = null;
        removed[i].system = null;
      }

      this.onChildrenChange(beginIndex);

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

      return removed;
    }
    else if (range === 0 && this.children.length === 0) {
      return EMPTY_ARRAY;
    }
    else {
      throw new RangeError('removeChildren: numeric values are outside the acceptable range.');
    }
  }

  /**
   * Remove this node from its parent (if exist)
   * @memberof Node#
   */
  remove() {
    if (this.parent) {
      this.parent.removeChild(this);
    }
  }

  /**
   * Add this object to another node
   * @memberof Node#
   * @param {Node} container Node to add to
   * @return {Node} Self for chaining
   */
  addTo(container) {
    container.addChild(this);
    return this;
  }

  /**
   * Useful function that returns a texture of the node that can then be used to create sprites
   * This can be quite useful if your Node is static / complicated and needs to be reused multiple times.
   * @memberof Node#
   * @param {CanvasRenderer|WebGLRenderer} renderer The renderer used to generate the texture
   * @param {Number} resolution                     The resolution of the texture being generated
   * @param {Number} scaleMode                      See {@link SCALE_MODES} for possible values
   * @return {Texture} a texture of the node
   */
  generateTexture(renderer, resolution, scaleMode) {
    var bounds = this.getLocalBounds();

    var renderTexture = new RenderTexture(renderer, bounds.width | 0, bounds.height | 0, scaleMode, resolution);

    _tempMatrix.tx = -bounds.x;
    _tempMatrix.ty = -bounds.y;

    renderTexture.render(this, _tempMatrix);

    return renderTexture;
  }

  /**
   * Updates the transform on all children of this node for rendering
   * @memberof Node#
   * @private
   */
  updateTransform() {
    if (!this.visible) {
      return;
    }

    this.displayObjectUpdateTransform();

    for (var i = 0, j = this.children.length; i < j; ++i) {
      this.children[i].updateTransform();
    }
  }

  /*
   * Updates the object transform for rendering
   *
   * TODO - Optimization pass!
   */
  displayObjectUpdateTransform() {
    // create some matrix refs for easy access
    var pt = this.parent.worldTransform;
    var wt = this.worldTransform;

    // temporary matrix variables
    var a, b, c, d, tx, ty;

    // looks like we are skewing
    if (this.skew.x || this.skew.y) {
      // I'm assuming that skewing is not going to be very common
      // With that in mind, we can do a full setTransform using the temp matrix
      _tempMatrix.setTransform(
        this.position.x,
        this.position.y,
        this.pivot.x,
        this.pivot.y,
        this.scale.x,
        this.scale.y,
        this.rotation,
        this.skew.x,
        this.skew.y
      );

      // now concat the matrix (inlined so that we can avoid using copy)
      wt.a = _tempMatrix.a * pt.a + _tempMatrix.b * pt.c;
      wt.b = _tempMatrix.a * pt.b + _tempMatrix.b * pt.d;
      wt.c = _tempMatrix.c * pt.a + _tempMatrix.d * pt.c;
      wt.d = _tempMatrix.c * pt.b + _tempMatrix.d * pt.d;
      wt.tx = _tempMatrix.tx * pt.a + _tempMatrix.ty * pt.c + pt.tx;
      wt.ty = _tempMatrix.tx * pt.b + _tempMatrix.ty * pt.d + pt.ty;
    }
    else {
      // so if rotation is between 0 then we can simplify the multiplication process...
      if (this.rotation % CONST.PI_2) {
        // check to see if the rotation is the same as the previous render. This means we only need to use sin and cos when rotation actually changes
        if (this.rotation !== this.rotationCache) {
          this.rotationCache = this.rotation;
          this._sr = Math.sin(this.rotation);
          this._cr = Math.cos(this.rotation);
        }

        // get the matrix values of the displayobject based on its transform properties..
        a = this._cr * this.scale.x;
        b = this._sr * this.scale.x;
        c = -this._sr * this.scale.y;
        d = this._cr * this.scale.y;
        tx = this.position.x;
        ty = this.position.y;

        // check for pivot.. not often used so geared towards that fact!
        if (this.pivot.x || this.pivot.y) {
          tx -= this.pivot.x * a + this.pivot.y * c;
          ty -= this.pivot.x * b + this.pivot.y * d;
        }

        // concat the parent matrix with the objects transform.
        wt.a = a * pt.a + b * pt.c;
        wt.b = a * pt.b + b * pt.d;
        wt.c = c * pt.a + d * pt.c;
        wt.d = c * pt.b + d * pt.d;
        wt.tx = tx * pt.a + ty * pt.c + pt.tx;
        wt.ty = tx * pt.b + ty * pt.d + pt.ty;
      }
      else {
        // lets do the fast version as we know there is no rotation..
        a = this.scale.x;
        d = this.scale.y;

        tx = this.position.x - this.pivot.x * a;
        ty = this.position.y - this.pivot.y * d;

        wt.a = a * pt.a;
        wt.b = a * pt.b;
        wt.c = d * pt.c;
        wt.d = d * pt.d;
        wt.tx = tx * pt.a + ty * pt.c + pt.tx;
        wt.ty = tx * pt.b + ty * pt.d + pt.ty;
      }
    }

    // multiply the alphas..
    this.worldAlpha = this.alpha * this.parent.worldAlpha;

    // reset the bounds each time this is called!
    this._currentBounds = null;
  }

  /**
   * Convenience function to set the postion, 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 {Node} This for chaining.
   */
  setTransform(x, y, scaleX, scaleY, rotation, skewX, skewY, pivotX, pivotY) {
    this.position.x = x || 0;
    this.position.y = y || 0;
    this.scale.x = !scaleX ? 1 : scaleX;
    this.scale.y = !scaleY ? 1 : scaleY;
    this.rotation = rotation || 0;
    this.skew.x = skewX || 0;
    this.skew.y = skewY || 0;
    this.pivot.x = pivotX || 0;
    this.pivot.y = pivotY || 0;
    return this;
  }

  /**
   * Retrieves the bounds of the Node as a rectangle. The bounds calculation takes all visible children into consideration.
   * @memberof Node#
   * @return {Rectangle} The rectangular bounding area
   */
  getBounds() {
    if (!this._currentBounds) {

      if (this.children.length === 0) {
        return math.Rectangle.EMPTY;
      }

      // TODO the bounds have already been calculated this render session so return what we have

      var minX = Infinity;
      var minY = Infinity;

      var maxX = -Infinity;
      var maxY = -Infinity;

      var childBounds;
      var childMaxX;
      var childMaxY;

      var childVisible = false;

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

        if (!child.visible) {
          continue;
        }

        childVisible = true;

        childBounds = this.children[i].getBounds();

        minX = minX < childBounds.x ? minX : childBounds.x;
        minY = minY < childBounds.y ? minY : childBounds.y;

        childMaxX = childBounds.width + childBounds.x;
        childMaxY = childBounds.height + childBounds.y;

        maxX = maxX > childMaxX ? maxX : childMaxX;
        maxY = maxY > childMaxY ? maxY : childMaxY;
      }

      if (!childVisible) {
        return math.Rectangle.EMPTY;
      }

      var bounds = this._bounds;

      bounds.x = minX;
      bounds.y = minY;
      bounds.width = maxX - minX;
      bounds.height = maxY - minY;

      this._currentBounds = bounds;
    }

    return this._currentBounds;
  }

  /**
   * Retrieves the non-global local bounds of the Node as a rectangle.
   * The calculation takes all visible children into consideration.
   * @memberof Node#
   * @return {Rectangle} The rectangular bounding area
   */
  getLocalBounds() {
    var matrixCache = this.worldTransform;

    this.worldTransform = math.Matrix.IDENTITY;

    for (var i = 0, j = this.children.length; i < j; ++i) {
      this.children[i].updateTransform();
    }

    this.worldTransform = matrixCache;

    this._currentBounds = null;

    return this.getBounds(math.Matrix.IDENTITY);
  }

  /**
   * Returns the global position of the displayObject.
   *
   * @memberof Node#
   * @param {Vector} point the point to write the global value to. If null a new point will be returned
   * @return {Vector} Global position vector
   */
  getGlobalPosition(point) {
    point = point || Vector.create();

    if (this.parent) {
      this.displayObjectUpdateTransform();

      point.x = this.worldTransform.tx;
      point.y = this.worldTransform.ty;
    }
    else {
      point.x = this.position.x;
      point.y = this.position.y;
    }

    return point;
  }

  /**
   * Calculates the global position of this node
   *
   * @param {Vector} position The world origin to calculate from
   * @return {Vector} A point representing the position of this node
   */
  toGlobal(position) {
    // this parent check is for just in case the item is a root node.
    // 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 = _tempDisplayObjectParent;
      this.displayObjectUpdateTransform();
      this.parent = null;
    }
    else {
      this.displayObjectUpdateTransform();
    }

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

  /**
   * Calculates the local position of this node relative to another point
   *
   * @param {Vector} position       The world origin to calculate from
   * @param {Node} [from]  The Node to calculate the global position from
   * @param {Vector} [point]        A Point in which to store the value, optional (otherwise will create a new Point)
   * @return {Vector} A point representing the position of this node
   */
  toLocal(position, from, point) {
    if (from) {
      position = from.toGlobal(position);
    }

    // this parent check is for just in case the item is a root node.
    // 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 = _tempDisplayObjectParent;
      this.displayObjectUpdateTransform();
      this.parent = null;
    }
    else {
      this.displayObjectUpdateTransform();
    }

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

  /**
   * Renders the object using the WebGL renderer
   * @memberof Node#
   * @param {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;
    }

    var i, j;

    // do a quick check to see if this element has a mask or a filter.
    if (this._mask || this._filters) {
      renderer.currentRenderer.flush();

      // push filter first as we need to ensure the stencil buffer is correct for any masking
      if (this._filters && this._filters.length) {
        renderer.filterManager.pushFilter(this, this._filters);
      }

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

      renderer.currentRenderer.start();

      // 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 (i = 0, j = this.children.length; i < j; i++) {
        this.children[i].renderWebGL(renderer);
      }

      renderer.currentRenderer.flush();

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

      if (this._filters) {
        renderer.filterManager.popFilter();

      }
      renderer.currentRenderer.start();
    }
    else {
      this._renderWebGL(renderer);

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

  /**
   * To be overridden by the subclass
   * @memberof Node#
   * @param {WebGLRenderer} renderer The renderer
   * @private
   */
  _renderWebGL(renderer) {/* eslint no-unused-vars:0 */
    // this is where content itself gets rendered...
  }

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


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

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

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

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

  /**
   * Destroys the node
   * @memberof Node#
   * @param {Boolean} [destroyChildren=false] if set to true, all the children will have their destroy method called as well
   */
  destroy(destroyChildren) {
    this.position = null;
    this.scale = null;
    this.pivot = null;
    this.skew = null;

    this.parent = null;

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

    this.worldTransform = null;
    this.filterArea = null;

    if (destroyChildren) {
      for (var i = 0, j = this.children.length; i < j; ++i) {
        this.children[i].destroy(destroyChildren);
      }
    }

    this.removeChildren();

    this.children = null;
  }
}

// performance increase to avoid using call.. (10x faster)
Node.prototype.containerUpdateTransform = Node.prototype.updateTransform;
Node.prototype.containerGetBounds = Node.prototype.getBounds;

Object.defineProperties(Node.prototype, {
  /**
   * The position of this Node on the x axis relative to the local coordinates of the parent.
   *
   * @member {Number}
   * @memberof Node#
   */
  x: {
    get: function() {
      return this.position.x;
    },
    set: function(value) {
      this.position.x = value;
    },
  },

  /**
   * The position of this Node on the y axis relative to the local coordinates of the parent.
   *
   * @member {Number}
   * @memberof Node#
   */
  y: {
    get: function() {
      return this.position.y;
    },
    set: function(value) {
      this.position.y = value;
    },
  },

  /**
   * Indicates if this node is globally visible.
   *
   * @member {Boolean}
   * @memberof Node#
   * @readonly
   */
  worldVisible: {
    get: function() {
      var item = this;

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

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

      return true;
    },
  },

  /**
   * Sets a mask for this Node. A mask is an object that limits the visibility of an object to the shape of the mask applied to it.
   * A regular mask must be a Graphics or a Sprite object. This allows for much faster masking in canvas as it utilises shape clipping.
   * To remove a mask, set this property to null.
   *
   * @todo For the moment, CanvasRenderer doesn't support Sprite as mask.
   *
   * @member {Graphics|Sprite}
   * @memberof Node#
   */
  mask: {
    get: function() {
      return this._mask;
    },
    set: function(value) {
      if (this._mask) {
        this._mask.renderable = true;
      }

      this._mask = value;

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

  /**
   * Sets the filters for this Node.
   * * 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 {AbstractFilter[]}
   * @memberof Node#
   */
  filters: {
    get: function() {
      return this._filters && this._filters.slice();
    },
    set: function(value) {
      this._filters = value && value.slice();
    },
  },

  /**
   * Sets the gfx system this node will be rendered with
   *
   * @member {SystemGfx}
   * @memberof Node#
   */
  system: {
    get: function() {
      return this._system;
    },
    set: function(value) {
      this._system = value;

      if (Array.isArray(this.children)) {
        for (var i = 0; i < this.children.length; i++) {
          this.children[i].system = value;
        }
      }
    },
  },

  /**
   * The width of the Node, setting this will actually modify the scale to achieve the value set
   *
   * @member {Number}
   * @memberof Node#
   */
  width: {
    get: function() {
      return this.scale.x * this.getLocalBounds().width;
    },
    set: function(value) {
      var width = this.getLocalBounds().width;

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

  /**
   * The height of the Node, setting this will actually modify the scale to achieve the value set
   *
   * @member {Number}
   * @memberof Node#
   */
  height: {
    get: function() {
      return this.scale.y * this.getLocalBounds().height;
    },
    set: function(value) {
      var height = this.getLocalBounds().height;

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

/**
 * @module engine/gfx/core/Node
 */
module.exports = Node;