math/Vector.js

import { lerp, fi_lerp, fixed, isVector } from "./Utils.js";

/**
 * 2D Vector
 */
class Vector {
    /**
     * @param {number} x
     * @param {number} y
     */
    constructor(x = 0, y = 0) {
        this.x = x;
        this.y = y;
    }

    /**
     * Negate the vector
     * @return {Vector}
     */
    negative() {
        this.x = -this.x;
        this.y = -this.y;
        return this;
    }

    /**
     * Add a vector or a number
     * @param {(Vector|number)} v
     * @return {Vector}
     */
    add(v) {
        if (isVector(v)) {
            this.x += v.x;
            this.y += v.y;
        } else {
            this.x += v;
            this.y += v;
        }
        return this;
    }

    /**
     * Subtract a vector or a number
     * @param {(Vector|number)} v
     * @return {Vector}
     */
    subtract(v) {
        if (isVector(v)) {
            this.x -= v.x;
            this.y -= v.y;
        } else {
            this.x -= v;
            this.y -= v;
        }
        return this;
    }

    /**
     * Multiply with a vector or a scalar value
     * @param {(Vector|number)} v
     * @return {Vector}
     */
    multiply(v) {
        if (isVector(v)) {
            this.x *= v.x;
            this.y *= v.y;
        } else {
            this.x *= v;
            this.y *= v;
        }
        return this;
    }

    /**
     * Divide by a vector or a scalar value
     * @param {(Vector|number)} v
     * @return {Vector}
     */
    divide(v) {
        if (isVector(v)) {
            if (v.x != 0) this.x /= v.x;
            if (v.y != 0) this.y /= v.y;
        } else {
            if (v != 0) {
                this.x /= v;
                this.y /= v;
            }
        }
        return this;
    }

    /**
     * Compare coordinates
     * @param {Vector} v
     * @return {boolean} 
     */
    equals(v) {
        return this.x == v.x && this.y == v.y;
    }

    /**
     * Get the dot product of v and the vector
     * @param {Vector} v
     * @return {number} 
     */
    dot(v) {
        return this.x * v.x + this.y * v.y;
    }

    /**
     * Get the cross product of v and the vector
     * @param {Vector} v
     * @return {number} 
     */
    cross(v) {
        return this.x * v.y - this.y * v.x
    }

    /**
     * get the length of this vector
     * @returns {number}
     */
    length() {
        return Math.sqrt(this.dot(this));
    }

    /**
     * Normalize the vector (set length to 1)
     * @return {Vector} 
     */
    normalize() {
        return this.divide(this.length());
    }

    /**
     * Limit the vector to the passed maximum length
     * @param {number} len
     * @return {Vector} 
     */
    limit(len) {
        var l = this.length()
        this.normalize()
        return this.multiply(Math.min(l, len));
    }

    /**
     * Get the smallest value of both coordinates
     * @return {number} 
     */
    min() {
        return Math.min(this.x, this.y);
    }

    /**
     * Get the biggest value of both coordinates
     * @return {number} 
     */
    max() {
        return Math.max(this.x, this.y);
    }

    /**
     * Radian representation of this vector  
     * 0 = a vector pointing to the right
     * -0.5*Math.PI = a vector pointing up
     * @return {number} 
     */
    toAngles() {
        let a = Math.atan2(-this.y, this.x);
        if (a < 0) {
            a += (2 * Math.PI);
        }
        return a;
    }

    /**
     * Direction as char, u=up, d=down, l=left, r=right
     * @param {boolean} full when true, its the full name i.e. "down" instead of "d"
     * @return {string} 
     */
    toDirection(full = false) {
        let a = this.toAngles() * (180 / Math.PI);

        if (a > 45 && a < 135) return full ? "up" : "u";
        if (a >= 135 && a <= 225) return full ? "left" : "l";
        if (a > 225 && a < 315) return full ? "down" : "d";
        return full ? "right" : "r"
    }

    /**
     * get x and y as array
     * @return {Array} 
     */
    toArray() {
        return [this.x, this.y];
    }

    /**
     * get a clone of this Vector
     * @return {Vector} 
     */
    clone() {
        return new Vector(this.x, this.y);
    }

    /**
     * Set x and y  
     * If y is undefined, x is treated as a Vector and its coordinates are applied
     * @param {number} x
     * @param {number} y
     * @return {Vector} 
     */
    set(x, y) {
        if (typeof y == "undefined" && isVector(x)) {
            this.x = x.x;
            this.y = x.y;
        } else {
            this.x = x;
            this.y = y;
        }
        return this;
    }

    /**
     * cut of digits
     * @param {number} d amount of digits
     * @return {Vector} 
     */
    fixed(d = 3) {
        this.x = fixed(this.x, d);
        this.y = fixed(this.y, d);
        return this;
    }

    /**
     * Create a vector
     * @static
     * @param {number} x
     * @param {number} y
     * @return {Vector} 
     */
    static create(x, y) {
        return new Vector(x, y);
    }

    /**
     * Get a random vector
     * @static
     * @param {number} min minimum for x and y
     * @param {number} max maximum for x and y
     * @return {Vector} 
     */
    static random(min, max) {
        let r = max - min;
        return new Vector(Math.random() * r + min, Math.random() * r + min);
    }

    /**
     * Add two vectors or a vector and a number and return the resulting new vector
     * @static
     * @param {Vector} a
     * @param {(Vector|number)} b
     * @return {Vector} 
     */
    static add(a, b) {
        if (b instanceof Vector || isVector(b)) return new Vector(a.x + b.x, a.y + b.y);
        else return new Vector(a.x + b, a.y + b);
    }

    /**
     * Subtract two vectors or a vector and a number and return the resulting new vector
     * @static
     * @param {Vector} a
     * @param {(Vector|number)} b
     * @return {Vector} 
     */
    static subtract(a, b) {
        if (b instanceof Vector || isVector(b)) return new Vector(a.x - b.x, a.y - b.y);
        else return new Vector(a.x - b, a.y - b);
    }

    /**
     * Multiply two vectors or a vector and a number and return the resulting new vector
     * @static
     * @param {Vector} a
     * @param {(Vector|number)} b
     * @return {Vector} 
     */
    static multiply(a, b) {
        if (b instanceof Vector) return new Vector(a.x * b.x, a.y * b.y);
        else return new Vector(a.x * b, a.y * b);
    }

    /**
     * Divide two vectors or a vector and a number and return the resulting new vector
     * @static
     * @param {Vector} a
     * @param {(Vector|number)} b
     * @return {Vector} 
     */
    static divide(a, b) {
        if (b instanceof Vector) return new Vector(a.x / b.x, a.y / b.y);
        else return new Vector(a.x / b, a.y / b);
    }

    /**
     * Get the distance between two vectors
     * *Note:* use {@link distLessThan} or {@link distGreaterThan} for comparing distances for more performance
     * @static
     * @param {Vector} a
     * @param {Vector} b
     * @return {number} 
     */
    static dist(a, b) {
        return Vector.subtract(a, b).length();
    }

    /**
     * Check if the distance between two vectors is less than the provided value
     * @static
     * @param {Vector} a
     * @param {Vector} b
     * @param {number} c
     * @param {boolean} equal also return true if the distance is exactly c
     * @return {boolean} 
     */
    static distLessThan(a, b, c, equal = false) {
        let v = Vector.subtract(a, b);
        return equal ? v.dot(v) <= c * c : v.dot(v) < c * c;
    }

    /**
     * Check if the distance between two vectors is greater than the provided value
     * @static
     * @param {Vector} a
     * @param {Vector} b
     * @param {number} c
     * @param {boolean} equal also return true if the distance is exactly c
     * @return {boolean} 
     */
    static distGreaterThan(a, b, c, equal = false) {
        let v = Vector.subtract(a, b);
        return equal ? v.dot(v) >= c * c : v.dot(v) > c * c;
    }

    /**
     * return the linear interpolation between a and b at percent t
     * @static
     * @param {Vector} a
     * @param {Vector} b
     * @param {number} t
     * @return {Vector} 
     */
    static lerp(a, b, t) {
        return {
            x: lerp(a.x, b.x, t),
            y: lerp(a.y, b.y, t)
        }
    }

    /**
     * return the linear interpolation between a and b at percent t
     * @static
     * @param {Vector} a from vector
     * @param {Vector} b to vector
     * @param {number} p percentage
     * @param {Number} dt delta time in ms since last frame
     * @param {Number} targetFPS target (context) fps
     * @return {Vector} 
     */
    static fi_lerp(a, b, p, dt, fps = 60) {
        return {
            x: fi_lerp(a.x, b.x, p, dt, fps),
            y: fi_lerp(a.y, b.y, p, dt, fps)
        }
    }

    /**
     * Get the orthogonal projected Point on the line A -> B  
     * *Note:* the resulting point isn't necessarily between A and B
     * @static
     * @param {Vector} p the point to project onto the line
     * @param {Vector} a
     * @param {Vector} b
     * @return {Vector} 
     */
    static projectionPoint(p, a, b) {
        var ap = Vector.subtract(p, a);
        var ab = Vector.subtract(b, a);
        ab.normalize();
        ab.multiply(ap.dot(ab));
        return Vector.add(a, ab);
    }

    /**
     * Get a vector from a direction
     * @param {string} dir for example up, down, left, right
     * @return {Vector} 
     */
    static fromDirection(dir) {
        switch (dir) {
            case "up":
            case "u":
                return new Vector(0, -1);
            case "right":
            case "r":
                return new Vector(1, 0);
            case "down":
            case "d":
                return new Vector(0, 1);
            case "left":
            case "l":
                return new Vector(-1, 0);
        }

        return new Vector(0, 0);
    }

    /**
     * Get a vector from a cardinal direction
     * @param {string} dir for example n,e,sw,nw
     * @return {Vector} 
     */
    static fromCardinalDirection(dir) {
        switch (dir) {
            case "n":
                return new Vector(0, -1);
            case "e":
                return new Vector(1, 0);
            case "s":
                return new Vector(0, 1);
            case "w":
                return new Vector(-1, 0);
            case "nw":
                return new Vector(-1, -1);
            case "ne":
                return new Vector(1, -1);
            case "sw":
                return new Vector(-1, 1);
            case "se":
                return new Vector(1, 1);
        }

        return new Vector(0, 0);
    }

    /**
     * Relative magnitude of the angular gap between the vectors
     * this will always return positive values, it does not reveal the direction of rotation
     */
    static dotAngleBetween(a, b) {
        let d = a.length() * b.length();
        let p = d !== 0 ? a.dot(b) / d : 0;
        return Math.acos(p);
    }

    /**
     * Absolute angular measure between the two vectors
     * Calculated using the bearing angles (signed angle of rotation measured counter-clockwise from positive x)
     */
    static angleBetween(a, b) {
        let angle = Math.atan2(b.y, b.x) - Math.atan2(a.y, a.x);
        if (angle < 0) angle += 2 * Math.PI;
        return angle;
    }
}

export default Vector;