UNPKG

@inglorious/utils

Version:

A set of general-purpose utility functions designed with functional programming principles in mind.

427 lines (373 loc) 12.4 kB
/** * @typedef {import("../../types/math").Vector} Vector * @typedef {import("../../types/math").Vector2} Vector2 * @typedef {import("../../types/math").Vector3} Vector3 */ import { v } from "../v.js" import { abs as nAbs, clamp as nClamp, mod as nMod, snap as nSnap, } from "./numbers.js" import { quaternion } from "./quaternion.js" import { hypothenuse } from "./triangle.js" import { atan2, cos, sin } from "./trigonometry.js" import { cross, sum } from "./vectors.js" const ZERO_VECTOR = v(0, 0, 0) // eslint-disable-line no-magic-numbers const UNIT_VECTOR = v(1, 0, 0) // eslint-disable-line no-magic-numbers const X = 0 const Y = 1 const Z = 2 const LAST_COORDINATE = 1 const TWO_COORDINATES = 2 const NO_Y = 0 const DEFAULT_PRECISION = 1 const DEFAULT_DECIMALS = 0 /** * Returns the absolute value of each component in the vector. * @param {Vector} vector - The input vector. * @returns {Vector} The vector with absolute values. */ export function abs(vector) { return v(...vector.map(nAbs)) } /** * Calculates the angle of the vector in radians. * @param {Vector} vector - The input vector. * @returns {number} The angle of the vector in the XZ plane (for 3D) or XY plane (for 2D), in radians. */ export function angle(vector) { return atan2(vector[vector.length - LAST_COORDINATE], vector[X]) } /** * Clamps the magnitude of the vector between the given min and max values. * @param {Vector} vector - The input vector. * @param {number|Vector} min - The minimum magnitude or a vector representing the lower bounds for each component. * @param {number|Vector} max - The maximum magnitude or a vector representing the upper bounds for each component. * @returns {Vector} The clamped vector. */ export function clamp(vector, min, max) { const length = magnitude(vector) if (typeof min === "number" && length < min) { return setMagnitude(vector, min) } if (typeof max === "number" && length > max) { return setMagnitude(vector, max) } if (typeof min !== "number" && typeof max !== "number") { return v( ...vector.map((coordinate, index) => nClamp(coordinate, min[index], max[index]), ), ) } return vector } /** * Returns the conjugate of the vector. * @param {Vector} vector - The input vector. * @returns {Vector} The conjugated vector. */ export function conjugate(vector) { return v( ...vector.map((coordinate, index) => (index ? -coordinate : coordinate)), ) } /** * Creates a 3D vector with the given magnitude and angle. * @param {number} magnitude - The magnitude of the vector. * @param {number} angle - The angle of the vector in radians. * @returns {Vector3} The created 3D vector. */ export function createVector(magnitude, angle) { return scale(fromAngle(angle), magnitude) } /** * Divides a scalar by each component of the vector. * @param {number} scalar - The dividend. * @param {Vector} vector - The divisor vector. * @returns {Vector} The resulting vector. */ export function divideBy(scalar, vector) { return v(...vector.map((coordinate) => scalar / coordinate)) } /** * Divides each component of the vector by the given scalar. * @param {Vector} vector - The input vector. * @param {number} scalar - The scalar value. * @returns {Vector} The resulting vector. */ export function divide(vector, scalar) { return v(...vector.map((coordinate) => coordinate / scalar)) } /** * Converts a 2D vector [x, z] into a 3D vector [x, 0, z]. * @param {Vector2} vector - A 2D vector represented as [x, z]. * @returns {Vector3} A 3D vector represented as [x, 0, z]. */ export function from2D(vector) { const [x, z] = vector return v(x, NO_Y, z) } /** * Creates a 3D unit vector from the given angle in the XZ plane. * @param {number} angle - The angle in radians. * @returns {Vector3} The unit vector. */ export function fromAngle(angle) { return rotate(UNIT_VECTOR, angle) } /** * Checks if a value is a vector. * This is determined by checking for the `__isVector__` property, which is * added by the `v()` factory function for efficient type checking. * @param {*} value - The value to check. * @returns {boolean} True if the value is a vector, false otherwise. */ export function isVector(value) { return !!value?.__isVector__ } /** * Calculates the magnitude (length) of the vector. * @param {Vector} vector - The input vector. * @returns {number} The magnitude of the vector. */ export function magnitude(vector) { return hypothenuse(...vector) } export const length = magnitude /** * Calculates the modulus of each component in the vector with the given divisor. * @param {Vector} vector - The input vector. * @param {number} divisor - The divisor value. * @returns {Vector} The resulting vector. */ export function mod(vector, divisor) { return v(...vector.map((coordinate) => nMod(coordinate, divisor))) } /** * Calculates the modulus of a scalar with each component of the vector. * @param {number} scalar - The dividend. * @param {Vector} vector - The divisor vector. * @returns {Vector} The resulting vector. */ export function modOf(scalar, vector) { return v(...vector.map((coordinate) => nMod(scalar, coordinate))) } /** * Multiplies each component of the vector by the given scalar. * @param {Vector} vector - The input vector. * @param {number} scalar - The scalar value. * @returns {Vector} The resulting vector. */ export function multiply(vector, scalar) { return v(...vector.map((coordinate) => coordinate * scalar)) } /** * Alias for the power function. * @type {typeof power} */ export const pow = power /** * Raises each component of the vector to the given exponent. * @param {Vector} vector - The input vector. * @param {number} exponent - The exponent value. * @returns {Vector} The resulting vector. */ export function power(vector, exponent) { return v(...vector.map((coordinate) => coordinate ** exponent)) } /** * Raises a scalar to the power of each component of the vector. * @param {number} scalar - The base value. * @param {Vector} vector - The exponent vector. * @returns {Vector} The resulting vector. */ export function powerOf(scalar, vector) { return v(...vector.map((coordinate) => scalar ** coordinate)) } /** * Normalizes the vector to have a magnitude of 1. * @param {Vector} vector - The input vector. * @returns {Vector} The normalized vector. */ export function normalize(vector) { const length = magnitude(vector) return v(...vector.map((coordinate) => coordinate / length)) } /** * Alias for the mod function. * @type {typeof mod} */ export const remainder = mod /** * Rotates the vector by the given angle. Handles 2D and 3D vectors. * For 3D vectors, rotation is around the Y-axis. * @param {Vector} vector - The input vector (2D or 3D). * @param {number} angle - The angle in radians. * @returns {Vector} The rotated vector. */ export function rotate(vector, angle) { const is2D = vector.length === TWO_COORDINATES const vector3 = is2D ? from2D(vector) : vector const result = rotateWithQuaternion(vector3, angle) return is2D ? to2D(result) : result } /** * Alias for the multiply function. * @type {typeof multiply} */ export const scale = multiply /** * Sets the angle of the vector in the XZ plane while maintaining its magnitude. * @param {Vector} vector - The input vector. * @param {number} angle - The new angle in radians. * @returns {Vector3} The vector with the updated angle. */ export function setAngle(vector, angle) { const length = magnitude(vector) const [x, z] = toCartesian([length, angle]) return v(x, NO_Y, z) } /** * Alias for the setMagnitude function. * @type {typeof setMagnitude} */ export const setLength = setMagnitude /** * Sets the magnitude of the vector while maintaining its direction. * @param {Vector} vector - The input vector. * @param {number} length - The new magnitude. * @returns {Vector} The vector with the updated magnitude. */ export function setMagnitude(vector, length) { const normalized = normalize(vector) return scale(normalized, length) } /** * Shifts the components of the vector by the given index. * @param {Vector} vector - The input vector. * @param {number} index - The index to shift by. * @returns {Vector} The shifted vector. */ export function shift(vector, index) { return v(...vector.slice(index), ...vector.slice(X, index)) } /** * Snaps each component of the vector to the nearest multiple of the given precision. * @param {Vector} vector - The input vector. * @param {number} [precision=DEFAULT_PRECISION] - The precision value. * @returns {Vector} The snapped vector. */ export function snap(vector, precision = DEFAULT_PRECISION) { return v( ...vector.map((coordinate) => nSnap(coordinate, -precision, precision)), ) } /** * Alias for the multiply function. * @type {typeof multiply} */ export const times = multiply /** * Converts a 3D vector [x, y, z] into a 2D vector [x, z]. * @param {Vector3} vector - A 3D vector represented as [x, y, z]. * @returns {Vector2} A 2D vector represented as [x, z]. */ export function to2D(vector) { const [x, , z] = vector return v(x, z) } /** * Converts 2D polar coordinates to 2D Cartesian coordinates. * @param {Vector2} vector - The polar coordinates [magnitude, angle]. * @returns {Vector2} The Cartesian coordinates [x, y]. */ export function toCartesian([magnitude, angle]) { return v(magnitude * cos(angle), magnitude * sin(angle)) } /** * Converts a 3D cartesian vector to cylindrical coordinates. * @param {Vector3} vector - The input vector [x, y, z]. * @returns {Vector3} The cylindrical coordinates [radius, theta, z]. */ export function toCylindrical(vector) { const radius = magnitude(vector) const theta = angle(vector) return v(radius * cos(theta), radius * sin(theta), vector[Z]) } /** * Converts a 2D cartesian vector to 2D polar coordinates. * @param {Vector2} vector - The input vector [x, y]. * @returns {Vector2} The polar coordinates [magnitude, angle]. */ export function toPolar(vector) { return v(magnitude(vector), angle(vector)) } /** * Converts a vector to a string representation. * @param {Vector} vector - The input vector. * @param {number} [decimals=DEFAULT_DECIMALS] - The number of decimal places. * @returns {string} The string representation of the vector. */ export function toString(vector, decimals = DEFAULT_DECIMALS) { return `[${vector .map((coordinate) => coordinate.toFixed(decimals)) .join(", ")}]` } /** * Converts a 3D cartesian vector to spherical coordinates [radius, inclination, azimuth]. * - radius (r): distance from the origin. * - inclination (θ): angle from the Y-axis (0 to PI). * - azimuth (φ): angle from the X-axis in the XZ-plane (-PI to PI). * @param {Vector3} vector The cartesian vector [x, y, z]. * @returns {Vector3} The spherical coordinates [r, θ, φ]. */ export function toSpherical(vector) { const r = magnitude(vector) if (!r) { return zero() } // In a Y-up system, inclination is the angle with the Y axis. const inclination = Math.acos(vector[Y] / r) // Azimuth is the angle in the XZ plane, which `angle()` calculates. const azimuth = angle(vector) return v(r, inclination, azimuth) } /** * Creates a 3D unit vector in the XZ plane with the given angle. * @param {number} [angle=0] - The angle in radians. * @returns {Vector3} The unit vector. */ export function unit(angle) { if (!angle) { return v(...UNIT_VECTOR) } return setAngle(UNIT_VECTOR, angle) } /** * Creates a zero vector of 3 dimensions. * @returns {Vector3} The zero vector. */ export function zero() { return v(...ZERO_VECTOR) } /** * Rotates a 3D vector around the Y-axis using a quaternion. * @param {Vector3} vector - The input vector. * @param {number} angle - The angle in radians. * @returns {Vector3} The rotated vector. */ function rotateWithQuaternion(vector, angle) { if (!angle) { return vector } // @see https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Performance_comparisons const [w, ...r] = quaternion(angle) const result = sum( vector, cross(scale(r, 2), sum(cross(r, vector), scale(vector, w))), // eslint-disable-line no-magic-numbers ) return conjugate(result) // HACK: not really sure why I should invert the result, it just works this way }