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;