UNPKG

@fimbul-works/vec

Version:

A comprehensive TypeScript vector math library providing 2D, 3D, and 4D vector operations with a focus on performance and type safety.

890 lines (889 loc) 28.4 kB
"use strict"; /** * Documentation for 2D Vector class. * @module Vec2 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.Vec2 = void 0; const { PI, sin, cos, atan2, sqrt, abs } = Math; const PI2 = PI * 2; /** * Represents a 2D vector with various operations. */ class Vec2 { #magnitude = 0; #prevXY = new Float64Array([0, 0]); #xy = new Float64Array(2); /** * Computes and updates the magnitude of the vector. * @private * @param {number} x - The x-component of the vector. * @param {number} y - The y-component of the vector. * @returns {void} */ #computeMagnitude(x = this.#xy[0], y = this.#xy[1]) { if (x !== this.#prevXY[0] || y !== this.#prevXY[1]) { this.#magnitude = sqrt(x ** 2 + y ** 2); this.#prevXY[0] = x; this.#prevXY[1] = y; } } /** * Creates a new Vec2 instance. * @param {number} x - The x-coordinate of the vector. * @param {number} y - The y-coordinate of the vector. */ constructor(x = 0, y = x) { this.#xy[0] = x; this.#xy[1] = y; this.#computeMagnitude(x, y); } /** * Adds two vectors. * @param v - The first vector. * @param w - The second vector. * @returns A new Vec2 instance representing the sum. */ static add(v, w) { return new Vec2(v.#xy[0] + w.#xy[0], v.#xy[1] + w.#xy[1]); } /** * Subtracts one vector from another. * @param v - The vector to subtract from. * @param w - The vector to subtract. * @returns A new Vec2 instance representing the difference. */ static subtract(v, w) { return new Vec2(v.#xy[0] - w.#xy[0], v.#xy[1] - w.#xy[1]); } /** * Multiplies one vector with another. * @param v - The first vector. * @param w - The second vector. * @returns A new Vec2 instance representing the multiplied value. */ static multiply(v, w) { return new Vec2(v.x * w.x, v.y * w.y); } /** * Divides one vector with another. * @param v - Divident. * @param w - Divisor. * @returns A new Vec2 instance representing the divided value. */ static divide(v, w) { return new Vec2(w.x === 0 ? Number.POSITIVE_INFINITY : v.x / w.x, w.y === 0 ? Number.POSITIVE_INFINITY : v.y / w.y); } /** * Calculates the angle between two vectors. * @param v - The first vector. * @param w - The second vector. * @returns The angle between the vectors in radians. */ static angleBetween(v, w) { const vX = v.#xy[0]; const vY = v.#xy[1]; const wX = w.#xy[0]; const wY = w.#xy[1]; return atan2(vX * wY - vY * wX, vX * wX + vY * wY); } /** * Calculates the Euclidean distance between two vectors. * @param v - The first vector. * @param w - The second vector. * @returns The distance between the vectors. */ static distance(v, w) { return sqrt((v.#xy[0] - w.#xy[0]) ** 2 + (v.#xy[1] - w.#xy[1]) ** 2); } /** * Calculates the Chebyshev distance between two vectors. * @param v - The first vector. * @param w - The second vector. * @returns The Chebyshev distance between the vectors. */ static distanceChebyshev(v, w) { const absX = abs(v.#xy[0] - w.#xy[0]); const absY = abs(v.#xy[1] - w.#xy[1]); return absX >= absY ? absX : absY; } /** * Calculates the Manhattan distance between two vectors. * @param v - The first vector. * @param w - The second vector. * @returns The Manhattan distance between the vectors. */ static distanceManhattan(v, w) { return abs(v.#xy[0] - w.#xy[0]) + abs(v.#xy[1] - w.#xy[1]); } /** * Calculates the Minkowski distance between two vectors. * @param v - The first vector. * @param w - The second vector. * @param p - The order of the Minkowski distance. * @returns The Minkowski distance between the vectors. */ static distanceMinkowski(v, w, p) { return (abs(v.#xy[0] - w.#xy[0]) ** p + abs(v.#xy[1] - w.#xy[1]) ** p) ** (1 / p); } /** * Calculates the squared Euclidean distance between two vectors. * @param v - The first vector. * @param w - The second vector. * @returns The squared distance between the vectors. */ static distanceSq(v, w) { return (v.#xy[0] - w.#xy[0]) ** 2 + (v.#xy[1] - w.#xy[1]) ** 2; } /** * Calculates the dot product of two vectors. * @param v - The first vector. * @param w - The second vector. * @returns The dot product of the vectors. */ static dot(v, w) { return v.#xy[0] * w.#xy[0] + v.#xy[1] * w.#xy[1]; } /** * Calculates the cross product of two vectors. * @param v - The first vector. * @param w - The second vector. * @returns The cross product scalar value. */ static cross(v, w) { return v.#xy[0] * w.#xy[1] - v.#xy[1] * w.#xy[0]; } /** * Reflects the vector across a normal vector. * @param v - The vector to reflect. * @param normal - The normal vector to reflect across (must be normalized). * @returns A new Vec2 instance representing the reflected vector. */ static reflect(v, normal) { const dot = v.dot(normal); return new Vec2(v.#xy[0] - 2 * dot * normal.#xy[0], v.#xy[1] - 2 * dot * normal.#xy[1]); } /** * Creates a Vec2 instance from polar coordinates. * @param r - The radius. * @param theta - The angle in radians. * @returns A new Vec2 instance. */ static fromPolarCoords(r, theta) { return new Vec2(r * cos(theta), r * sin(theta)); } /** * Creates an immutable Vec2-like object. * @param x - The x-coordinate of the vector. * @param y - The y-coordinate of the vector. * @returns An immutable object with Vec2-like properties. */ static immutable(x = 0, y = 0) { const data = new Float64Array(10); data[0] = x; data[1] = y; data[4] = atan2(y, x) + (y < 0 ? PI2 : 0); data[5] = -atan2(x, y) + (x > 0 ? PI2 : 0); const magnitudeSq = x ** 2 + y ** 2; data[8] = sqrt(magnitudeSq); data[9] = magnitudeSq; const isInfinite = x === Number.POSITIVE_INFINITY || x === Number.NEGATIVE_INFINITY || y === Number.POSITIVE_INFINITY || y === Number.NEGATIVE_INFINITY; const _isNaN = Number.isNaN(x) && Number.isNaN(y); const isZero = x === 0 && y === 0; const xy = Object.freeze([...data.slice(0, 2)]); return { get x() { return data[0]; }, get y() { return data[1]; }, get xy() { return xy; }, get magnitude() { return data[8]; }, get magnitudeSq() { return data[10]; }, get angleX() { return data[4]; }, get angleY() { return data[5]; }, get isInfinite() { return isInfinite; }, get isNaN() { return _isNaN; }, get isZero() { return isZero; }, }; } /** * Checks if a vector has infinite components. * @param v - The vector to check. * @returns True if the vector has infinite components, false otherwise. */ static isInfinite(v) { const x = v.#xy[0]; const y = v.#xy[1]; return (x === Number.POSITIVE_INFINITY || x === Number.NEGATIVE_INFINITY || y === Number.POSITIVE_INFINITY || y === Number.NEGATIVE_INFINITY); } /** * Checks if a vector has NaN components. * @param v - The vector to check. * @returns True if the vector has NaN components, false otherwise. */ static isNaN(v) { return Number.isNaN(v.#xy[0]) || Number.isNaN(v.#xy[1]); } /** * Checks if a vector is zero. * @param v - The vector to check. * @returns True if the vector is zero, false otherwise. */ static isZero(v) { return v.#xy[0] === 0 && v.#xy[1] === 0; } /** * Performs linear interpolation between two vectors. * @param v - The first vector. * @param w - The second vector. * @param t - The interpolation parameter (0 to 1). * @returns A new Vec2 instance representing the interpolated vector. */ static lerp(v, w, t) { if (t > 1) t = 1; else if (t < 0) t = 0; const vX = v.#xy[0]; const vY = v.#xy[1]; return new Vec2(vX + (w.#xy[0] - vX) * t, vY + (w.#xy[1] - vY) * t); } /** * Negates a vector. * @param v - The vector to negate. * @returns A new Vec2 instance representing the negated vector. */ static negate(v) { return new Vec2(-v.#xy[0], -v.#xy[1]); } /** * Normalizes a vector. * @param v - The vector to normalize. * @returns A new Vec2 instance representing the normalized vector. */ static normalize(v) { const m = v.#magnitude; return new Vec2(v.#xy[0] / m, v.#xy[1] / m); } /** * Projects one vector onto another. * @param v - The vector to project. * @param w - The vector to project onto. * @returns A new Vec2 instance representing the projected vector. */ static project(v, w) { const vX = v.#xy[0]; const vY = v.#xy[1]; const wX = w.#xy[0]; const wY = w.#xy[1]; const wM = w.#magnitude; const f = v.#magnitude * cos(atan2(vX * wY - vY * wX, vX * wX + vY * wY)); return new Vec2((wX / wM) * f, (wY / wM) * f); } /** * Creates a random unit vector. * @param random - A function that returns a random number between 0 and 1. * @returns A new Vec2 instance representing a random unit vector. */ static random(random = Math.random) { const phi = random() * PI2; return new Vec2(cos(phi), sin(phi)); } /** * Checks if two vectors are equal. * @param v - The first vector. * @param w - The second vector. * @returns True if the vectors are equal, false otherwise. */ static satisfyEquality(v, w) { return v.#xy[0] === w.#xy[0] && v.#xy[1] === w.#xy[1]; } /** * Checks if two vectors are opposite. * @param v - The first vector. * @param w - The second vector. * @returns True if the vectors are opposite, false otherwise. */ static satisfyOpposition(v, w) { return v.#xy[0] === -w.#xy[0] && v.#xy[1] === -w.#xy[1]; } /** * Compares a vector with another vector using an epsilon value for floating-point comparison. * @param v - The first vector. * @param w - The second vector. * @param epsilon - The maximum difference between components to consider them equal. * @returns True if the vectors are equal within epsilon, false otherwise. */ static equals(v, w, epsilon = Number.EPSILON) { return abs(v.#xy[0] - w.#xy[0]) <= epsilon && abs(v.#xy[1] - w.#xy[1]) <= epsilon; } /** * Scales a vector by a scalar value. * @param v - The vector to scale. * @param c - The scalar value. * @returns A new Vec2 instance representing the scaled vector. */ static scale(v, c) { return new Vec2(v.#xy[0] * c, v.#xy[1] * c); } /** * Creates a zero vector. * @returns A new Vec2 instance representing a zero vector. */ static zero() { return new Vec2(); } /** * Creates a vector with all components set to 1.0. * @returns A new Vec2 instance representing a vector with all components set to 1.0. */ static one() { return new Vec2(1, 1); } /** * Creates a Vec2 from an array. * @param {[number, number] | number[]} arr - An array containing the x and y coordinates. * @returns {Vec2} A new Vec2 instance. */ static fromArray(arr) { return new Vec2(arr[0] ?? 0, arr[1] ?? 0); } /** * Creates a Vec2 from an object with x and y properties. * @param {{ x: number; y: number }} obj - An object with x and y properties. * @returns {Vec2} A new Vec2 instance. */ static fromObject(obj) { return new Vec2(obj.x, obj.y); } /** * Creates a Vec2 instance from a JSON-parsed object. * @param json - The JSON-parsed object containing x and y properties. * @returns A new Vec2 instance. */ static fromJSON(json) { return Vec2.fromObject(json); } /** * Gets the x-component of the vector. * @returns The x-component. */ get x() { return this.#xy[0]; } /** * Sets the x-component of the vector. * @param x - The new x-component. */ set x(x) { this.#xy[0] = x; this.#computeMagnitude(); } /** * Gets the y-component of the vector. * @returns The y-component. */ get y() { return this.#xy[1]; } /** * Sets the y-component of the vector. * @param y - The new y-component. */ set y(y) { this.#xy[1] = y; this.#computeMagnitude(); } /** * Gets a copy of the vector's components as an array. * @returns An array containing the x and y components of the vector. */ get xy() { return Array.from(this.#xy.slice()); } /** * Sets both components of the vector at once. * @param xy - An array containing the new x and y components. */ set xy(xy) { this.#xy[0] = xy[0]; this.#xy[1] = xy[1]; this.#computeMagnitude(); } /** * Gets the angle between the vector and the positive x-axis in radians. * @returns The angle in radians, always in the range [0, 2π). */ get angleX() { return atan2(this.#xy[1], this.#xy[0]) + (this.#xy[1] < 0 ? PI2 : 0); } /** * Sets the angle between the vector and the positive x-axis, maintaining the vector's magnitude. * @param phi - The new angle in radians. */ set angleX(phi) { const m = this.#magnitude; this.#xy[0] = m * cos(phi); this.#xy[1] = m * sin(phi); this.#computeMagnitude(); } /** * Gets the angle between the vector and the positive y-axis in radians. * @returns The angle in radians, always in the range [0, 2π). */ get angleY() { return -atan2(this.#xy[0], this.#xy[1]) + (this.#xy[0] > 0 ? PI2 : 0); } /** * Sets the angle between the vector and the positive y-axis, maintaining the vector's magnitude. * @param phi - The new angle in radians. */ set angleY(phi) { const m = this.#magnitude; this.#xy[0] = m * -sin(phi); this.#xy[1] = m * cos(phi); this.#computeMagnitude(); } /** * Gets the magnitude (length) of the vector. * @returns The magnitude of the vector. */ get magnitude() { return this.#magnitude; } /** * Sets the magnitude (length) of the vector, maintaining its direction. * @param m - The new magnitude. */ set magnitude(m) { const M = this.#magnitude; this.#xy[0] = (this.#xy[0] / M) * m; this.#xy[1] = (this.#xy[1] / M) * m; this.#computeMagnitude(); } /** * Gets the squared magnitude of the vector. * This is faster to compute than the actual magnitude and is useful for comparisons. * @returns The squared magnitude of the vector. */ get magnitudeSq() { return this.#magnitude ** 2; } /** * Adds another vector to this vector. * @param v - The vector to add. * @returns This Vec2 instance for method chaining. */ add(v) { this.#xy[0] += v.#xy[0]; this.#xy[1] += v.#xy[1]; this.#computeMagnitude(); return this; } /** * Subtracts another vector from this vector. * @param v - The vector to subtract. * @returns This Vec2 instance for method chaining. */ subtract(v) { this.#xy[0] -= v.#xy[0]; this.#xy[1] -= v.#xy[1]; this.#computeMagnitude(); return this; } /** * Multiplies this vector with another vector. * @param v - The vector to multiply with. * @returns This Vec2 instance for method chaining. */ multiply(v) { this.#xy[0] *= v.#xy[0]; this.#xy[1] *= v.#xy[1]; this.#computeMagnitude(); return this; } /** * Divides this vector with another vector. * @param v - The vector to divide with. * @returns This Vec2 instance for method chaining. */ divide(v) { this.#xy[0] = v.#xy[0] === 0 ? Number.POSITIVE_INFINITY : this.#xy[0] / v.#xy[0]; this.#xy[1] = v.#xy[1] === 0 ? Number.POSITIVE_INFINITY : this.#xy[1] / v.#xy[1]; this.#computeMagnitude(); return this; } /** * Calculates the angle between this vector and another vector. * @param v - The other vector. * @returns The angle between the vectors in radians. */ angleBetween(v) { const x = this.#xy[0]; const y = this.#xy[1]; const vX = v.#xy[0]; const vY = v.#xy[1]; return atan2(x * vY - y * vX, x * vX + y * vY); } /** * Clamps the magnitude of this vector between a minimum and maximum value. * @param min - The minimum magnitude. * @param max - The maximum magnitude. * @returns This Vec2 instance for method chaining. */ clamp(min, max) { const m = this.#magnitude; if (m > max) { this.#xy[0] = (this.#xy[0] / m) * max; this.#xy[1] = (this.#xy[1] / m) * max; this.#computeMagnitude(); } else if (m < min) { this.#xy[0] = (this.#xy[0] / m) * min; this.#xy[1] = (this.#xy[1] / m) * min; this.#computeMagnitude(); } return this; } /** * Creates a copy of this vector. * @returns A new Vec2 instance with the same components. */ clone() { return new Vec2(this.#xy[0], this.#xy[1]); } /** * Copies the components of another vector to this vector. * @param v - The vector to copy from. * @returns This Vec2 instance for method chaining. */ copy(v) { this.#xy[0] = v.#xy[0]; this.#xy[1] = v.#xy[1]; this.#computeMagnitude(); return this; } /** * Calculates the distance between this vector and another vector. * @param v - The other vector. * @returns The distance between the vectors. */ distance(v) { return sqrt((this.#xy[0] - v.#xy[0]) ** 2 + (this.#xy[1] - v.#xy[1]) ** 2); } /** * Calculates the Chebyshev distance between this vector and another vector. * @param v - The other vector. * @returns The Chebyshev distance between the vectors. */ distanceChebyshev(v) { const absX = abs(this.#xy[0] - v.#xy[0]); const absY = abs(this.#xy[1] - v.#xy[1]); return absX >= absY ? absX : absY; } /** * Calculates the Manhattan distance between this vector and another vector. * @param v - The other vector. * @returns The Manhattan distance between the vectors. */ distanceManhattan(v) { return abs(this.#xy[0] - v.#xy[0]) + abs(this.#xy[1] - v.#xy[1]); } /** * Calculates the Minkowski distance between this vector and another vector. * @param v - The other vector. * @param p - The order of the Minkowski distance. * @returns The Minkowski distance between the vectors. */ distanceMinkowski(v, p) { return (abs(this.#xy[0] - v.#xy[0]) ** p + abs(this.#xy[1] - v.#xy[1]) ** p) ** (1 / p); } /** * Calculates the squared distance between this vector and another vector. * @param v - The other vector. * @returns The squared distance between the vectors. */ distanceSq(v) { return (this.#xy[0] - v.#xy[0]) ** 2 + (this.#xy[1] - v.#xy[1]) ** 2; } /** * Calculates the dot product of this vector with another vector. * @param v - The other vector. * @returns The dot product of the vectors. */ dot(v) { return this.#xy[0] * v.#xy[0] + this.#xy[1] * v.#xy[1]; } /** * Calculates the cross product of this vector with another vector. * @param v - The other vector. * @returns The cross product of the vectors. */ cross(v) { return this.#xy[0] * v.#xy[1] - this.#xy[1] * v.#xy[0]; } /** * Reflects this vector across a normal vector. * @param normal - The normal vector to reflect across (must be normalized). * @returns A new Vec2 instance representing the reflected vector. */ reflect(normal) { const dot = this.dot(normal); return new Vec2(this.#xy[0] - 2 * dot * normal.#xy[0], this.#xy[1] - 2 * dot * normal.#xy[1]); } /** * Checks if this vector has infinite components. * @returns True if the vector has infinite components, false otherwise. */ isInfinite() { const x = this.#xy[0]; const y = this.#xy[1]; return (x === Number.POSITIVE_INFINITY || x === Number.NEGATIVE_INFINITY || y === Number.POSITIVE_INFINITY || y === Number.NEGATIVE_INFINITY); } /** * Checks if this vector has NaN components. * @returns True if the vector has NaN components, false otherwise. */ isNaN() { return Number.isNaN(this.#xy[0]) || Number.isNaN(this.#xy[1]); } /** * Checks if this vector is zero. * @returns True if the vector is zero, false otherwise. */ isZero() { return this.#xy[0] === 0 && this.#xy[1] === 0; } /** * Limits the maximum magnitude of this vector. * @param max - The maximum magnitude. * @returns This Vec2 instance for method chaining. */ limitMax(max) { const m = this.#magnitude; if (m > max) { this.#xy[0] = (this.#xy[0] / m) * max; this.#xy[1] = (this.#xy[1] / m) * max; this.#computeMagnitude(); } return this; } /** * Limits the minimum magnitude of this vector. * @param min - The minimum magnitude. * @returns This Vec2 instance for method chaining. */ limitMin(min) { const m = this.#magnitude; if (m < min) { this.#xy[0] = (this.#xy[0] / m) * min; this.#xy[1] = (this.#xy[1] / m) * min; this.#computeMagnitude(); } return this; } /** * Sets this vector to point towards another vector. * @param v - The vector to look at. * @returns This Vec2 instance for method chaining. */ lookAt(v) { const m = this.#magnitude; const vM = v.#magnitude; this.#xy[0] = (v.#xy[0] / vM) * m; this.#xy[1] = (v.#xy[1] / vM) * m; this.#computeMagnitude(); return this; } /** * Negates this vector. * @returns This Vec2 instance for method chaining. */ negate() { this.#xy[0] *= -1; this.#xy[1] *= -1; this.#computeMagnitude(); return this; } /** * Normalizes this vector. * @returns This Vec2 instance for method chaining. */ normalize() { const m = this.#magnitude; this.#xy[0] /= m; this.#xy[1] /= m; this.#computeMagnitude(); return this; } /** * Projects this vector onto another vector. * @param v - The vector to project onto. * @returns This Vec2 instance for method chaining. */ project(v) { const x = this.#xy[0]; const y = this.#xy[1]; const vX = v.#xy[0]; const vY = v.#xy[1]; const vM = v.#magnitude; const f = this.#magnitude * cos(atan2(x * vY - y * vX, x * vX + y * vY)); this.#xy[0] = (vX / vM) * f; this.#xy[1] = (vY / vM) * f; this.#computeMagnitude(); return this; } /** * Sets this vector to a random direction with the same magnitude. * @param random - A function that returns a random number between 0 and 1. * @returns This Vec2 instance for method chaining. */ random(random = Math.random) { const phi = random() * PI2; const m = this.#magnitude; this.#xy[0] = m * cos(phi); this.#xy[1] = m * sin(phi); this.#computeMagnitude(); return this; } /** * Rotates this vector around the Z-axis. * @param phi - The angle of rotation in radians. * @returns This Vec2 instance for method chaining. */ rotateZ(phi) { const cosPhi = cos(phi); const sinPhi = sin(phi); const x = this.#xy[0]; const y = this.#xy[1]; this.#xy[0] = x * cosPhi - y * sinPhi; this.#xy[1] = x * sinPhi + y * cosPhi; this.#computeMagnitude(); return this; } /** * Checks if this vector is equal to another vector. * @param v - The other vector. * @returns True if the vectors are equal, false otherwise. */ satisfyEquality(v) { return this.#xy[0] === v.#xy[0] && this.#xy[1] === v.#xy[1]; } /** * Checks if this vector is opposite to another vector. * @param v - The other vector. * @returns True if the vectors are opposite, false otherwise. */ satisfyOpposition(v) { return this.#xy[0] === -v.#xy[0] && this.#xy[1] === -v.#xy[1]; } /** * Compares this vector with another vector using an epsilon value for floating-point comparison. * @param v - The vector to compare with. * @param epsilon - The maximum difference between components to consider them equal. * @returns True if the vectors are equal within epsilon, false otherwise. */ equals(v, epsilon = Number.EPSILON) { return abs(this.#xy[0] - v.#xy[0]) <= epsilon && abs(this.#xy[1] - v.#xy[1]) <= epsilon; } /** * Scales this vector by a scalar value. * @param c - The scalar value. * @returns This Vec2 instance for method chaining. */ scale(c) { this.#xy[0] *= c; this.#xy[1] *= c; this.#computeMagnitude(); return this; } /** * Rotates this vector 90 degrees to the left. * @returns This Vec2 instance for method chaining. */ turnLeft() { const x = this.#xy[0]; this.#xy[0] = -this.#xy[1]; this.#xy[1] = x; this.#computeMagnitude(); return this; } /** * Rotates this vector 90 degrees to the right. * @returns This Vec2 instance for method chaining. */ turnRight() { const x = this.#xy[0]; this.#xy[0] = this.#xy[1]; this.#xy[1] = -x; this.#computeMagnitude(); return this; } /** * Sets this vector to zero. * @returns This Vec2 instance for method chaining. */ zero() { this.#xy[0] = 0; this.#xy[1] = 0; this.#computeMagnitude(); return this; } /** * Makes the Vec2 instance iterable. * @yields The x and y components of the vector. */ *[Symbol.iterator]() { yield this.#xy[0]; yield this.#xy[1]; } /** * Returns a string representation of the vector. * @returns A string in the format "Vec2(x, y)". */ toString() { return `Vec2(${this.#xy[0]}, ${this.#xy[1]})`; } /** * Converts the vector to a plain object. * @returns An object with x and y properties. */ toObject() { return { x: this.#xy[0], y: this.#xy[1] }; } /** * Serializes the vector to a JSON-friendly format. * @returns A JSON-friendly object representation of the vector. */ toJSON() { return this.toObject(); } } exports.Vec2 = Vec2;