// utilMath.js// Created by Ada <ada@thingvellir.net> on 2025-12-06// Copyright 2025 Overte e.V.//// Distributed under the Apache License, Version 2.0.// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html//// SPDX-License-Identifier: Apache-2.0"use strict"; /** @type {number} */const DEGREES_TO_RADIANS = Math.PI / 180; /** @type {number} */const RADIANS_TO_DEGREES = 180 / Math.PI; /** * @param {number} from * @param {number} to * @param {number} t * @returns {number} */function lerp(from, to, t) { return from + t * (to - from);} /** * @param {number} value * @param {number} min * @param {number} max * @returns {number} */function clamp(value, min, max) { return Math.max(min, Math.min(max, value));} /** * Rounds a `Number` to N fractional digits. * Similar to `Number.toFixed`, but without stringifying. * @param {number} number * @param {number} digits - Fractional decimal digits * @returns {number} */function quantize(number, digits) { const scale = 10 ** digits; return Math.round(number * scale) / scale;} /** * @param {number} a * @param {number} b * @param {number} [epsilon=1e-5] * @returns {boolean} `true` if `a` is roughly equal to `b`, * within the range specified by `epsilon`, otherwise `false`. */function withinEpsilon(a, b, epsilon = 1e-5) { return Math.abs(a - b) <= epsilon;} /** * Converts a human-readable duration into milliseconds. * @param {Object} duration * @param {number} [duration.seconds=0] * @param {number} [duration.minutes=0] * @param {number} [duration.hours=0] * @param {number} [duration.weeks=0] * @param {number} [duration.months=0] * @param {number} [duration.years=0] * @returns {number} */function durationMillis({ seconds = 0, minutes = 0, hours = 0, days = 0, weeks = 0, months = 0, years = 0,}) { const MSEC_PER_SEC = 1000; const MSEC_PER_MIN = MSEC_PER_SEC * 60; const MSEC_PER_HOUR = MSEC_PER_MIN * 60; const MSEC_PER_DAY = MSEC_PER_HOUR * 24; const MSEC_PER_WEEK = MSEC_PER_DAY * 7; const MSEC_PER_MONTH = MSEC_PER_DAY * 31; const MSEC_PER_YEAR = MSEC_PER_MONTH * 12; let duration = 0; duration += seconds * MSEC_PER_SEC; duration += minutes * MSEC_PER_MIN; duration += hours * MSEC_PER_HOUR; duration += days * MSEC_PER_DAY; duration += weeks * MSEC_PER_WEEK; duration += months * MSEC_PER_MONTH; duration += years * MSEC_PER_YEAR; return duration;} /** * A scalar 3-vector. Used for representing positions and Euler angles. * * Compatible with the existing {@link Vec3} type. */class Vector3 { /** * `Vector3(0, 0, 0)` * @returns {Vector3} */ static get ZERO() { return new Vector3(0, 0, 0); } /** * `Vector3(0, 1, 0)` * @returns {Vector3} */ static get UP() { return new Vector3(0, 1, 0); } /** * `Vector3(0, -1, 0)` * @returns {Vector3} */ static get DOWN() { return new Vector3(0, -1, 0); } /** * `Vector3(1, 0, 0)` * @returns {Vector3} */ static get RIGHT() { return new Vector3(1, 0, 0); } /** * `Vector3(-1, 0, 0)` * @returns {Vector3} */ static get LEFT() { return new Vector3(-1, 0, 0); } /** * `Vector3(0, 0, -1)` * @returns {Vector3} */ static get FORWARD() { return new Vector3(0, 0, -1); } /** * `Vector3(0, 0, 1)` * @returns {Vector3} */ static get BACKWARD() { return new Vector3(0, 0, 1); } /** @type {number} */ x; /** @type {number} */ y; /** @type {number} */ z; /** * Alias of `x` * @returns {number} */ get [0]() { return this.x; } /** * Alias of `y` * @returns {number} */ get [1]() { return this.y; } /** * Alias of `z` * @returns {number} */ get [2]() { return this.z; } *[Symbol.iterator]() { yield this.x; yield this.y; yield this.z; } /** @returns {string} */ toString() { return `Vector3(${this.x}, ${this.y}, ${this.z})`; } /** * @overload * @param {number} [x=0] * @param {number} [y=0] * @param {number} [z=0] */ /** * @overload * @param {Object|Vector3} vec * @param {number} [vec.x=0] * @param {number} [vec.y=0] * @param {number} [vec.z=0] */ /** * @overload * @param {number[]} [xyz=[0, 0, 0]] */ constructor(...args) { if (args[0] instanceof Vector3 || typeof(args[0]) === "object") { const { x = 0, y = 0, z = 0 } = args[0]; this.x = x; this.y = y; this.z = z; } else if (args[0] instanceof Array) { const [ x = 0, y = 0, z = 0 ] = args[0]; this.x = x; this.y = y; this.z = z; } else { const [ x = 0, y = 0, z = 0 ] = args; this.x = x; this.y = y; this.z = z; } } /** * @param {Vector3} rhs * @returns {Vector3} `this` + `rhs` */ add(rhs) { return new Vector3(this.x + rhs.x, this.y + rhs.y, this.z + rhs.z); } /** * @param {Vector3} rhs * @returns {Vector3} `this` - `rhs` */ subtract(rhs) { return new Vector3(this.x - rhs.x, this.y - rhs.y, this.z - rhs.z); } /** * @param {Vector3|number} rhs * @returns {Vector3} `this` * `rhs` */ multiply(rhs) { if (typeof(rhs) === "number") { return new Vector3(this.x * rhs, this.y * rhs, this.z * rhs); } else if (rhs instanceof Quaternion) { // The convention is for Quat * Vec, // but permit Vec * Quat as an // undocumented convenience alias return rhs.multiply(this); } else { return new Vector3(this.x * rhs.x, this.y * rhs.y, this.z * rhs.z); } } /** * @param {Vector3|number} rhs * @returns {Vector3} `this` / `rhs` */ divide(rhs) { if (typeof(rhs) === "number") { return new Vector3(this.x / rhs, this.y / rhs, this.z / rhs); } else { return new Vector3(this.x / rhs.x, this.y / rhs.y, this.z / rhs.z); } } /** * Only `true` if `this` and `rhs` * are *exactly* equal. See {@link Vector3.withinEpsilon} * @param {Vector3} rhs * @returns {boolean} `this` === `rhs` */ equal(rhs) { return this.x === rhs.x && this.y === rhs.y && this.z === rhs.z; } /** * @param {Vector3} rhs * @param {number} [epsilon=1e-5] * @returns {Vector3} `this` ≈ `rhs` */ withinEpsilon(rhs, epsilon = 1e-5) { return ( withinEpsilon(this.x, rhs.x, epsilon) && withinEpsilon(this.y, rhs.y, epsilon) && withinEpsilon(this.z, rhs.z, epsilon) ); } /** * @returns {Vector3} The euclidean length of `this` */ length() { return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); } /** * @returns {Vector3} `this` normalized to a length of 1, * or `Vector3(0, 0, 0)` if the total length is zero. */ normalized() { // prevent infinities or NaNs if we try to normalize (0, 0, 0) const epsilon = 1e-5; const length = this.length(); if (length >= epsilon) { return new Vector3(this.x / length, this.y / length, this.z / length); } else { return new Vector3(0, 0, 0); } } /** * @param {Vector3} rhs * @returns {number} Dot product of `this` and `rhs`, * approaching 1 if they point in a similar direction, * -1 if opposite, and 0 if a right angle to eachother. */ dot(rhs) { return this.x * rhs.x + this.y * rhs.x + this.z * rhs.z; } /** * @param {Vector3} rhs * @returns {Vector3} Cross product of `this` and `rhs` */ cross(rhs) { const { x: ax, y: ay, z: az } = this; const { x: bx, y: by, z: bz } = rhs; return new Vector3( (ay * bz) - (az * by), (az * bx) - (ax * bz), (ax * by) - (ay * bx), ); } /** * @param {number} digits * @returns {Vector3} */ quantize(digits) { return new Vector3( quantize(this.x, digits), quantize(this.y, digits), quantize(this.z, digits) ); } /** * @returns {Vector3} */ round() { return new Vector3(Math.round(this.x), Math.round(this.y), Math.round(this.z)); } /** * @returns {Vector3} */ abs() { return new Vector3(Math.abs(this.x), Math.abs(this.y), Math.abs(this.z)); } /** * @param {Vector3} rhs * @returns {Vector3} */ distance(rhs) { const { x: lx, y: ly, z: lz } = this; const { x: rx, y: ry, z: rz } = rhs; const x = (lx - rx) * (lx - rx); const y = (ly - ry) * (ly - ry); const z = (lz - rz) * (lz - rz); return Math.sqrt(x + y + z); } /** * @param {Vector3} origin * @returns {Vector3} `this` - `origin` */ relativeTo(origin) { return this.subtract(origin); } /** * @param {Vector3} rhs * @param {number} t * @returns {Vector3} */ lerpTo(rhs, t) { let { x: lx, y: ly, z: lz } = this; let { x: rx, y: ry, z: rz } = rhs; let x = lerp(lx, rx, t); let y = lerp(ly, ry, t); let z = lerp(lz, rz, t); return new Vector3(x, y, z); }} /** * A rotation quaternion. Quaternions represent rotations unambiguously, * unlike Euler angles which can suffer from gimbal lock, * but aren't generally human-readable. * * See {@link Quaternion.toEulerDegrees} and {@link Quaternion.fromPitchYawRollDegrees}. * * Compatible with the existing {@link Quat} type. * * {@link https://en.wikipedia.org/wiki/Quaternion} */class Quaternion { /** * The identity quaternion. Represents no rotation. * * @returns {Quaternion} `Quaternion(0, 0, 0, 1)` */ static get IDENTITY() { return new Quaternion(0, 0, 0, 1); } /** @type {number} */ x; /** @type {number} */ y; /** @type {number} */ z; /** @type {number} */ w; /** * Alias of `x` * @returns {number} */ get [0]() { return this.x; } /** * Alias of `y` * @returns {number} */ get [1]() { return this.y; } /** * Alias of `z` * @returns {number} */ get [2]() { return this.z; } /** * Alias of `w` * @returns {number} */ get [3]() { return this.w; } *[Symbol.iterator]() { yield this.x; yield this.y; yield this.z; yield this.w; } /** @returns {string} */ toString() { return `Quaternion(${this.x}, ${this.y}, ${this.z}, ${this.w})`; } /** * @overload * @throws Throws an error if x, y, z, or w is missing * @param {number} x * @param {number} y * @param {number} z * @param {number} w */ /** * @overload * @throws Throws an error if x, y, z, or w is missing * @param {number[]} xyzw */ /** * @overload * @throws Throws an error if x, y, z, or w is missing * @param {Object|Quaternion} quat * @param {number} quat.x * @param {number} quat.y * @param {number} quat.z * @param {number} quat.w */ constructor(...args) { if (args.length === 0) { this.x = 0; this.y = 0; this.z = 0; this.w = 1; } else if (args[0] instanceof Quaternion || typeof(args[0]) === "object") { const { x, y, z, w } = args[0]; if ( x === undefined || y === undefined || z === undefined || w === undefined ) { throw new Error("x, y, z, and w must all be specified"); } this.x = x; this.y = y; this.z = z; this.w = w; } else if (args[0] instanceof Array) { const [ x, y, z, w ] = args[0]; if ( x === undefined || y === undefined || z === undefined || w === undefined ) { throw new Error("x, y, z, and w must all be specified"); } this.x = x; this.y = y; this.z = z; this.w = w; } else { const [ x, y, z, w ] = args; if (Lines 1–500 of 1450. Load more
// utilMath.js // Created by Ada <ada@thingvellir.net> on 2025-12-06 // Copyright 2025 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // // SPDX-License-Identifier: Apache-2.0 "use strict"; /** @type {number} */ const DEGREES_TO_RADIANS = Math.PI / 180; /** @type {number} */ const RADIANS_TO_DEGREES = 180 / Math.PI; /** * @param {number} from * @param {number} to * @param {number} t * @returns {number} */ function lerp(from, to, t) { return from + t * (to - from); } /** * @param {number} value * @param {number} min * @param {number} max * @returns {number} */ function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } /** * Rounds a `Number` to N fractional digits. * Similar to `Number.toFixed`, but without stringifying. * @param {number} number * @param {number} digits - Fractional decimal digits * @returns {number} */ function quantize(number, digits) { const scale = 10 ** digits; return Math.round(number * scale) / scale; } /** * @param {number} a * @param {number} b * @param {number} [epsilon=1e-5] * @returns {boolean} `true` if `a` is roughly equal to `b`, * within the range specified by `epsilon`, otherwise `false`. */ function withinEpsilon(a, b, epsilon = 1e-5) { return Math.abs(a - b) <= epsilon; } /** * Converts a human-readable duration into milliseconds. * @param {Object} duration * @param {number} [duration.seconds=0] * @param {number} [duration.minutes=0] * @param {number} [duration.hours=0] * @param {number} [duration.weeks=0] * @param {number} [duration.months=0] * @param {number} [duration.years=0] * @returns {number} */ function durationMillis({ seconds = 0, minutes = 0, hours = 0, days = 0, weeks = 0, months = 0, years = 0, }) { const MSEC_PER_SEC = 1000; const MSEC_PER_MIN = MSEC_PER_SEC * 60; const MSEC_PER_HOUR = MSEC_PER_MIN * 60; const MSEC_PER_DAY = MSEC_PER_HOUR * 24; const MSEC_PER_WEEK = MSEC_PER_DAY * 7; const MSEC_PER_MONTH = MSEC_PER_DAY * 31; const MSEC_PER_YEAR = MSEC_PER_MONTH * 12; let duration = 0; duration += seconds * MSEC_PER_SEC; duration += minutes * MSEC_PER_MIN; duration += hours * MSEC_PER_HOUR; duration += days * MSEC_PER_DAY; duration += weeks * MSEC_PER_WEEK; duration += months * MSEC_PER_MONTH; duration += years * MSEC_PER_YEAR; return duration; } /** * A scalar 3-vector. Used for representing positions and Euler angles. * * Compatible with the existing {@link Vec3} type. */ class Vector3 { /** * `Vector3(0, 0, 0)` * @returns {Vector3} */ static get ZERO() { return new Vector3(0, 0, 0); } /** * `Vector3(0, 1, 0)` * @returns {Vector3} */ static get UP() { return new Vector3(0, 1, 0); } /** * `Vector3(0, -1, 0)` * @returns {Vector3} */ static get DOWN() { return new Vector3(0, -1, 0); } /** * `Vector3(1, 0, 0)` * @returns {Vector3} */ static get RIGHT() { return new Vector3(1, 0, 0); } /** * `Vector3(-1, 0, 0)` * @returns {Vector3} */ static get LEFT() { return new Vector3(-1, 0, 0); } /** * `Vector3(0, 0, -1)` * @returns {Vector3} */ static get FORWARD() { return new Vector3(0, 0, -1); } /** * `Vector3(0, 0, 1)` * @returns {Vector3} */ static get BACKWARD() { return new Vector3(0, 0, 1); } /** @type {number} */ x; /** @type {number} */ y; /** @type {number} */ z; /** * Alias of `x` * @returns {number} */ get [0]() { return this.x; } /** * Alias of `y` * @returns {number} */ get [1]() { return this.y; } /** * Alias of `z` * @returns {number} */ get [2]() { return this.z; } *[Symbol.iterator]() { yield this.x; yield this.y; yield this.z; } /** @returns {string} */ toString() { return `Vector3(${this.x}, ${this.y}, ${this.z})`; } /** * @overload * @param {number} [x=0] * @param {number} [y=0] * @param {number} [z=0] */ /** * @overload * @param {Object|Vector3} vec * @param {number} [vec.x=0] * @param {number} [vec.y=0] * @param {number} [vec.z=0] */ /** * @overload * @param {number[]} [xyz=[0, 0, 0]] */ constructor(...args) { if (args[0] instanceof Vector3 || typeof(args[0]) === "object") { const { x = 0, y = 0, z = 0 } = args[0]; this.x = x; this.y = y; this.z = z; } else if (args[0] instanceof Array) { const [ x = 0, y = 0, z = 0 ] = args[0]; this.x = x; this.y = y; this.z = z; } else { const [ x = 0, y = 0, z = 0 ] = args; this.x = x; this.y = y; this.z = z; } } /** * @param {Vector3} rhs * @returns {Vector3} `this` + `rhs` */ add(rhs) { return new Vector3(this.x + rhs.x, this.y + rhs.y, this.z + rhs.z); } /** * @param {Vector3} rhs * @returns {Vector3} `this` - `rhs` */ subtract(rhs) { return new Vector3(this.x - rhs.x, this.y - rhs.y, this.z - rhs.z); } /** * @param {Vector3|number} rhs * @returns {Vector3} `this` * `rhs` */ multiply(rhs) { if (typeof(rhs) === "number") { return new Vector3(this.x * rhs, this.y * rhs, this.z * rhs); } else if (rhs instanceof Quaternion) { // The convention is for Quat * Vec, // but permit Vec * Quat as an // undocumented convenience alias return rhs.multiply(this); } else { return new Vector3(this.x * rhs.x, this.y * rhs.y, this.z * rhs.z); } } /** * @param {Vector3|number} rhs * @returns {Vector3} `this` / `rhs` */ divide(rhs) { if (typeof(rhs) === "number") { return new Vector3(this.x / rhs, this.y / rhs, this.z / rhs); } else { return new Vector3(this.x / rhs.x, this.y / rhs.y, this.z / rhs.z); } } /** * Only `true` if `this` and `rhs` * are *exactly* equal. See {@link Vector3.withinEpsilon} * @param {Vector3} rhs * @returns {boolean} `this` === `rhs` */ equal(rhs) { return this.x === rhs.x && this.y === rhs.y && this.z === rhs.z; } /** * @param {Vector3} rhs * @param {number} [epsilon=1e-5] * @returns {Vector3} `this` ≈ `rhs` */ withinEpsilon(rhs, epsilon = 1e-5) { return ( withinEpsilon(this.x, rhs.x, epsilon) && withinEpsilon(this.y, rhs.y, epsilon) && withinEpsilon(this.z, rhs.z, epsilon) ); } /** * @returns {Vector3} The euclidean length of `this` */ length() { return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); } /** * @returns {Vector3} `this` normalized to a length of 1, * or `Vector3(0, 0, 0)` if the total length is zero. */ normalized() { // prevent infinities or NaNs if we try to normalize (0, 0, 0) const epsilon = 1e-5; const length = this.length(); if (length >= epsilon) { return new Vector3(this.x / length, this.y / length, this.z / length); } else { return new Vector3(0, 0, 0); } } /** * @param {Vector3} rhs * @returns {number} Dot product of `this` and `rhs`, * approaching 1 if they point in a similar direction, * -1 if opposite, and 0 if a right angle to eachother. */ dot(rhs) { return this.x * rhs.x + this.y * rhs.x + this.z * rhs.z; } /** * @param {Vector3} rhs * @returns {Vector3} Cross product of `this` and `rhs` */ cross(rhs) { const { x: ax, y: ay, z: az } = this; const { x: bx, y: by, z: bz } = rhs; return new Vector3( (ay * bz) - (az * by), (az * bx) - (ax * bz), (ax * by) - (ay * bx), ); } /** * @param {number} digits * @returns {Vector3} */ quantize(digits) { return new Vector3( quantize(this.x, digits), quantize(this.y, digits), quantize(this.z, digits) ); } /** * @returns {Vector3} */ round() { return new Vector3(Math.round(this.x), Math.round(this.y), Math.round(this.z)); } /** * @returns {Vector3} */ abs() { return new Vector3(Math.abs(this.x), Math.abs(this.y), Math.abs(this.z)); } /** * @param {Vector3} rhs * @returns {Vector3} */ distance(rhs) { const { x: lx, y: ly, z: lz } = this; const { x: rx, y: ry, z: rz } = rhs; const x = (lx - rx) * (lx - rx); const y = (ly - ry) * (ly - ry); const z = (lz - rz) * (lz - rz); return Math.sqrt(x + y + z); } /** * @param {Vector3} origin * @returns {Vector3} `this` - `origin` */ relativeTo(origin) { return this.subtract(origin); } /** * @param {Vector3} rhs * @param {number} t * @returns {Vector3} */ lerpTo(rhs, t) { let { x: lx, y: ly, z: lz } = this; let { x: rx, y: ry, z: rz } = rhs; let x = lerp(lx, rx, t); let y = lerp(ly, ry, t); let z = lerp(lz, rz, t); return new Vector3(x, y, z); } } /** * A rotation quaternion. Quaternions represent rotations unambiguously, * unlike Euler angles which can suffer from gimbal lock, * but aren't generally human-readable. * * See {@link Quaternion.toEulerDegrees} and {@link Quaternion.fromPitchYawRollDegrees}. * * Compatible with the existing {@link Quat} type. * * {@link https://en.wikipedia.org/wiki/Quaternion} */ class Quaternion { /** * The identity quaternion. Represents no rotation. * * @returns {Quaternion} `Quaternion(0, 0, 0, 1)` */ static get IDENTITY() { return new Quaternion(0, 0, 0, 1); } /** @type {number} */ x; /** @type {number} */ y; /** @type {number} */ z; /** @type {number} */ w; /** * Alias of `x` * @returns {number} */ get [0]() { return this.x; } /** * Alias of `y` * @returns {number} */ get [1]() { return this.y; } /** * Alias of `z` * @returns {number} */ get [2]() { return this.z; } /** * Alias of `w` * @returns {number} */ get [3]() { return this.w; } *[Symbol.iterator]() { yield this.x; yield this.y; yield this.z; yield this.w; } /** @returns {string} */ toString() { return `Quaternion(${this.x}, ${this.y}, ${this.z}, ${this.w})`; } /** * @overload * @throws Throws an error if x, y, z, or w is missing * @param {number} x * @param {number} y * @param {number} z * @param {number} w */ /** * @overload * @throws Throws an error if x, y, z, or w is missing * @param {number[]} xyzw */ /** * @overload * @throws Throws an error if x, y, z, or w is missing * @param {Object|Quaternion} quat * @param {number} quat.x * @param {number} quat.y * @param {number} quat.z * @param {number} quat.w */ constructor(...args) { if (args.length === 0) { this.x = 0; this.y = 0; this.z = 0; this.w = 1; } else if (args[0] instanceof Quaternion || typeof(args[0]) === "object") { const { x, y, z, w } = args[0]; if ( x === undefined || y === undefined || z === undefined || w === undefined ) { throw new Error("x, y, z, and w must all be specified"); } this.x = x; this.y = y; this.z = z; this.w = w; } else if (args[0] instanceof Array) { const [ x, y, z, w ] = args[0]; if ( x === undefined || y === undefined || z === undefined || w === undefined ) { throw new Error("x, y, z, and w must all be specified"); } this.x = x; this.y = y; this.z = z; this.w = w; } else { const [ x, y, z, w ] = args; if (