/** * @author Richard Davey * @copyright 2016 Photon Storm Ltd. * @license {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License} */ var MATH_CONST = require('../math/const'); var WrapAngle = require('../math/angle/Wrap'); /** * 2D Transformation Component. * * @class */ var Transform = function (gameObject, x, y, scaleX, scaleY) { if (x === undefined) { x = 0; } if (y === undefined) { y = 0; } if (scaleX === undefined) { scaleX = 1; } if (scaleY === undefined) { scaleY = 1; } this.gameObject = gameObject; this.state = (gameObject.state) ? gameObject.state : gameObject.parent.state; this.game = this.state.game; // a = scale X // b = shear Y // c = shear X // d = scale Y // tx / ty = translation // World Transform this.world = { a: scaleX, b: 0, c: 0, d: scaleY, tx: x, ty: y }; // Previous Transform (used for interpolation) this.old = { a: scaleX, b: 0, c: 0, d: scaleY, tx: x, ty: y }; // Cached Transform Calculations this.cache = { a: 1, b: 0, c: 0, d: 1, sr: 0, cr: 0 }; // GL Vertex Data this.glVertextData = { x0: 0, y0: 0, x1: 0, y1: 0, x2: 0, y2: 0, x3: 0, y3: 0 }; // Canvas SetTransform Data this.canvasData = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 }; this.immediate = false; this.interpolate = false; this.hasLocalRotation = false; // Private value holders, accessed via the getters and setters this._posX = x; this._posY = y; this._scaleX = scaleX; this._scaleY = scaleY; this._rotation = 0; this._pivotX = 0; this._pivotY = 0; this._anchorX = 0; this._anchorY = 0; this._worldRotation = 0; this._worldScaleX = scaleX; this._worldScaleY = scaleY; this._dirty = true; this._dirtyVertex = true; this.state.sys.updates.add(this); // The parent Transform (NOT the parent GameObject, although very often they are related) this.parent = null; // Any child Transforms of this one - note that they don't have to belong to Game Objects // that are children of the owner of this Transform this.children = []; }; Transform.prototype.constructor = Transform; Transform.prototype = { add: function (child) { return this.addAt(child, this.children.length); }, addAt: function (child, index) { // Invalid child? if (child === this || child.parent === this || index < 0 || index > this.children.length) { console.log('Invalid child'); return child; } // Child already parented? Remove it if (child.parent) { child.parent.remove(child); } child.parent = this; this.children.splice(index, 0, child); this.dirty = true; this.updateAncestors(); return child; }, remove: function (child) { // Invalid child? if (child === this || child.parent !== this) { return child; } var index = this.children.indexOf(child); if (index !== -1) { return this.removeAt(index); } }, removeAt: function (index) { // Valid index? if (index >= 0 && index < this.children.length) { var child = this.children.splice(index, 1); if (child[0]) { child[0].parent = null; return child[0]; } } }, enableInterpolation: function () { this.interpolate = true; this.syncInterpolation(); }, syncInterpolation: function () { this._dirty = true; this.update(); var old = this.old; var world = this.world; old.a = world.a; old.b = world.b; old.c = world.c; old.d = world.d; old.tx = world.tx; old.ty = world.ty; }, disableInterpolation: function () { this.interpolate = false; }, setPosition: function (x, y) { if (y === undefined) { y = x; } this._posX = x; this._posY = y; return this.update(); }, setScale: function (x, y) { if (y === undefined) { y = x; } this._scaleX = x; this._scaleY = y; this.updateCache(); return this.update(); }, setPivot: function (x, y) { if (y === undefined) { y = x; } this._pivotX = x; this._pivotY = y; return this.update(); }, setAnchor: function (x, y) { if (y === undefined) { y = x; } this._anchorX = x; this._anchorY = y; this.dirty = true; }, setRotation: function (rotation) { this.rotation = rotation; return this.update(); }, // Updates the Transform.world object, ready for rendering // Assuming this Transform is a root node (i.e. no transform parent) updateFromRoot: function () { var old = this.old; var world = this.world; old.a = world.a; old.b = world.b; old.c = world.c; old.d = world.d; old.tx = world.tx; old.ty = world.ty; if (this.hasLocalRotation) { // console.log(this.name, 'Transform.updateFromRoot'); world.a = this.cache.a; world.b = this.cache.b; world.c = this.cache.c; world.d = this.cache.d; world.tx = this._posX - ((this._pivotX * this.cache.a) + (this._pivotY * this.cache.c)); world.ty = this._posY - ((this._pivotX * this.cache.b) + (this._pivotY * this.cache.d)); this._worldRotation = Math.atan2(-this.cache.c, this.cache.d); } else { // console.log(this.name, 'Transform.updateFromRoot FAST'); world.a = this._scaleX; world.b = 0; world.c = 0; world.d = this._scaleY; world.tx = this._posX - (this._pivotX * this._scaleX); world.ty = this._posY - (this._pivotY * this._scaleY); this._worldRotation = 0; } this._worldScaleX = this._scaleX; this._worldScaleY = this._scaleY; return this; }, updateFromParent: function () { var old = this.old; var world = this.world; old.a = world.a; old.b = world.b; old.c = world.c; old.d = world.d; old.tx = world.tx; old.ty = world.ty; var parent = this.parent.world; var tx = 0; var ty = 0; if (this.hasLocalRotation) { // console.log(this.name, 'Transform.updateFromParent', this.parent.name); var a = this.cache.a; var b = this.cache.b; var c = this.cache.c; var d = this.cache.d; tx = this._posX - ((this._pivotX * a) + (this._pivotY * c)); ty = this._posY - ((this._pivotX * b) + (this._pivotY * d)); world.a = (a * parent.a) + (b * parent.c); world.b = (a * parent.b) + (b * parent.d); world.c = (c * parent.a) + (d * parent.c); world.d = (c * parent.b) + (d * parent.d); } else { // console.log(this.name, 'Transform.updateFromParent FAST', this.parent.name); tx = this._posX - (this._pivotX * this._scaleX); ty = this._posY - (this._pivotY * this._scaleY); world.a = this._scaleX * parent.a; world.b = this._scaleX * parent.b; world.c = this._scaleY * parent.c; world.d = this._scaleY * parent.d; } this._worldRotation = Math.atan2(-this.world.c, this.world.d); world.tx = (tx * parent.a) + (ty * parent.c) + parent.tx; world.ty = (tx * parent.b) + (ty * parent.d) + parent.ty; this._worldScaleX = this._scaleX * Math.sqrt((world.a * world.a) + (world.c * world.c)); this._worldScaleY = this._scaleY * Math.sqrt((world.b * world.b) + (world.d * world.d)); return this; }, updateAncestors: function () { // console.log(this.name, 'Transform.updateAncestors'); // No parent? Then just update the children and leave, our job is done if (!this.parent) { // console.log(this.name, 'updateAncestors has no parent Transform'); this.updateFromRoot(); this.updateChildren(); this.dirty = false; return this; } // console.log(this.name, 'start updateAncestors while'); // Gets all parent nodes, starting from this Transform. // Then updates from the top, down, but only on the ancestors, // not any other children - will give us accurate worldX etc properties var node = this.parent; var nodes = []; do { nodes.push(node); node = node.parent; } while (node); // We've got all the ancestors in the 'nodes' array, let's loop it while (nodes.length) { node = nodes.pop(); if (node.parent) { node.updateFromParent(); } else { node.updateFromRoot(); } } // By this point all of this Transforms ancestors have been // updated, in the correct order, so we can now do this one // and any of its children too this.update(); }, updateChildren: function () { // console.log(this.name, 'Transform.updateChildren'); for (var i = 0; i < this.children.length; i++) { this.children[i].update(); } }, updateFromDirtyParent: function () { // console.log(this.name, 'is updateFromDirtyParent', this.parent.name); this.updateFromParent(); if (this.children.length) { for (var i = 0; i < this.children.length; i++) { this.children[i].updateFromDirtyParent(); } } this._dirty = false; this._dirtyVertex = true; }, update: function () { if (!this._dirty) { return; } // If we got this far then this Transform is dirty // so we need to update it from its parent // and then force the update to all children if (this.parent) { this.updateFromParent(); } else { this.updateFromRoot(); } var len = this.children.length; if (len) { for (var i = 0; i < len; i++) { this.children[i].updateFromDirtyParent(); } } this._dirty = false; this._dirtyVertex = true; }, updateCache: function () { this.cache.a = this.cache.cr * this._scaleX; this.cache.b = this.cache.sr * this._scaleX; this.cache.c = -this.cache.sr * this._scaleY; this.cache.d = this.cache.cr * this._scaleY; }, updateVertexData: function (interpolationPercentage) { if (!this.gameObject.frame || (!this._dirtyVertex && !this.interpolate)) { return; } var frame = this.gameObject.frame; var w0; var h0; var w1; var h1; if (frame.data.trim) { // If the sprite is trimmed, add the extra space before transforming w1 = frame.x - (this._anchorX * frame.width); w0 = w1 + frame.cutWidth; h1 = frame.y - (this._anchorY * frame.height); h0 = h1 + frame.cutHeight; } else { w0 = frame.cutWidth * (1 - this._anchorX); w1 = frame.cutWidth * -this._anchorX; h0 = frame.cutHeight * (1 - this._anchorY); h1 = frame.cutHeight * -this._anchorY; } var resolution = frame.source.resolution; var wt = this.world; var a = wt.a / resolution; var b = wt.b / resolution; var c = wt.c / resolution; var d = wt.d / resolution; var tx = wt.tx; var ty = wt.ty; if (this.interpolate) { var old = this.old; // Interpolate with the last position to reduce stuttering. a = old.a + ((a - old.a) * interpolationPercentage); b = old.b + ((b - old.b) * interpolationPercentage); c = old.c + ((c - old.c) * interpolationPercentage); d = old.d + ((d - old.d) * interpolationPercentage); tx = old.tx + ((tx - old.tx) * interpolationPercentage); ty = old.ty + ((ty - old.ty) * interpolationPercentage); } if (frame.rotated) { // var cw = frame.cutWidth; var ch = frame.height; var a0 = a; var b0 = b; var c0 = c; var d0 = d; var _w1 = w1; var _w0 = w0; // Offset before rotating tx = (wt.c * ch) + tx; ty = (wt.d * ch) + ty; // Rotate matrix by 90 degrees with precalc values for sine and cosine of rad(90) a = (a0 * 6.123233995736766e-17) + -c0; b = (b0 * 6.123233995736766e-17) + -d0; c = a0 + (c0 * 6.123233995736766e-17); d = b0 + (d0 * 6.123233995736766e-17); // Update UV coordinates frame.updateUVsInverted(); // Rotate dimensions w0 = h0; w1 = h1; h0 = _w0; h1 = _w1; } if (frame.autoRound === 1 || (frame.autoRound === -1 && this.game.renderer.roundPixels)) { tx |= 0; ty |= 0; } var vert = this.glVertextData; // Top Left Vert vert.x0 = (a * w1) + (c * h1) + tx; vert.y0 = (d * h1) + (b * w1) + ty; // Top Right Vert vert.x1 = (a * w0) + (c * h1) + tx; vert.y1 = (d * h1) + (b * w0) + ty; // Bottom Right Vert vert.x2 = (a * w0) + (c * h0) + tx; vert.y2 = (d * h0) + (b * w0) + ty; // Bottom Left Vert vert.x3 = (a * w1) + (c * h0) + tx; vert.y3 = (d * h0) + (b * w1) + ty; return vert; }, getVertexData: function (interpolationPercentage) { if (this.interpolate || this._dirtyVertex) { this.updateVertexData(interpolationPercentage); this._dirtyVertex = false; } return this.glVertextData; }, cloneVertexData: function () { var src = this.glVertextData; return { x0: src.x0, y0: src.y0, x1: src.x1, y1: src.y1, x2: src.x2, y2: src.y2, x3: src.x3, y3: src.y3 }; }, getCanvasTransformData: function (interpolationPercentage) { var world = this.world; var data = this.canvasData; if (this.interpolate) { var old = this.old; // Interpolate with the last position to reduce stuttering. data.a = old.a + ((world.a - old.a) * interpolationPercentage); data.b = old.b + ((world.b - old.b) * interpolationPercentage); data.c = old.c + ((world.c - old.c) * interpolationPercentage); data.d = old.d + ((world.d - old.d) * interpolationPercentage); data.tx = old.tx + ((world.tx - old.tx) * interpolationPercentage); data.ty = old.ty + ((world.ty - old.ty) * interpolationPercentage); } else { // Copy over the values to the canvasData object, in case the renderer needs to adjust them data.a = world.a; data.b = world.b; data.c = world.c; data.d = world.d; data.tx = world.tx; data.ty = world.ty; } return data; } }; Object.defineProperties(Transform.prototype, { // Transform getters / setters x: { enumerable: true, get: function () { return this._posX; }, set: function (value) { this._posX = value; this.dirty = true; } }, y: { enumerable: true, get: function () { return this._posY; }, set: function (value) { this._posY = value; this.dirty = true; } }, scale: { enumerable: true, get: function () { return this._scaleX; }, set: function (value) { this._scaleX = value; this._scaleY = value; this.dirty = true; this.updateCache(); } }, scaleX: { enumerable: true, get: function () { return this._scaleX; }, set: function (value) { this._scaleX = value; this.dirty = true; this.updateCache(); } }, scaleY: { enumerable: true, get: function () { return this._scaleY; }, set: function (value) { this._scaleY = value; this.dirty = true; this.updateCache(); } }, anchor: { enumerable: true, get: function () { return this._anchorX; }, set: function (value) { this.setAnchor(value); } }, anchorX: { enumerable: true, get: function () { return this._anchorX; }, set: function (value) { this._anchorX = value; this.dirty = true; } }, anchorY: { enumerable: true, get: function () { return this._anchorY; }, set: function (value) { this._anchorY = value; this.dirty = true; } }, pivotX: { enumerable: true, get: function () { return this._pivotX; }, set: function (value) { this._pivotX = value; this.dirty = true; this.updateCache(); } }, pivotY: { enumerable: true, get: function () { return this._pivotY; }, set: function (value) { this._pivotY = value; this.dirty = true; this.updateCache(); } }, angle: { enumerable: true, get: function () { return WrapAngle(this.rotation * MATH_CONST.RAD_TO_DEG); }, set: function (value) { this.rotation = WrapAngle(value) * MATH_CONST.DEG_TO_RAD; } }, rotation: { enumerable: true, get: function () { return this._rotation; }, set: function (value) { if (this._rotation === value) { return; } this._rotation = value; this.dirty = true; if (this._rotation % MATH_CONST.PI2) { this.cache.sr = Math.sin(this._rotation); this.cache.cr = Math.cos(this._rotation); this.updateCache(); this.hasLocalRotation = true; } else { this.hasLocalRotation = false; } } }, // Sets this *component* as being dirty dirty: { enumerable: true, get: function () { return this._dirty; }, set: function (value) { if (value) { if (!this._dirty) { this._dirty = true; if (this.immediate) { this.update(); } else { this._dirtyVertex = true; this.state.sys.updates.add(this); } } } else { this._dirty = false; } } }, // GLOBAL read-only properties from here on // Need *all* parents taken into account to get the correct values name: { enumerable: true, get: function () { return (this.gameObject) ? this.gameObject.name : ''; } }, worldRotation: { enumerable: true, get: function () { this.updateAncestors(); return this._worldRotation; } }, worldScaleX: { enumerable: true, get: function () { this.updateAncestors(); return this._worldScaleX; } }, worldScaleY: { enumerable: true, get: function () { this.updateAncestors(); return this._worldScaleY; } }, worldX: { enumerable: true, get: function () { this.updateAncestors(); return this.world.tx; } }, worldY: { enumerable: true, get: function () { this.updateAncestors(); return this.world.ty; } } }); module.exports = Transform;