game_vector_vec2.js

/**
 * @module Vec2
 * @fileOverview Contains Vec2 class.
 */

/**
 * @class
 * A two-dimensional vector. Used to represent coordinates in 2D space.
 */
class Vec2 {
    /**
     * Create a new Vec2 from ambiguous arguments.
     * @param {number|number[]|Vec2} args Arguments to create vector from
     * @returns {Vec2} Resulting vector
     * @constructor
     */
    static from(...args) {
        if (args.length === 1) {
            if (typeof args[0] === "number")
                return new Vec2(args[0], args[0]);
            else if (args[0] instanceof Array)
                return this.fromArray(args[0]);
            else if (args[0] instanceof Vec2)
                return args[0].copy();
        } else if (args.length === 2)
            return new Vec2(args[0], args[1]);

        return new Vec2();
    }

    /**
     * Create a new Vec2 from an array.
     * @param {number[2]} array Array to create vector from
     * @returns {Vec2} Resulting vector
     */
    static fromArray(array) {
        return new Vec2(array[0], array[1]);
    }

    /**
     * Create a new Vec2 from an angle
     * @param {number} rads Angle in radians
     * @returns {Vec2} Resulting vector
     */
    static fromAngle(rads) {
        return new Vec2(Math.cos(rads), Math.sin(rads)).norm();
    }

    /**
     * Create a random Vec2 within a specific radius.
     * @param {number} radius Radius to generate in
     * @returns {Vec2} Resulting random vector
     */
    static random(radius = 1) {
        return new Vec2(Math.random(), Math.random()).norm().mulv(Math.random() * radius);
    }

    /**
     * Create a random Vec2 on the edge of a specific radius.
     * @param {number} radius Radius to generate on
     * @returns {Vec2} Resulting random vector
     */
    static randomOuter(radius = 1) {
        return new Vec2(
            Math.random() - 0.5,
            Math.random() - 0.5
        ).norm().mulv(radius);
    }

    /**
     * Given multiple vectors, find the lowest coordinates in both directions.
     * @param {...Vec2} vectors Vectors to compare
     * @returns {Vec2} Resulting minimum vector
     */
    static min(...vectors) {
        let m = null;

        for (const vector of vectors) {
            if (m === null) {
                m = vector.copy();
                continue;
            }

            m.x = Math.min(m.x, vector.x);
            m.y = Math.min(m.y, vector.y);
        }

        if (m === null)
            return new Vec2();

        return m;
    }

    /**
     * Given multiple vectors, find the highest coordinates in both directions.
     * @param {...Vec2} vectors Vectors to compare
     * @returns {Vec2} Resulting maximum vector
     */
    static max(...vectors) {
        let m = null;

        for (const vector of vectors) {
            if (m === null) {
                m = vector.copy();
                continue;
            }

            m.x = Math.max(m.x, vector.x);
            m.y = Math.max(m.y, vector.y);
        }

        if (m === null)
            return new Vec2();

        return m;
    }

    /**
     * Create a new Vec2.
     * @param {number} x X-value of the vector
     * @param {number} y Y-value of the vector
     */
    constructor(x = 0, y = 0) {
        this.x = x;
        this.y = y;
    }

    /**
     * Get an exact copy of the vector.
     * @returns {Vec2} Copy of vector
     */
    copy() {
        return new Vec2(this.x, this.y);
    }


    //
    // Addition & Subtraction
    //

    /**
     * Add two vectors together.
     * @param {Vec2} other Other vector to add
     * @returns {Vec2} New Vec2 with resulting values
     */
    add(other) {
        return new Vec2(this.x + other.x, this.y + other.y);
    }

    /**
     * Add x and y values to the vector.
     * @param {number} x X-value to add
     * @param {number} y Y-value to add
     * @returns {Vec2} New Vec2 with resulting values
     */
    addv(x, y) {
        return new Vec2(this.x + x, this.y + y);
    }

    /**
     * Subtract a vector from another.
     * @param {Vec2} other Other vector to subtract by
     * @returns {Vec2} New Vec2 with resulting values
     */
    sub(other) {
        return new Vec2(this.x - other.x, this.y - other.y);
    }

    /**
     * Subtract x and y values from the vector.
     * @param {number} x X-value to subtract
     * @param {number} y Y-value to subtract
     * @returns {Vec2} New Vec2 with resulting values
     */
    subv(x, y) {
        return new Vec2(this.x - x, this.y - y);
    }


    //
    // Multiplication & Division
    //

    /**
     * Multiply two vectors together.
     * @param {Vec2} other Other vector to multiply by
     * @returns {Vec2} New Vec2 with resulting values
     */
    mul(other) {
        return new Vec2(this.x * other.x, this.y * other.y);
    }

    /**
     * Multiply the vector by a scalar.
     * @param {number} val Scalar value
     * @returns {Vec2} New Vec2 with scaled values
     */
    mulv(val) {
        return new Vec2(this.x * val, this.y * val);
    }

    /**
     * Divide the vector by another.
     * @param {Vec2} other Other vector to divide by
     * @returns {Vec2} New Vec2 with resulting values
     */
    div(other) {
        return new Vec2(this.x / other.x, this.y / other.y);
    }

    /**
     * Divide the vector by a scalar.
     * @param {number} val Scalar value
     * @returns {Vec2} New Vec2 with scaled values
     */
    divv(val) {
        return new Vec2(this.x / val, this.y / val);
    }


    //
    // Calculations & Conversions
    //

    /**
     * Get the magnitude/length of the vector.
     * @returns {number} Magnitude of vector
     */
    mag() {
        return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
    }

    /**
     * Get the distance (magnitude) from the vector to another.
     * @param {Vec2} other Other Vec2 to get distance to
     * @returns {number} Distance between the two vectors
     */
    dist(other) {
        return this.sub(other).mag();
    }

    /**
     * Get the dot product of the vector with another.
     * @param {Vec2} other Other Vec2 to use
     * @returns {number} Dot product of the two vectors
     */
    dot(other) {
        return this.x * other.x + this.y * other.y;
    }

    /**
     * Get the dot product of the vector with specific x and y values.
     * @param {number} x X-value to use
     * @param {number} y Y-value to use
     * @returns {number} Resulting dot product
     */
    dotv(x, y) {
        return this.x * x + this.y * y;
    }

    /**
     * Get the angle of the vector (in radians).
     * @returns {number} Angle of vector
     */
    angle() {
        if (this.isZero())
            return 0;

        return Math.atan2(this.y, this.x);
    }

    /**
     * Get a new vector by rotating (in radians) the existing vector.
     * @param {number} rad Radians to rotate by
     * @returns {Vec2} Resulting rotated vector
     */
    rot(rad) {
        const d = this.mag();
        const angle = this.angle();
        return new Vec2(Math.cos(angle + rad) * d, Math.sin(angle + rad) * d);
    }

    /**
     * Get the normalization of the vector.
     * @returns {Vec2} Normalized version of the vector
     */
    norm() {
        const mag = this.mag();
        if (mag === 0)
            return new Vec2();

        return this.divv(mag);
    }

    /**
     * Get a vector denoting the signs of each axis.
     * @returns {Vec2} Sign vector
     */
    sign() {
        return new Vec2(Math.sign(this.x), Math.sign(this.y));
    }

    /**
     * Invert the vector (inverts the signs of the values).
     * @returns {Vec2} Inverted Vec2
     */
    invert() {
        return new Vec2(-this.x, -this.y);
    }

    /**
     * Calculate the perpendicular vector.
     * @returns {Vec2} Perpendicular vector
     */
    perp() {
        return new Vec2(-this.y, this.x);
    }

    /**
     * Get the normal vector from another vector.
     * @param {Vec2} other Other Vec2 to use
     * @returns {Vec2} Resulting normal vector
     */
    normWith(other) {
        return this.sub(other).norm().perp();
    }

    /**
     * Floor the values of the vector. Can be used to round to a specified precision.
     * @param {number} v (Optional) Multiplies values by this value before flooring, then divides it afterwords.
     * Useful for rounding to a specific precision or specific decimal places.
     * @returns {Vec2} Resulting floored Vec2
     */
    floor(v = 1) {
        return new Vec2(Math.floor(this.x / v) * v, Math.floor(this.y / v) * v);
    }

    /**
     * Ceiling the values of the vector. Can be used to round to a specified precision.
     * @param {number} v (Optional) Multiplies values by this value before ceiling, then divides it afterwords.
     * Useful for rounding to a specific precision or specific decimal places.
     * @returns {Vec2} Resulting rounded up Vec2
     */
    ceil(v = 1) {
        return new Vec2(Math.ceil(this.x / v) * v, Math.ceil(this.y / v) * v);
    }

    /**
     * Convert the Vec2 to a standard array.
     * @returns {number[]} Vector as an array
     */
    toArray() {
        return [
            this.x,
            this.y
        ];
    }

    /**
     * Convert the Vec2 to a gl-matrix array.
     * @returns {number[]} Gl-matrix array
     */
    toGlArray() {
        const vector = vec2.create();
        vec2.set(vector, this.x, this.y);

        return vector;
    }

    /**
     * Transform the vector using a gl-matrix matrix.
     * @param matrix Matrix to transform with
     * @returns {Vec2} Transformed vector
     */
    transform(matrix) {
        const vector = this.toGlArray();
        vec2.transformMat3(vector, vector, matrix);

        return Vec2.fromArray(vector);
    }

    /**
     * Inversely transform the vector using a gl-matrix matrix.
     * @param matrix Matrix to inversely transform with
     * @returns {Vec2} Inversely transformed vector
     */
    inverseTransform(matrix) {
        mat3.invert(matrix, matrix);

        return this.transform(matrix);
    }

    /**
     * Convert to a translation matrix.
     * @returns Translation matrix
     */
    toTranslationMatrix() {
        const vector = this.toGlArray();
        const matrix = mat3.create();
        mat3.fromTranslation(matrix, vector);

        return matrix;
    }

    /**
     * Convert to a scaling matrix.
     * @returns Scaling matrix
     */
    toScaleMatrix() {
        const vector = this.toGlArray();
        const matrix = mat3.create();
        mat3.fromScaling(matrix, vector);

        return matrix;
    }

    /**
     * Project onto another vector.
     * @param {Vec2} other Vector to project onto
     * @returns {Vec2} Projected vector
     */
    project(other) {
        return other.mulv(this.dot(other) / other.dot(other));
    }

    /**
     * Mirror over another vector.
     * @param {Vec2} other Vector to mirror over
     * @returns {Vec2} Mirrored vector
     */
    mirrorOver(other) {
        return this.project(other).mulv(2).sub(this);
    }


    //
    // Checks
    //

    /**
     * Check if the vector is zero or not (both values equal to zero).
     * @returns {boolean} Whether the vector is zero
     */
    isZero() {
        return this.x === 0 && this.y === 0;
    }

    /**
     * Check if another vector is equal within an optionally specified range.
     * @param {Vec2} other Other vector to compare with
     * @param {number} range (Optional) Precision of the comparison. Set to 0.001 by default, this value is useful for
     * avoiding floating-point precision issues (defaults to 0.001).
     * @returns {boolean} Whether or not the vectors are equal, within the specified range.
     */
    equals(other, range = 0.001) {
        return Math.abs(this.x - other.x) < range &&
            Math.abs(this.y - other.y) < range;
    }

    //
    // Linear interpolation
    //

    /**
     * Perform linear interpolation between the vector and another.
     * @param {Vec2} other Other Vec2 to interpolate to
     * @param {number} alpha Progress between the values (0 - 1)
     * @returns {Vec2} Resulting Vec2
     */
    lerp(other, alpha) {
        let d = other.sub(this);
        d = d.mulv(alpha);

        return this.add(d);
    }

    /**
     * Perform linear interpolation between the vector and another, smoothly.
     * @param {Vec2} other Other Vec2 to interpolate to
     * @param {number} alpha Progress between the values (0 - 1)
     * @param {number} edge Amount (percentage) of the interpolation to smooth (0 - 1)
     * @returns {Vec2} Resulting Vec2
     */
    smoothLerp(other, alpha, edge = 0.1) {
        //if (alpha < edge)
        //    alpha = alpha / edge
        //else if (alpha > 1 - edge)
        //    alpha = (1 - alpha) / edge;

        alpha = alpha * alpha * (3 - 2 * alpha);

        return this.add(other.sub(this).mulv(alpha));
    }
}

export default Vec2;