Vector.js

/* global platypus */
import {arrayCache, greenSlice} from './utils/array.js';
import config from 'config';
import recycle from 'recycle';

export default (function () {
    /**
     * This class defines a multi-dimensional vector object and a variety of methods for manipulating the vector.
     *
     * @memberof platypus
     * @class Vector
     * @param {number|Array|Vector} x The x coordinate or an array or Vector describing the whole vector.
     * @param {number} [y] The y coordinate.
     * @param {number} [z] The z coordinate.
     * @property {number} x The x coordinate.
     * @property {number} [y] The y coordinate.
     * @property {number} [z] The z coordinate.
     */
    var Vector = function (x, y, z) {
            if (this.matrix) { // Recycled vectors will already have a matrix array. Resetting x, y, z to 0's to properly handle a set-up array of less than 3 dimensions.
                this.matrix[0] = 0;
                this.matrix[1] = 0;
                this.matrix[2] = 0;
            } else {
                this.matrix = arrayCache.setUp(0, 0, 0);
            }
            this.set(x, y, z);
        },
        proto = Vector.prototype;
    
    /**
     * The x component of the vector.
     *
     * @memberof platypus.Vector.prototype
     * @member x
     * @type number
     * @default 0
     */
    Object.defineProperty(proto, 'x', {
        get: function () {
            return this.matrix[0];
        },
        set: function (value) {
            this.matrix[0] = value;
        }
    });
    
    /**
     * The y component of the vector.
     *
     * @memberof platypus.Vector.prototype
     * @member y
     * @type number
     * @default 0
     */
    Object.defineProperty(proto, 'y', {
        get: function () {
            return this.matrix[1];
        },
        set: function (value) {
            this.matrix[1] = value;
        }
    });
    
    /**
     * The z component of the vector.
     *
     * @memberof platypus.Vector.prototype
     * @member z
     * @type number
     * @default 0
     */
    Object.defineProperty(proto, 'z', {
        get: function () {
            return this.matrix[2];
        },
        set: function (value) {
            this.matrix[2] = value;
        }
    });
    
    /**
     * Returns a string describing the vector in the format of "[x, y, z]".
     *
     * @method platypus.Vector#toString
     * @return {String}
     */
    proto.toString = function () {
        return '[' + this.matrix.join(',') + ']';
    };
    
    /**
     * Sets the coordinates of the vector.
     *
     * @method platypus.Vector#set
     * @param x {number|Array|Vector} The x coordinate or an array or Vector describing the whole vector.
     * @param [y] {number} The y coordinate, or if x is an array/Vector this is the number of elements to copy from the array/Vector.
     * @param [z] {number} The z coordinate.
     * @chainable
     */
    proto.set = function (x, y, z) {
        if (x && x.matrix) {                // Passing in a vector.
            return this.setVector(x, y);
        } else if (x && (typeof x.x === 'number') && (typeof x.y === 'number')) { // Passing in a vector-like object.
            return this.setXYZ(x.x, x.y, x.z);
        } else if (x && Array.isArray(x)) { // Passing in an array.
            return this.setArray(x, y);
        } else {                            // Passing in coordinates.
            return this.setXYZ(x, y, z);
        }
    };

    /**
     * Sets the coordinates of the vector.
     *
     * @method platypus.Vector#setXYZ
     * @param x {number} The x coordinate.
     * @param [y] {number} The y coordinate.
     * @param [z] {number} The z coordinate.
     * @chainable
     */
    proto.setXYZ = function (x, y, z) {
        var matrix = this.matrix;
        
        matrix[0] = x || 0;
        matrix[1] = y || 0;
        matrix[2] = z || 0;
        
        return this;
    };

    /**
     * Sets the coordinates of the vector.
     *
     * @method platypus.Vector#setVector
     * @param vector {Vector} The Vector to copy.
     * @param [dimensions] {number} The number of elements to copy from the Vector.
     * @chainable
     */
    proto.setVector = function (vector, dimensions) {
        return this.setArray(vector.matrix, dimensions);
    };

    /**
     * Sets the coordinates of the vector.
     *
     * @method platypus.Vector#setArray
     * @param arr {Array} The array to copy.
     * @param [dimensions] {number} The number of elements to copy from the Array.
     * @chainable
     */
    proto.setArray = function (arr, dimensions) {
        var q = dimensions || arr.length,
            matrix = this.matrix;
        
        while (q--) {
            matrix[q] = arr[q];
        }

        return this;
    };

    /**
     * Determines whether two vectors are equal.
     *
     * @method platypus.Vector#equals
     * @param x {number|Array|Vector} The x coordinate or an array or Vector to check against.
     * @param [y] {number} The y coordinate, or if x is an array/Vector this is the number of dimensions to check from the array/Vector.
     * @param [z] {number} The z coordinate.
     * @return {Boolean} Whether the vectors are equal.
     */
    proto.equals = function (x, y, z) {
        var m = null,
            q = 0,
            matrix = this.matrix;
        
        if (x && Array.isArray(x)) {   // Passing in an array.
            q = y || x.length;
            while (q--) {
                if (matrix[q] !== x[q]) {
                    return false;
                }
            }
            return true;
        } else if (x && x.matrix) {   // Passing in a vector.
            m = x.matrix;
            q = y || m.length;
            while (q--) {
                if (matrix[q] !== m[q]) {
                    return false;
                }
            }
            return true;
        } else {                     // Passing in coordinates.
            return ((typeof x === 'number') && (matrix[0] === x)) && ((typeof y !== 'number') || (matrix[1] === y)) && ((typeof z !== 'number') || (matrix[2] === z));
        }
    };
    
    /**
     * Returns the magnitude of the vector.
     *
     * @method platypus.Vector#magnitude
     * @param [dimensions] {number} The dimensions to include. Defaults to all dimensions.
     * @return {number} The magnitude of the vector.
     */
    proto.magnitude = function (dimensions) {
        return Math.sqrt(this.magnitudeSquared(dimensions));
    };
    
    /**
     * Returns the magnitude squared of the vector. This is slightly faster than finding the magnitude.
     *
     * @method platypus.Vector#magnitudeSquared
     * @param [dimensions] {number} The dimensions to include. Defaults to all dimensions.
     * @return {number} The magnitude squared of the vector.
     */
    proto.magnitudeSquared = function (dimensions) {
        var squares = 0,
            x = 0;

        dimensions = dimensions || this.matrix.length;

        for (x = 0; x < dimensions; x++) {
            squares += Math.pow(this.matrix[x], 2);
        }

        return squares;
    };
    
    /**
     * Returns the direction of the vector from the z-axis
     *
     * @method platypus.Vector#getAngle
     * @return {number} The direction of the vector in radians.
     */
    proto.getAngle = function () {
        var mag   = this.magnitude(2),
            angle = 0;

        if (mag !== 0) {
            angle = Math.acos(this.x / mag);
            if (this.y < 0) {
                angle = (Math.PI * 2) - angle;
            }
        }
        return angle;
    };
    
    /**
     * Returns a normalized copy of the vector.
     *
     * @method platypus.Vector#getUnit
     * @return {platypus.Vector} A normalized vector in the same direction as this vector.
     */
    proto.getUnit = function () {
        return Vector.setUp(this).normalize();
    };
    
    /**
     * Returns a copy of the Vector inverted.
     *
     * @method platypus.Vector#getInverse
     * @return {platypus.Vector}
     */
    proto.getInverse = function () {
        return Vector.setUp(this).multiply(-1);
    };
    
    /**
     * Normalizes the vector.
     *
     * @method platypus.Vector#normalize
     * @chainable
     */
    proto.normalize = function () {
        var mag = this.magnitude();
        
        if (mag === 0) {
            // Ignores attempt to normalize a vector of zero magnitude.
            return this;
        } else {
            return this.multiply(1 / mag);
        }
    };
    
    /**
     * Crosses this vector with the parameter vector.
     *
     * @method platypus.Vector#cross
     * @param vector {platypus.Vector} The vector to cross this vector with.
     * @chainable
     */
    proto.cross = (function () {
        var det = function (a, b, c, d) {
            return a * d - b * c;
        };
        
        return function (v) {
            var tempX = det(this.y, this.z, v.y, v.z),
                tempY = -det(this.x, this.z, v.x, v.z),
                tempZ = det(this.x, this.y, v.x, v.y);
            
            this.x = tempX;
            this.y = tempY;
            this.z = tempZ;
            
            return this;
        };
    }());
    
    /**
     * Crosses this vector with the parameter vector and returns the cross product.
     *
     * @method platypus.Vector#getCrossProduct
     * @param vector {platypus.Vector} The vector to cross this vector with.
     * @return {platypus.Vector} The cross product.
     */
    proto.getCrossProduct = function (v) {
        return Vector.setUp(this).cross(v);
    };
    
    /**
     * Rotates the vector by the given amount.
     *
     * @method platypus.Vector#rotate
     * @param angle {number} The amount to rotate the vector in radians.
     * @param [axis="z"] {String|Vector} A vector describing the axis around which the rotation should occur or 'x', 'y', or 'z'.
     * @chainable
     */
    proto.rotate = function (angle, axis) {
        var a    = axis,
            arr  = null,
            cos  = Math.cos(angle),
            sin  = Math.sin(angle),
            icos = 1 - cos,
            x    = 0,
            y    = 0,
            z    = 0,
            temp = Vector.setUp();
        
        if (a) {
            if (a === 'x') {
                a = temp.setXYZ(1, 0, 0);
            } else if (a === 'y') {
                a = temp.setXYZ(0, 1, 0);
            } else if (a === 'z') {
                a = temp.setXYZ(0, 0, 1);
            }
        } else {
            a = temp.setXYZ(0, 0, 1);
        }
        
        x     = a.x;
        y     = a.y;
        z     = a.z;
        
        arr = arrayCache.setUp(
            arrayCache.setUp(    cos + x * x * icos, x * y * icos - z * sin, x * z * icos + y * sin),
            arrayCache.setUp(y * x * icos + z * sin,     cos + y * y * icos, y * z * icos - x * sin),
            arrayCache.setUp(z * x * icos - y * sin, z * y * icos + x * sin,     cos + z * z * icos)
        );
        
        this.multiply(arr);

        temp.recycle();
        arrayCache.recycle(arr, 2);
        
        return this;
    };

    /**
     * Rotates the vector position around a given point on the cartesian plane.
     *
     * @method platypus.Vector#rotateAbout
     * @param point {Vector} A vector describing the point around which the rotation should occur.
     * @param angle {number} The amount to rotate the vector in radians.
     * @chainable
     */
    proto.rotateAbout = function (point, angle) {
        const cos = Math.cos(angle),
            sin = Math.sin(angle),
            dx = this.x - point.x,
            dy = this.y - point.y;

        this.x = point.x + (dx * cos - dy * sin);
        this.y = point.y + (dx * sin + dy * cos);

        return this;
    };
    
    /**
     * Scales the vector by the given factor or performs a transform if a matrix is provided.
     *
     * @method platypus.Vector#multiply
     * @param multiplier {number|Array} The factor to scale by or a 2D array describing a multiplication matrix.
     * @param limit {number} For scaling, determines which coordinates are affected.
     * @chainable
     */
    proto.multiply = function (multiplier, limit) {
        const
            matrix = this.matrix;
        
        if (Array.isArray(multiplier)) {
            const
                arr = greenSlice(matrix);

            if (multiplier.length === 2) {
                matrix[0] = arr[0] * multiplier[0][0] + arr[1] * multiplier[0][1];
                matrix[1] = arr[0] * multiplier[1][0] + arr[1] * multiplier[1][1];
            } else if (multiplier.length >= 3) {
                matrix[0] = arr[0] * multiplier[0][0] + arr[1] * multiplier[0][1] + arr[2] * multiplier[0][2];
                matrix[1] = arr[0] * multiplier[1][0] + arr[1] * multiplier[1][1] + arr[2] * multiplier[1][2];
                matrix[2] = arr[0] * multiplier[2][0] + arr[1] * multiplier[2][1] + arr[2] * multiplier[2][2];
            }

            arrayCache.recycle(arr);
        } else {
            const
                l = limit || matrix.length;

            for (let i = 0; i < l; i++) {
                matrix[i] *= multiplier;
            }
        }
        
        return this;
    };
    
    /**
     * Adds the given components to this vector.
     *
     * @method platypus.Vector#add
     * @param x {number|Array|Vector} The x component to add, or an array or vector describing the whole addition.
     * @param [y] {number} The y component to add or the limit if the first parameter is a vector or array.
     * @param [z] {number} The z component to add.
     * @chainable
     */
    proto.add = function (x, y, z) {
        var addMatrix = x,
            limit = 0,
            q = 0;

        if (!Array.isArray(addMatrix)) {
            if (addMatrix instanceof Vector) {
                addMatrix = addMatrix.matrix;
                limit = y || this.matrix.length;
            } else {
                addMatrix = [x || 0, y || 0, z || 0];
                limit = this.matrix.length;
            }
        } else {
            limit = y || this.matrix.length;
        }
        
        for (q = 0; q < limit; q++) {
            this.matrix[q] += addMatrix[q];
        }
        
        return this;
    };
    
    /**
     * Adds the given vector to this vector.
     *
     * @method platypus.Vector#addVector
     * @param otherVector {platypus.Vector} The vector to add.
     * @chainable
     */
    proto.addVector = function (otherVector, dimensions) {
        return this.add(otherVector, dimensions);
    };
    
    /**
     * Subtracts the given vector from this vector.
     *
     * @method platypus.Vector#subtractVector
     * @param otherVector {platypus.Vector} The vector to subtract.
     * @chainable
     */
    proto.subtractVector = function (otherVector, dimensions) {
        var inv = otherVector.getInverse();

        this.add(inv, dimensions);
        inv.recycle();

        return this;
    };

    /**
     * Returns the perpendicular vector.
     *
     * @method platypus.Vector#perpendicular
     * @param opposite {Boolean} Whether to negate the perpendicular vector.
     * @chainable
     */
    proto.perpendicular = function (negate) {
        const matrix = this.matrix,
            mult = (negate === true) ? -1 : 1,
            x = -this.matrix[1];

        matrix[1] = matrix[0];
        matrix[0] = x;

        if (negate) {
            matrix[1] *= mult;
            matrix[0] *= mult;
        }

        return this;
    };
    
    /**
     * Scales the vector by the given factor.
     *
     * @method platypus.Vector#multiply
     * @param factor {number} The factor to scale by.
     * @param limit {number} Determines which coordinates are affected. Defaults to all coordinates.
     * @chainable
     */
    proto.scale = function (factor, limit) {
        return this.multiply(factor, limit);
    };
    
    /**
     * Finds the dot product of the two vectors.
     *
     * @method platypus.Vector#dot
     * @param otherVector {platypus.Vector} The other vector.
     * @param limit {number} The number of vector indexes to include in the dot product.
     * @return {number} The dot product.
     */
    proto.dot = function (otherVector, limit) {
        var sum = 0,
            q = 0,
            m = this.matrix,
            oM = otherVector.matrix;
            
        q = limit || m.length;
        
        while (q--) {
            sum += m[q] * (oM[q] || 0);
        }
        
        return sum;
    };
    
    /**
     * Finds the shortest angle between the two vectors.
     *
     * @method platypus.Vector#angleTo
     * @param otherVector {platypus.Vector} The other vector.
     * @return {number} The angle between this vector and the received vector.
     */
    proto.angleTo = function (otherVector) {
        var v1 = this.getUnit(),
            v2 = otherVector.getUnit(),
            ang = 0;
            
        if (v1.magnitude() && v2.magnitude()) { // Probably want a less expensive check here for zero-length vectors.
            ang = Math.acos(v1.dot(v2));
        } else {
            platypus.debug.warn('Vector: Attempted to find the angle of a zero-length vector.');
            ang = NaN;
        }
            
        v1.recycle();
        v2.recycle();
        
        return ang;
    };
    
    /**
     * Finds the shortest signed angle between the two vectors.
     *
     * @method platypus.Vector#signedAngleTo
     * @param otherVector {platypus.Vector} The other vector.
     * @param normal {platypus.Vector} A normal vector determining the resultant sign of the angle between two vectors.
     * @return {number} The angle between this vector and the received vector.
     */
    proto.signedAngleTo = function (otherVector, normal) {
        var v1 = this.getUnit(),
            v2 = otherVector.getUnit(),
            v3 = v1.getCrossProduct(v2),
            ang = 0;
        
        if (v3.magnitude() === 0) {
            ang = 0;
        } else if (v3.dot(normal) < 0) {
            ang = -Math.acos(v1.dot(v2));
        } else {
            ang =  Math.acos(v1.dot(v2));
        }
        
        v1.recycle();
        v2.recycle();
        v3.recycle();
        
        return ang;
    };
    
    /**
     * Find the scalar value of projecting this vector onto the parameter vector or onto a vector at the specified angle away.
     *
     * @method platypus.Vector#scalerProjection
     * @param vectorOrAngle {Vector|number} The other vector or the angle between the vectors.
     * @return {number} The magnitude of the projection.
     */
    proto.scalarProjection = function (vectorOrAngle) {
        var v = null,
            d = 0;
        
        if (typeof vectorOrAngle === "number") {
            return this.magnitude(2) * Math.cos(vectorOrAngle);
        } else {
            v = Vector.setUp(vectorOrAngle).normalize();
            d = this.dot(v);
            v.recycle();
            return d;
        }
    };
    
    /**
     * Returns a copy of this vector.
     *
     * @method platypus.Vector#copy
     * @return {platypus.Vector} A copy of this vector.
     */
    proto.copy = function () {
        return Vector.setUp(this);
    };
    
    /**
     * Adds properties to an object that describe the coordinates of a vector.
     *
     * @method platypus.Vector.assign
     * @param object {Object} Object on which the coordinates and vector will be added.
     * @param propertyName {String} A string describing the property name where the vector is accessable.
     * @param [coordinateName*] {String} One or more parameters describing coordinate values on the object.
     */
    Vector.assign = (function () {
        var createProperty = function (property, obj, vector, index) {
            var temp = null,
                propertyInUse = false;
            
            if (typeof property === 'string') {
                if (typeof obj[property] !== 'undefined') {
                    temp = obj[property];
                    delete obj[property];
                    propertyInUse = true;
                }
            }
            
            Object.defineProperty(obj, property, {
                get: function () {
                    return vector.matrix[index];
                },
                set: function (value) {
                    vector.matrix[index] = value;
                },
                enumerable: true
            });
            
            if (propertyInUse) {
                obj[property] = temp;
            }
        };
        
        return function (obj, prop) {
            var i = 0;

            if (obj && prop) {
                if (!obj[prop]) {
                    obj[prop] = Vector.setUp();
                    
                    for (i = 2; i < arguments.length; i++) {
                        if (arguments[i] !== prop) {
                            createProperty(arguments[i], obj, obj[prop], i - 2);
                        }
                    }
                    
                    return null;
                }
                return obj[prop];
            } else {
                return null;
            }
        };
    }());

    /**
     * Returns a Vector from cache or creates a new one if none are available.
     *
     * @method platypus.Vector.setUp
     * @return {platypus.Vector} The instantiated Vector.
     */
    /**
     * Returns a Vector back to the cache. Prefer the Vector's recycle method since it recycles property objects as well.
     *
     * @method platypus.Vector.recycle
     * @param {platypus.Vector} vector The Vector to be recycled.
     */
    /**
     * Relinquishes properties of the vector and recycles it.
     *
     * @method platypus.Vector#recycle
     */
    recycle.add(Vector, 'Vector', Vector, function () {
        this.matrix.length = 0;
    }, true, config.dev);
    
    return Vector;
}());