Home Reference Source Test

src/functions.js

import { RAD2DEG, DEG2RAD, TAU } from './const';

/**
 * Component-wise addition of two arrays/vectors.
 * If target is NOT specified, the first argument will
 * be mutated. Target will be overwritten and NOT included in the sum.
 * @param {number[]} to left operand
 * @param {number[]} from right operand
 * @param {number[]} target optional array/vector to store the result
 * @return {number[]} array/vector
 */
export function add(to, from, target = null) {
  if (target) {
    if (target.length === 0) target.length = to.length;
  } else {
    target = to;
  }
  for (let i = 0; i < target.length; i++) {
    target[i] = to[i] + from[i];
  }
  return target;
}

/**
 * Component-wise addition of one vector with a scaled version of another vector.
 * If target is NOT specified, the first argument will
 * be mutated. Target will be overwritten and NOT included in the sum.
 * @param {number[]} to left operand
 * @param {number[]} from right operand
 * @param {number} factor scaling factor to apply to from-vector
 * @param {number[]} target optional array/vector to store the result
 * @return {number[]} array/vector
 */
export function addScaled(to, from, factor, target = null) {
  if (target) {
    if (target.length === 0) target.length = to.length;
  } else {
    target = to;
  }
  for (let i = 0; i < target.length; i++) {
    target[i] = to[i] + from[i] * factor;
  }
  return target;
}

/**
 * Component-wise addition of a set of arrays/vectors.
 * If target is NOT specified, the first element in the set will
 * be mutated. Target (if it has valid values) WILL be included in the sum.
 * @param {number[][]} vectors Array of equally length arrays/vectors (to be added)
 * @param {number[]} target optional array/vector to add into
 * @return {number[]} array/vector
 */
export function addAll(vectors, target = null) {
  let start = 0;
  if (!target) {
    target = vectors[0];
    start++;
  }

  for (let i = start; i < vectors.length; i++) {
    for (let j = 0; j < vectors[i].length; j++) {
      if (i === 0 && !Number.isFinite(target[j])) {
        target[j] = vectors[i][j];
      } else {
        target[j] += vectors[i][j];
      }
    }
  }
  return target;
}

/**
 * Component-wise subtraction of two arrays/vectors.
 * If target is NOT specified, the first argument will
 * be mutated. Target will be overwritten and NOT included in the sum.
 * @param {number[]} from left operand
 * @param {number[]} vector right operand
 * @param {number[]} target optional array/vector to store the result
 * @return {number[]} array/vector
 */
export function sub(from, vector, target = null) {
  if (target) {
    if (target.length === 0) target.length = from.length;
  } else {
    target = from;
  }
  for (let i = 0; i < target.length; i++) {
    target[i] = from[i] - vector[i];
  }
  return target;
}

/**
 * Component-wise addition of one vector with a scaled version of another vector.
 * If target is NOT specified, the first argument will
 * be mutated. Target will be overwritten and NOT included in the sum.
 * @param {number[]} from left operand
 * @param {number[]} vector right operand
 * @param {number} factor scaling factor to apply to vector that will be subtracted
 * @param {number[]} target optional array/vector to store the result
 * @return {number[]} array/vector
 */
export function subScaled(from, vector, factor, target = null) {
  if (target) {
    if (target.length === 0) target.length = from.length;
  } else {
    target = from;
  }
  for (let i = 0; i < target.length; i++) {
    target[i] = from[i] - vector[i] * factor;
  }
  return target;
}

/**
 * Component-wise subtraction of array/vector and all elements in vectors.
 * If target is NOT specified, the first argument will
 * be mutated. Target will be overwritten and NOT included in the sum.
 * @param {number[]} from left operand
 * @param {number[][]} vectors right operand
 * @param {number[]} target optional array/vector to store the result
 * @return {number[]} array/vector
 */
export function subAll(from, vectors, target = null) {
  if (target) {
    if (target.length === 0) target.length = from.length;
  } else {
    target = from;
  }

  for (let i = 0; i < vectors.length; i++) {
    for (let j = 0; j < vectors[i].length; j++) {
      if (i === 0) {
        target[j] = from[j] - vectors[i][j];
      } else {
        target[j] -= vectors[i][j];
      }
    }
  }
  return target;
}

/**
 * Create a vector from two points. This function will NOT mutate any
 * arguments unless 'target' is the same as 'from'.
 * @param {number[]} from start coordinates
 * @param {number[]} to end coordinates
 * @param {number[]} target optional array/vector to store the resulting vector
 * @return {number[]} vector
 */
export function vec(from, to, target = null) {
  if (!target) {
    target = to.slice();
  } else {
    if (target.length === 0) {
      target.length = to.length;
    }
    for (let i = 0; i < target.length; i++) {
      target[i] = to[i];
    }
  }
  return sub(target, from);
}

/**
 * Component-wise scaling of an array or vector
 * @param {number[]} arr array/vector to scale
 * @param {number} factor scaling factor
 * @param {number[]} target optional array/vector to store the result
 * @return {number[]} scaled array/vector
 */
export function scale(arr, factor, target = null) {
  target = target || arr;
  for (let i = 0; i < arr.length; i++) {
    target[i] = arr[i] * factor;
  }
  return target;
}

/**
 * Computes the sum of squares
 * @param {number[]} arr array/vector to compute
 * @return {number}
 */
export function sumsqr(arr) {
  return arr.reduce((sum, v) => sum + v ** 2, 0);
}

/**
 * Computes the scalar value (length) of a vector
 * @param {number[]} vector array/vector to compute
 * @return {number}
 */
export function scalar(vector) {
  const sq = sumsqr(vector);
  if (sq === 0) return sq;
  return Math.sqrt(sq);
}

/**
 * Normalizes an array/vector
 * @param {number[]} vector array/vector to notmalize
 * @param {number[]} target optional array/vector to store the result
 * @return {number[]} normalized array/vector
 */
export function norm(vector, target = null) {
  target = target || vector;
  const sc = scalar(vector);
  const f = sc === 0 ? 0 : 1 / sc;
  return scale(vector, f, target);
}

/**
 * Describes relationships between two points.
 * @param {number[]} from start coordinates
 * @param {number[]} to end coordinates
 * @param {number[]} target optional array/vector to store the result
 * @return {{vector: number[], sqr: number, distance: number, unit: number[]}}
 */
export function descr(from, to, target = null) {
  if (!target) {
    target = to.slice();
  } else {
    if (target.length === 0) {
      target.length = to.length;
    }
    for (let i = 0; i < target.length; i++) {
      target[i] = to[i];
    }
  }
  const vector = sub(target, from);
  const sqr = sumsqr(vector);
  const dst = Math.sqrt(sqr);
  const unit = scale(vector, dst > 0 ? 1 / dst : 0, vector.slice());
  return {
    vector,
    sqr,
    dist: dst,
    unit,
  };
}

/**
 * Get a unit vector that is perpendicular to the input vector. Only for 2d vectors!
 * @param {number[]} vector 2d vector
 * @param {number[]} target optional array/vector to store the resulting vector
 * @return {number[]} normalized, perpendicular vector
 */
export function orth2(vector, target = null) {
  target = target || vector;
  const x = -vector[1];
  target[1] = vector[0];
  target[0] = x;
  return norm(target);
}

/**
 * Find the axis aligned angle of a 3d vector
 * @param {number[]} vector
 * @param {number} axis which axis to measure from X=0, Y=1, Z=2 (defaults to 0)
 * @return {number} angle in radians
 */
export function angle(vector, axis = 0) {
  if (axis > 2 || axis < 0) return undefined;

  const [x, y, z] = vector;
  let a, b, c;
  switch (axis) {
    case 0: a = y; b = z; c = x; break;
    case 1: a = x; b = z; c = y; break;
    default: a = x; b = y; c = z;
  }
  const l = Math.sqrt(a ** 2 + b ** 2);
  return Math.atan2(l, c);
}

/**
 * Find the axis aligned angle of a 2d vector
 * @param {number[]} vector
 * @return {number} angle in radians
 */
export function angle2(vector) {
  return Math.atan2(vector[1], vector[0]);
}

/**
 * Get a unit vector between two points/coordinates. This function
 * does not mutate any arguments, but a target may still be used to
 * control the return type or to avoid creating additional arrays.
 * @param {number[]} from start coordinates
 * @param {number[]} to end coordinates
 * @param {number[]} target optional array/vector to store the resulting vector
 * @return {number[]} unit vector between from and to
 */
export function dir(from, to, target = null) {
  return norm(vec(from, to, target));
}

/**
 * Calculate the distance between two points/coordinates.
 * @param {number[]} p1 point/coordinates
 * @param {number[]} p2 point/coordinates
 * @return {number} distance
 */
export function dist(p1, p2) {
  return scalar(vec(p1, p2));
}

/**
 * Calculate the dot product between two vectors
 * @param {number[]} v1 left hand operand
 * @param {number[]} v2 right hand operand
 * @return {number} the dot product
 */
export function dot(v1, v2) {
  return v1.reduce((sum, c, i) => sum + c * v2[i], 0);
}

/**
 * Find the cross product between two 3d vectors
 * @param {number[]} v1 left hand operand (3d vector)
 * @param {number[]} v2 right hand operand (3d vector)
 * @param {number[]} target optional array/vector to store the resulting vector
 * @return {number[]} the cross product vector (normal)
 */
export function cross(v1, v2, target = null) {
  target = target || new Array(3);
  target[0] = (v1[1] * v2[2]) - (v1[2] * v2[1]);
  target[1] = (v1[2] * v2[0]) - (v1[0] * v2[2]);
  target[2] = (v1[0] * v2[1]) - (v1[1] * v2[0]);

  return target;
}

/**
 * Get the triple product between three 3d vectors
 * @param {number[]} v1 left hand operand for the dot product (3d vector)
 * @param {number[]} v2 left hand operand for the cross product (3d vector)
 * @param {number[]} v3 right hand operand for the cross product (3d vector)
 * @return {number} triple product
 */
export function triple(v1, v2, v3) {
  return dot(v1, cross(v2, v3));
}

/**
 * Find the psudo cross product between two 2d vectors
 * @param {number[]} v1 left hand operand (2d vector)
 * @param {number[]} v2 right hand operand (2d vector)
 * @return {number} signed area of the parallellogram defined by v1 and v2
 */
export function cross2(v1, v2) {
  return (v1[0] * v2[1]) - (v1[1] * v2[0]);
}

/**
 * Clamps the value to min or max if value is less than min or greater than max
 * @param {number} value value to clamp
 * @param {number} min minimum value
 * @param {number} max maximum value
 * @return {number} clamped value
 */
export function clampValue(value, min = 0, max = 1) {
  if (value < min) return min;
  if (value > max) return max;
  return value;
}

/**
 * Clamps each value in array according to min and max.
 * @param {number[]} arr values to clamp
 * @param {number} min minimum value
 * @param {number} max maximum value
 * @param {number[]} target optional array/vector to store the result
 * @return {number[]} array of clamped values
 */
export function clampArray(arr, min = 0, max = 1, target = null) {
  target = target || arr;
  for (let i = 0; i < target.length; i++) {
    target[i] = clampValue(target[i], min, max);
  }
  return target;
}

/**
 * Clamps the argument according to min and max. Arg can be either a numeric
 * value or an array of numeric values.
 * @param {number|number[]} arg value or array of values to clamp
 * @param {number} min minimum value
 * @param {number} max maximum value
 * @param {number[]} target optional array/vector to store the result
 * @return {number|number[]} clamped version of arg
 */
export function clamp(arg, min = 0, max = 1, target = null) {
  if (Array.isArray(arg)) {
    return clampArray(arg, min, max, target);
  }
  return clampValue(arg, min, max);
}

/**
 * As glsl step function for single numeric values
 * @param {number} edge value to test
 * @param {number} x threshold value
 * @return {number} returns 0 or 1
 */
export function stepValue(edge, x) {
  return x >= edge ? 1 : 0;
}

/**
 * As glsl step function for multiple numeric values
 * @param {number[]} edges values to test
 * @param {number|number[]} x threshold values
 * @param {number[]} target optional array/vector to store the result
 * @return {number[]} results for each value in edges
 */
export function stepArray(edges, x, target = null) {
  const m = Array.isArray(x) ? i => x[i] : () => x;
  target = target || edges;
  for (let i = 0; i < target.length; i++) {
    target[i] = stepValue(target[i], m(i));
  }
  return target;
}

/**
 * Implementation of glsl step function. Returns 0 if an edge is less than the
 * threshold x, otherwise 1
 * @param {number|number[]} edge number/array to test
 * @param {number|number[]} x threshold(s)
 * @param {number[]} target optional array/vector to store the result (if edge is array)
 * @return {number|number[]}
 */
export function step(edge, x, target = null) {
  if (Array.isArray(edge)) {
    return stepArray(edge, x, target);
  }
  return stepValue(edge, x);
}

/**
 * Implementation of glsl smoothstep function
 * @param {number} edge0
 * @param {number} edge1
 * @param {number} x threshold
 */
export function smoothstep(edge0, edge1, x) {
  const t = clampValue((x - edge0) / (edge1 - edge0));
  return t * t * (3.0 - 2.0 * t);
}

/**
 * Linear interpolation between two numbers
 * @param {number} a interpolate from
 * @param {number} b interpolate to
 * @param {number} t time 0 = a, 1 = b
 * @return {number} the interpolated value
 */
export function lerp(a, b, t) {
  const m = clampValue(t, 0, 1);
  return a * (1 - m) + b * m;
}

/**
 * Mix (interpolate) numbers or arrays (similar to glsl implementation).
 * Works on both vectors and matrices, since they are both arrays.
 * @param {number[]} a interpolate from
 * @param {number[]} b interpolate to
 * @param {number|number[]} t time 0 = a, 1 = b
 * @param {number[]} target optional array/vector to store the result
 * @return {number[]} interpolated array/vector/matrix
 */
export function mix(a, b, t, target = null) {
  const m = Array.isArray(t) ? i => t[i] : () => t;
  target = target || a;
  for (let i = 0; i < target.length; i++) {
    target[i] = lerp(a[i], b[i], m(i));
  }
  return target;
}

/**
 * Generates a list of interpolated values between from and to,
 * where the number of elements returned are controlled by the
 * steps argument.
 * @param {number|number[]} from value to interpolate from
 * @param {number|number[]} to value to interpolate to
 * @param {number} steps interpolation steps
 * @param {number} start start time of interpolation [0-1]
 * @param {number} end end time of interpolation [0-1]
 */
export function seq(from, to, steps, start = 0, end = 1) {
  let f;
  if (Array.isArray(from)) {
    f = t => mix(from, to, t, from.slice());
  } else {
    f = t => lerp(from, to, t);
  }
  const target = [];
  const incr = (end - start) / (steps - 1);
  for (let i = 0; i < steps - 1; i++) {
    const x = start + i * incr;
    target.push(f(x));
  }
  target.push(f(end));
  return target;
}

/**
 * Generates a list of interpolated values between 0 and 1,
 * where the number of elements returned are controlled by the
 * steps argument.
 * @param {number} steps interpolation steps
 */
export function seqI(steps) {
  const target = [];
  const incr = 1 / (steps - 1);
  for (let i = 0; i < steps - 1; i++) {
    target.push(lerp(0, 1, i * incr));
  }
  target.push(1);
  return target;
}

/**
 * Rounds a number to the specific number of digits. Works with either a
 * single number or an array of numbers, which means it can be used with vectors and
 * matrices as well.
 * @param {number|number[]} v value to round
 * @param {number} digits number of digits to round to
 * @return {number} rounded value
 */
export function round(v, digits = 1) {
  const f = 10 ** digits;
  if (!Array.isArray(v)) {
    return Math.round(v * f) / f;
  }
  for (let i = 0; i < v.length; i++) {
    v[i] = Math.round(v[i] * f) / f;
  }
  return v;
}

/**
 * Convert degrees to radians
 * @param {number} d degrees
 * @returns {number} radians
 */
export function rad(d) {
  return d * DEG2RAD;
}

/**
 * Convert radians to degrees
 * @param {number} r radians
 * @returns {number} degrees
 */
export function deg(r) {
  return r * RAD2DEG;
}

/**
 * Normalise an angle to be between -PI to +PI
 * @param {number} r radians
 * @return {number} normalised angle
 */
export function nrad(r) {
  const v = r % TAU;
  return (v < 0 ? v + TAU : v);
}

/**
 * Test if a vector is a null vector
 * @param {number[]} v vector to test
 * @param {number} epsilon optional epsilon value
 */
export function isNullVec(v, epsilon = 0) {
  return epsilon ? v.every(val => Math.abs(val) - epsilon <= 0) : v.every(val => val === 0);
}