UNPKG

colorjs.io

Version:

Let’s get serious about color

472 lines (426 loc) 11.4 kB
/** * Various utility functions */ export { default as multiplyMatrices, multiply_v3_m3x3 } from "./multiply-matrices.js"; /** * Check if a value is a string (including a String object) * @param {any} str - Value to check * @returns {str is string} */ export function isString (str) { return type(str) === "string"; } /** * Determine the internal JavaScript [[Class]] of an object. * @param {any} o - Value to check * @returns {string} */ export function type (o) { let str = Object.prototype.toString.call(o); return (str.match(/^\[object\s+(.*?)\]$/)[1] || "").toLowerCase(); } /** * @param {number} n * @param {{ precision?: number | undefined, unit?: string | undefined }} options * @returns {string} */ export function serializeNumber (n, { precision = 16, unit }) { if (isNone(n)) { return "none"; } n = +toPrecision(n, precision); return n + (unit ?? ""); } /** * Check if a value corresponds to a none argument * @param {any} n - Value to check * @returns {n is null} */ export function isNone (n) { return n === null; } /** * Replace none values with 0 * @param {number | null} n * @returns {number} */ export function skipNone (n) { return isNone(n) ? 0 : n; } /** * Round a number to a certain number of significant digits * @param {number} n - The number to round * @param {number} precision - Number of significant digits */ export function toPrecision (n, precision) { if (n === 0) { return 0; } let integer = ~~n; let digits = 0; if (integer && precision) { digits = ~~Math.log10(Math.abs(integer)) + 1; } const multiplier = 10.0 ** (precision - digits); return Math.floor(n * multiplier + 0.5) / multiplier; } /** * Interpolate between two values, either of which may be a `none` value. * A `none` endpoint is treated as having the other endpoint's value; * if both are `none`, the result is `none` as well. * @param {number | null} start * @param {number | null} end * @param {number} p * @returns {number | null} */ export function interpolate (start, end, p) { if (isNone(start) || isNaN(start)) { return end; } if (isNone(end) || isNaN(end)) { return start; } return start + (end - start) * p; } /** * @param {number} start * @param {number} end * @param {number} value */ export function interpolateInv (start, end, value) { return (value - start) / (end - start); } /** * @param {[number, number]} from * @param {[number, number]} to * @param {number} value */ export function mapRange (from, to, value) { if ( !from || !to || from === to || (from[0] === to[0] && from[1] === to[1]) || isNaN(value) || value === null ) { // Ranges missing or the same return value; } return interpolate(to[0], to[1], interpolateInv(from[0], from[1], value)); } /** * Clamp value between the minimum and maximum * @param {number} min minimum value to return * @param {number} val the value to return if it is between min and max * @param {number} max maximum value to return */ export function clamp (min, val, max) { return Math.max(Math.min(max, val), min); } /** * Copy sign of one value to another. * @param {number} to - Number to copy sign to * @param {number} from - Number to copy sign from */ export function copySign (to, from) { return Math.sign(to) === Math.sign(from) ? to : -to; } /** * Perform pow on a signed number and copy sign to result * @param {number} base The base number * @param {number} exp The exponent */ export function spow (base, exp) { return copySign(Math.abs(base) ** exp, base); } /** * Perform a divide, but return zero if the denominator is zero * @param {number} n The numerator * @param {number} d The denominator */ export function zdiv (n, d) { return d === 0 ? 0 : n / d; } /** * Perform a bisect on a sorted list and locate the insertion point for * a value in arr to maintain sorted order. * @param {number[]} arr - array of sorted numbers * @param {number} value - value to find insertion point for * @param {number} lo - used to specify a the low end of a subset of the list * @param {number} hi - used to specify a the high end of a subset of the list */ export function bisectLeft (arr, value, lo = 0, hi = arr.length) { while (lo < hi) { const mid = (lo + hi) >> 1; if (arr[mid] < value) { lo = mid + 1; } else { hi = mid; } } return lo; } /** * Determines whether an argument is an instance of a constructor, including subclasses. * This is done by first just checking `instanceof`, * and then comparing the string names of the constructors if that fails. * @param {any} arg * @param {C} constructor * @template {new (...args: any) => any} C * @returns {arg is InstanceType<C>} */ export function isInstance (arg, constructor) { if (arg instanceof constructor) { return true; } const targetName = constructor.name; while (arg) { const proto = Object.getPrototypeOf(arg); const constructorName = proto?.constructor?.name; if (constructorName === targetName) { return true; } if (!constructorName || constructorName === "Object") { return false; } arg = proto; } return false; } /** * Generate a matrix of size NxN with the given diagonal values of length N. * * @param {number[]} values * @returns {number[][]} */ export function diag (values) { const n = values.length; const matrix = []; for (let i = 0; i < n; i++) { matrix[i] = []; for (let j = 0; j < n; j++) { matrix[i][j] = i === j ? values[i] : 0; } } return matrix; } /** * Calculate the LU decomposition of an NxN matrix. * * P is returned as PA = UL or A = P'UL which follows Matlab and Octave opposed to Scipy which returns P as * A = PUL or P'A = UL. For matrix inverse, we need P such that PA = UL and it is faster not having to invert * P, even if we can invert it fairly fast as it is just a shuffled identity matrix. * * P is returned as a permutation matrix unless pIndices is true, in which case P would be returned as * a vector containing the indexes such that A[P,:] = L*U. * * Reference: * - https://www.statlect.com/matrix-algebra/Gaussian-elimination * - https://www.sciencedirect.com/topics/mathematics/partial-pivoting * * @overload * @param {number[][]} matrix * @param {{ pIndices?: false | undefined }} [options] * @returns {[number[][], number[][], number[][]]} */ /** * @overload * @param {number[][]} matrix * @param {{ pIndices?: true }} [options] * @returns {[number[], number[][], number[][]]} */ /** * @param {number[][]} matrix * @param {{ pIndices?: boolean | undefined }} [options] * @returns {[number[] | number[][], number[][], number[][]]} */ export function lu (matrix, { pIndices = false } = {}) { let p1, p2, l, u; const n = matrix.length; // Initialize the triangle matrices along with the permutation matrix. if (pIndices) { p1 = Array.from({ length: n }, (_, index) => index); l = diag(new Array(n).fill(1)); } else { p2 = diag(new Array(n).fill(1)); l = structuredClone(p2); } u = structuredClone(matrix); // Create upper and lower triangle in 'u' and 'l'. 'p' tracks the permutation (relative position of rows) for (let i = 0; i < n - 1; i++) { // Partial pivoting: identify the row with the maximal value in the column let j = i; let maximum = Math.abs(u[i][i]); for (let k = i + 1; k < n; k++) { const a = Math.abs(u[k][i]); if (a > maximum) { j = k; maximum = a; } } // Partial pivoting: Swap rows if (j != i) { // Exchange current upper triangle row with row with maximal value at pivot // Update permutation matrix as well [u[i], u[j]] = [u[j], u[i]]; if (pIndices) { [p1[i], p1[j]] = [p1[j], p1[i]]; } else { [p2[i], p2[j]] = [p2[j], p2[i]]; } // Only swap columns up to the pivot for the lower triangle, // if on first row, there is nothing to swap if (i) { for (let k = 0; k < i; k++) { [l[i][k], l[j][k]] = [l[j][k], l[i][k]]; } } } // Zero at pivot point, nothing to do else if (!maximum) { continue; } // We have a pivot point, let's zero out everything above and below // the 'l' and 'u' diagonal respectively for (let j = i + 1; j < n; j++) { const scalar = u[j][i] / u[i][i]; for (let k = i; k < n; k++) { u[j][k] += -u[i][k] * scalar; l[j][k] += l[i][k] * scalar; } } } if (pIndices) { return [p1, l, u]; } return [p2, l, u]; } /** * Forward substitution for solution of ax = b where a and b are matricies. * * @param {number[][]} a * @param {number[][]} b * @param {number} n * @returns {number[][]} */ function forwardSubMatrix (a, b, n) { for (let i = 0; i < n; i++) { const v = b[i]; for (let j = 0; j < i; j++) { for (let k = 0; k < n; k++) { v[k] -= a[i][j] * b[j][k]; } } for (let j = 0; j < n; j++) { v[j] /= a[i][i]; } } return b; } /** * Back substitution for solution of ax = b where a and b are matricies. * * @param {number[][]} a * @param {number[][]} b * @param {number} n * @returns {number[][]} */ function backSubMatrix (a, b, n) { for (let i = n - 1; i > -1; i--) { const v = b[i]; for (let j = i + 1; j < n; j++) { for (var k = 0; k < n; k++) { v[k] -= a[i][j] * b[j][k]; } } for (let j = 0; j < n; j++) { b[i][j] /= a[i][i]; } } return b; } /** * Forward substitution for solution of ax = b where a is a matrix and b is a vector. * * @param {number[][]} a * @param {number[]} b * @param {number} n * @returns {number[]} */ function forwardSubVector (a, b, n) { for (let i = 0; i < n; i++) { let v = b[i]; for (let j = 0; j < i; j++) { v -= a[i][j] * b[j]; } b[i] = v / a[i][i]; } return b; } /** * Back substitution for solution of ax = b where a is a matrix and b is a vector. * * @param {number[][]} a * @param {number[]} b * @param {number} n * @returns {number[]} */ function backSubVector (a, b, n) { for (let i = n - 1; i > -1; i--) { let v = b[i]; for (let j = i + 1; j < n; j++) { v -= a[i][j] * b[j]; } b[i] = v / a[i][i]; } return b; } /** * Invert a NxN matrix. * * @param {number[][]} matrix * @returns {number[][]} */ export function inv (matrix) { // Calculate the LU decomposition. const [p, l, u] = lu(matrix); const n = l.length; // Floating point math can produce very small, non-zero determinants for singular matrices. // This seems to happen in Numpy as well. // Don't bother calculating sign as we only care about how close to zero we are. if (l.map((row, i) => row[i] * u[i][i]).reduce((acc, val) => acc * val, 1) === 0.0) { throw new Error("Matrix is singular"); } // Solve for the identity matrix (will give us inverse) // Permutation matrix is the identity matrix, even if shuffled. return backSubMatrix(u, forwardSubMatrix(l, p, n), n); } /** * Solve a NxN matrix representing a system of equations. * * @param {number[][]} matrix * @param {number[]} vector * @returns {number[]} */ export function solve (matrix, vector) { // Calculate the LU decomposition. const [p, l, u] = lu(matrix, { pIndices: true }); const n = l.length; // If determinant is zero, we can't solve. if (l.map((row, i) => row[i] * u[i][i]).reduce((acc, val) => acc * val, 1) === 0.0) { throw new Error("Matrix is singular"); } return backSubVector( u, forwardSubVector( l, p.map(i => vector[i]), n, ), n, ); }