Home Manual Reference Source Test Repository

lib/magik-vector.js

/**
 * magikVector class for handling 2 dimensional, 3 dimensional or n dimensional
 * vectors
 *
 * @example
 * <code>
 * // import the vector and create a new vector object
 * const MagikVector = import 'MagikVector';
 * const vector = new MagikVector();
 *
 * // initialise a new vector with `x` and `y` coordinates
 * const vector2D = new MagikVector(12, 42);
 *
 * // it's also possible to add a `z` coordinate
 * const vector3D = new MagikVector(15, 12, 71);
 *
 * // or even more coordinates
 * const multiDimensional = new MagikVector(15, 12, 71, 7, 38, 0);
 *
 * // create a MagikVector from an Array
 * const myCoordinates = [4, 6, 28, 5, 33, 12, 8, 22, 785, 38, 56];
 * const multiDimensional = new MagikVector(...myCoordinates);
 * </code>
 */
class MagikVector {

    /**
     * Initialise a new Vector instance with coordinates as arguments, either
     * supply the individual coordinates or supply an array with number values.
     *
     * @example
     * <code>
     *      // two dimensional
     * const vector2D = new MagikVector(12, 15);
     *
     * // three dimensional
     * const vector3D = new MagikVector(12, 15, 71);
     *
     * // multi dimensional
     * const multiDmensional = new MagikVector(3, 4, 5, 99, 12, 14, 42);
     *
     * // from Array
     * const myCoordinates = [4, 6, 28, 5, 33, 12, 8, 22, 785, 38, 56];
     * const multiDimensional = new MagikVector(...myCoordinates);
     * </code>
     * @constructor
     * @param {...number|array}[args] optional coordinate list or Array
     */
    constructor(...args) {

        this.coordinates = [];

        args.forEach((coordinate, index) => {

            this.coordinates[index] = coordinate;

        });

        this.length = this.coordinates.length;

    }

    /**
     * Returns a new Vector with random coordinates, defaults to a 3D vector
     *
     * @param [dimensions=3] number of dimensions to use
     * @static
     * @returns {MagikVector}
     */
    static random(dimensions = 3) {

        const coordinates = [];

        for(let i = 0; i < dimensions; ++i) {

            coordinates[i] = Math.random();

        }

        return (new MagikVector(...coordinates)).normalise();

    }

    /**
     * Alias for random()
     *
     * @alias MagikVector.random()
     * @returns {MagikVector}
     */
    static rand(dimensions) {

        return MagikVector.random(dimensions);

    }

    /**
     * Returns a random integer optionally bound by the minimum(included) and
     * maximum (included) arguments. If only one argument is supplied, it is
     * the maximum number (same as `MagikVector.randomInteger(0, maximum)`)
     *
     * @param {number} [minimum] minimal value
     * @param {number} [maximum] maximal value
     * @returns {number}
     */
    static randomInteger(...args) {

        let response;

        // two arguments given, minimum and maximum
        if(args.length === 2) {

            const minimum = Math.ceil(args[0]);
            const maximum = Math.floor(args[1]);
            response = Math.floor(Math.random() * (maximum - minimum + 1)) + minimum;

        } else {

            const maximum = args[0] ? Math.floor(args[0]) : Number.MAX_SAFE_INTEGER;
            response = Math.floor(Math.random() * (maximum + 1));

        }

        return response;

    }

    /**
     * @alias MagikVector.randomInteger
     */
    static randomInt(minimal, maximal) {

        return MagikVector.randomInteger(minimal, maximal);

    }

    /**
     * Converts degrees to radians
     *
     * @example
     * <code>
     * radians = MagikVector.toRadians(degrees);
     * </code>
     * @param {number} degrees
     * @returns {number}
     */
    static toRadians(degrees) {

        return degrees * (Math.PI / 180);

    }

    /**
     * Converts radians to degrees
     *
     * @example
     * <code>
     * degrees = MagikVector.toDegrees(radians);
     * </code>
     * @param {number} radians
     * @returns {number}
     */
    static toDegrees(radians) {

        return radians * (180 / Math.PI);

    }

    /**
     * Returns the `x` coordinate of this Vector.
     *
     * @returns {number} the x coordinate
     */
    getX() {

        return this.coordinates[0];

    }

    /**
     * Returns the `y` coordinate of this Vector
     *
     * @returns {number} the x coordinate
     */
    getY() {

        return this.coordinates[1];

    }

    /**
     * Returns the `z` coordinate of this Vector
     *
     * @returns {number} the x coordinate
     */
    getZ() {

        return this.coordinates[2];

    }

    /**
     * Returns the coordinate at the specified index, consider using getX(),
     * getY() and getZ() to retrieve the coordinates of a 2D or 3D vector
     *
     * @see getX
     * @see getY
     * @see getX
     * @param index
     * @returns {number|undefined} the coordinate at the specified index or undefined
     */
    getCoordinate(index) {

        return this.coordinates[index];

    }

    /**
     * Alias of getCoordinate
     *
     * @alias this.getCoordinate
     */
    getCoord(index) {

        return this.getCoordinate(index);

    }

    /**
     * Sets the `x` coordinate of this Vector.
     *
     * @param {number} value the value to set
     * @returns {MagikVector} the object itself
     */
    setX(value) {

        this.coordinates[0] = value;
        return this;

    }

    /**
     * Returns the `y` coordinate of this Vector
     *
     * @param {number} value the value to set
     * @returns {MagikVector} the object itself
     */
    setY(value) {

        this.coordinates[1] = value;
        return this;

    }

    /**
     * Returns the `z` coordinate of this Vector
     *
     * @param {number} value the value to set
     * @returns {MagikVector} the object itself
     */
    setZ(value) {

        this.coordinates[2] = value;
        return this;

    }

    /**
     * Sets the coordinate at the specified index, consider using setX(),
     * setY() and setZ() to set the coordinates of a 2D or 3D vector
     *
     * @see setX
     * @see setY
     * @see setX
     * @param {number} index the  index to set the value for
     * @param {number} value the value to set
     * @returns {MagikVector} the object itself
     */
    setCoordinate(index, value) {

        this.coordinates[index] = value;
        return this;

    }

    /**
     * Alias of setCoordinate
     *
     * @alias this.getCoordinate
     */
    setCoord(index, value) {

        this.setCoordinate(index, value);
        return this;

    }

    /**
     * Adds given vector to the current vector, i.e. adds the individual
     * coordinates.
     *
     * @param {Vector} vector
     * @returns {MagikVector} returns the current Vector
     */
    add(vector) {

        if(this.length !== vector.length) {

            throw new RangeError(`add(): Vectors must have the same number of coordinates, got ${this.length} and ${vector.length}`);

        }

        this.coordinates.forEach((coordinate, index) => {

            this.coordinates[index] += vector.coordinates[index];

        });

        return this;

    }

    /**
     * subtracts given vector from the current vector, i.e. subtracts the
     * individual coordinates.
     *
     * @param {Vector} vector
     * @returns {MagikVector} returns the current Vector
     */
    subtract(vector) {

        if(this.length !== vector.length) {

            throw new RangeError(`subtract(): Vectors must have the same number of coordinates, got ${this.length} and ${vector.length}`);

        }

        this.coordinates.forEach((coordinate, index) => {

            this.coordinates[index] -= vector.coordinates[index];

        });

        return this;

    }

    /**
     * Alias of substract()
     *
     * @alias this.subtract
     */
    sub(vector) {

        return this.substract(vector);

    }

    /**
     * Multiplies the current Vector by value, you can supply either a scalar value
     * or a Vector
     *
     * @param {Vector|number} value scalar or Vector to use to multiply
     * @returns {Vector|number} returns the current Vector
     */
    multiply(value) {

        if(typeof value === 'number') {

            this.coordinates.forEach((coordinate, index) => {

                this.coordinates[index] *= value;

            });

        } else {

            if(this.length !== value.length) {

                throw new Error(`multiply(): Vectors must have the same number of coordinates, got ${this.length} and ${value.length}`);

            }

            this.coordinates.forEach((coordinate, index) => {

                this.coordinates[index] *= value.coordinates[index];

            });

        }

        return this;

    }

    /**
     * Alias of multiply()
     *
     * @alias this.multiply
     */
    mult(value) {

        return this.multiply(value);

    }

    /**
     * Divides the current vector by the supplied scallar or Vector value
     *
     * @param {MagikVector|number} value scalar or MagikVector to use to divide
     * @returns {MagikVector} the divided MagikVector
     */
    divide(value) {

        if(typeof value === 'number') {

            this.coordinates.forEach((coordinate, index) => {

                this.coordinates[index] /= value;

            });

        } else {

            if(this.length !== value.length) {

                throw new Error(`divide(): Vectors must have the same number of coordinates, got ${this.length} and ${value.length}`);

            }

            this.coordinates.forEach((coordinate, index) => {

                this.coordinates[index] /= value.coordinates[index];

            });

        }

        return this;

    }

    /**
     * Alias of divide()
     *
     * @alias this.divide()
     */
    div(value) {

        return this.divide(value);

    }

    /**
     * Returns a clone of the current vector, i.e. creates a new Vector Object with
     * the same coordinates as the current vector
     *
     * @returns {MagikVector}
     */
    clone() {

        return new MagikVector(...this.coordinates);

    }

    /**
     * Returns the magnitude squared
     *
     * @returns {number}
     */
    getMagnitudeSquared() {

        let sum = 0;

        this.coordinates.forEach(coordinate => {

            sum += coordinate * coordinate;

        });

        return sum;

    }

    /**
     * Returns the magnitude of the vector
     *
     * @returns {number}
     */
    getMagnitude() {

        return Math.sqrt(this.getMagnitudeSquared());

    }

    /**
     * Sets the magnitude of the Vector and returns the changed vector
     *
     * @param magnitude
     * @returns {Vector}
     */
    setMagnitude(magnitude) {

        this.normalise();
        this.multiply(magnitude);
        return this;

    }

    /**
     * @alias this.setMagnitude()
     */
    setMag(magnitude) {

        return this.setMagnitude(magnitude);

    }

    /**
     * Calculates the direction, i.e. the angle of rotation for this vector
     * (only 2D vectors)
     *
     * @return {number} the angle of rotation
     */
    getDirection() {

        if(this.length !== 2) {

            throw new RangeError(`getDirection(): Direction can only be calculated for 2D vectors, length = ${this.length}`);

        }

        const direction = Math.atan2(this.getY(), this.getX());

        // TODO implement angleMode
        // return this.angleMode === RADIANS ? direction : this.radiansToDegrees(direction);

        return direction;

    }

    /**
     * @alias this.getDirection
     */
    getDir() {

        return this.getDirection();

    }

    /**
     * Returns the calculated distance from the current Vector to the supplied one
     *
     * @param {MagikVector} vector
     * @returns {number} the distance
     */
    getDistanceTo(vector) {

        return vector.clone().subtract(this).getMagnitude();

    }

    /**
     * @alias this.getDistanceTo
     */
    getDistance(vector) {

        return this.getDistanceTo(vector);

    }

    /**
     *
     * @alias this.getDistanceTo
     */
    getDist(vector) {

        return this.getDistanceTo(vector);

    }

    /**
     * Returns the dot Product of the current Vector with the supplied Vector,
     * throws an Error if both Vectors do not have the same number of
     * coordinates
     *
     * @param {Vector} vector
     * @returns {number}
     */
    dotProduct(vector) {

        if(this.length !== vector.length) {

            throw new RangeError(`dotProduct(): Vectors must have the same number of coordinates, got ${this.length} and ${vector.length}`);

        }

        let sum = 0;

        this.coordinates.forEach((coordinate, index) => {

            sum += this.coordinates[index] * vector.coordinates[index];

        });

        return sum;

    }

    /**
     * Alias for dotProduct()
     *
     * @alias this.dotProduct
     */
    dot(vector) {

        return this.dotProduct(vector);

    }

    /**
     * Normalises the Vector
     *
     * @returns {MagikVector} the normalised Vector
     */
    normalise() {

        const magnitude = this.getMagnitude();

        this.coordinates.forEach((coordinate, index) => {

            this.coordinates[index] = coordinate / magnitude;

        });

        return this;

    }

    /**
     * Alias of normalise()
     *
     * @alias this.normalise
     */
    normalize() {

        return this.normalise();

    }

    /**
     * Limits the magnitude of the Vector to the supplied scalar value
     *
     * @param {number} scalar
     * @returns {MagikVector}
     */
    limit(scalar) {

        if(this.getMagnitude() > scalar) {

            this.normalise();
            this.multiply(scalar);

        }

        return this;

    }

    /**
     * Returns the string representation of the Vector
     *
     * @returns {string} the string representation of the Vector
     */
    toString() {

        return `(${this.coordinates.join(', ')})`;

    }

}

if(typeof exports === 'object' && typeof module !== 'undefined') {

    module.exports = MagikVector;

}