/**
* @class Phaser.Plugin.Isometric.Projector
*
* @classdesc
* Creates a new Isometric Projector object, which has helpers for projecting x, y and z coordinates into axonometric x and y equivalents.
*
* @constructor
* @param {Phaser.Game} game - The current game object.
* @param {number} projectionAngle - The angle of the axonometric projection in radians. Defaults to approx. 0.4636476 (Math.atan(0.5) which is suitable for 2:1 pixel art dimetric)
* @return {Phaser.Plugin.Isometric.Cube} This Cube object.
*/
Phaser.Plugin.Isometric.Projector = function (game, projectionAngle) {
/**
* @property {Phaser.Game} game - The current game object.
*/
this.game = game;
/**
* @property {array} _transform - The pre-calculated axonometric transformation values.
* @private
*/
this._transform = null;
/**
* @property {number} _projectionAngle - The cached angle of projection in radians.
* @private
*/
this._projectionAngle = 0;
/**
* @property {number} projectionAngle - The angle of projection in radians.
* @default
*/
this.projectionAngle = projectionAngle || Phaser.Plugin.Isometric.CLASSIC;
/**
* @property {Phaser.Point} anchor - The x and y offset multipliers as a ratio of the game world size.
* @default
*/
this.anchor = new Phaser.Point(0.5, 0);
};
// Projection angles
Phaser.Plugin.Isometric.CLASSIC = Math.atan(0.5);
Phaser.Plugin.Isometric.ISOMETRIC = Math.PI / 6;
Phaser.Plugin.Isometric.MILITARY = Math.PI / 4;
Phaser.Plugin.Isometric.Projector.prototype = {
/**
* Use axonometric projection to transform a 3D Point3 coordinate to a 2D Point coordinate. If given the coordinates will be set into the object, otherwise a brand new Point object will be created and returned.
* @method Phaser.Plugin.Isometric.Projector#project
* @param {Phaser.Plugin.Isometric.Point3} point3 - The Point3 to project from.
* @param {Phaser.Point} out - The Point to project to.
* @return {Phaser.Point} The transformed Point.
*/
project: function (point3, out) {
if (typeof out === "undefined") {
out = new Phaser.Point();
}
out.x = (point3.x - point3.y) * this._transform[0];
out.y = ((point3.x + point3.y) * this._transform[1]) - point3.z;
out.x += this.game.world.width * this.anchor.x;
out.y += this.game.world.height * this.anchor.y;
return out;
},
/**
* Use axonometric projection to transform a 3D Point3 coordinate to a 2D Point coordinate, ignoring the z-axis. If given the coordinates will be set into the object, otherwise a brand new Point object will be created and returned.
* @method Phaser.Plugin.Isometric.Projector#projectXY
* @param {Phaser.Plugin.Isometric.Point3} point3 - The Point3 to project from.
* @param {Phaser.Point} out - The Point to project to.
* @return {Phaser.Point} The transformed Point.
*/
projectXY: function (point3, out) {
if (typeof out === "undefined") {
out = new Phaser.Point();
}
out.x = (point3.x - point3.y) * this._transform[0];
out.y = (point3.x + point3.y) * this._transform[1];
out.x += this.game.world.width * this.anchor.x;
out.y += this.game.world.height * this.anchor.y;
return out;
},
/**
* Use reverse axonometric projection to transform a 2D Point coordinate to a 3D Point3 coordinate. If given the coordinates will be set into the object, otherwise a brand new Point3 object will be created and returned.
* @method Phaser.Plugin.Isometric.Projector#unproject
* @param {Phaser.Plugin.Isometric.Point} point - The Point to project from.
* @param {Phaser.Plugin.Isometric.Point3} out - The Point3 to project to.
* @param {number} [z] - Specified z-plane to project to.
* @return {Phaser.Plugin.Isometric.Point3} The transformed Point3.
*/
unproject: function (point, out, z) {
if (typeof out === "undefined") {
out = new Phaser.Plugin.Isometric.Point3();
}
z = z || 0;
var x = point.x - this.game.world.x - (this.game.world.width * this.anchor.x);
var y = point.y - this.game.world.y - (this.game.world.height * this.anchor.y) + z;
out.x = x / (2 * this._transform[0]) + y / (2 * this._transform[1]);
out.y = -(x / (2 * this._transform[0])) + y / (2 * this._transform[1]);
out.z = z;
return out;
},
/**
* Perform a simple depth sort on all IsoSprites in the passed group. This function is fast and will accurately sort items on a single z-plane, but breaks down when items are above/below one another in certain configurations.
*
* @method Phaser.Plugin.Isometric.Projector#simpleSort
* @param {Phaser.Group} group - A group of IsoSprites to sort.
*/
simpleSort: function(group) {
group.sort("depth");
},
/**
* Perform a volume-based topological sort on all IsoSprites in the passed group or array. Will use the body if available, otherwise it will use an automatically generated bounding cube. If a group is passed, <code>Phaser.Group#sort</code> is automatically called on the specified property.
* Routine adapted from this tutorial: http://mazebert.com/2013/04/18/isometric-depth-sorting/
*
* @method Phaser.Plugin.Isometric.Projector#topologicalSort
* @param {Phaser.Group|array} group - A group or array of IsoSprites to sort.
* @param {number} [padding] - The amount of extra tolerance in the depth sorting; larger values reduce flickering when objects collide, but also introduce inaccuracy when objects are close. Defaults to 1.5.
* @param {string} [prop] - The property to store the depth information on. If not specified, it will default to 'isoDepth'.
*/
topologicalSort: function (group, padding, prop) {
var children, isGroup;
if (group instanceof Phaser.Group) {
children = group.children;
isGroup = true;
}
else if (group.length) {
children = group;
}
else {
return;
}
prop = prop || "isoDepth";
if (typeof padding === "undefined") {
padding = 1.5;
}
else {
padding = padding;
}
var a, b, i, j, bounds, behindIndex, len = children.length;
for (i = 0; i < len; i++) {
a = children[i];
behindIndex = 0;
if (!a.isoSpritesBehind) {
a.isoSpritesBehind = [];
}
for (j = 0; j < len; j++) {
if (i != j) {
b = children[j];
bounds = a.body || a.isoBounds;
if (b._isoPosition.x + padding < bounds.frontX - padding && b._isoPosition.y + padding < bounds.frontY - padding && b._isoPosition.z + padding < bounds.top - padding) {
a.isoSpritesBehind[behindIndex++] = b;
}
}
}
a.isoVisitedFlag = false;
}
var _sortDepth = 0;
function visitNode(node) {
if (node.isoVisitedFlag === false) {
node.isoVisitedFlag = true;
var spritesBehindLength = node.isoSpritesBehind.length;
for (var k = 0; k < spritesBehindLength; k++) {
if (node.isoSpritesBehind[k] === null) {
break;
}
else {
visitNode(node.isoSpritesBehind[k]);
node.isoSpritesBehind[k] = null;
}
}
node[prop] = _sortDepth++;
}
}
for (i = 0; i < len; i++) {
visitNode(children[i]);
}
if (isGroup) {
group.sort(prop);
}
}
};
/**
* @name Phaser.Plugin.Isometric.Projector#projectionAngle
* @property {number} projectionAngle - The angle of axonometric projection.
*/
Object.defineProperty(Phaser.Plugin.Isometric.Projector.prototype, "projectionAngle", {
get: function () {
return this._projectionAngle;
},
set: function (value) {
if (value === this._projectionAngle) {
return;
}
this._projectionAngle = value;
this._transform = [Math.cos(this._projectionAngle), Math.sin(this._projectionAngle)];
}
});