modeling/src/curves/bezier/create.js

/**
 * Represents a bezier easing function.
 * @typedef {Object} bezier
 * @property {Array} points - The control points for the bezier curve. The first and last point will also be the start and end of the curve
 * @property {string} pointType - A reference to the type and dimensionality of the points that the curve was created from
 * @property {number} dimensions - The dimensionality of the bezier
 * @property {Array} permutations - A pre-calculation of the bezier algorithm's co-efficients
 * @property {Array} tangentPermutations - A pre-calculation of the bezier algorithm's tangent co-efficients
 *
 */

/**
 * Creates an object representing a bezier easing curve.
 * Curves can have both an arbitrary number of control points, and an arbitrary number of dimensions.
 *
 * @example
 * const b = bezier.create([0,10]) // a linear progression from 0 to 10
 * const b = bezier.create([0, 0, 10, 10]) // a symmetrical cubic easing curve that starts slowly and ends slowly from 0 to 10
 * const b = bezier.create([0,0,0], [0,5,10], [10,0,-5], [10,10,10]]) // a cubic 3 dimensional easing curve that can generate position arrays for modelling
 * // Usage
 * let position = bezier.valueAt(t,b) // where 0 < t < 1
 * let tangent = bezier.tangentAt(t,b) // where 0 < t < 1
 *
 * @param {Array} points An array with at least 2 elements of either all numbers, or all arrays of numbers that are the same size.
 * @returns {bezier} a new bezier data object
 * @alias module:modeling/curves/bezier.create
 */
const create = (points) => {
  if (!Array.isArray(points)) throw new Error('Bezier points must be a valid array/')
  if (points.length < 2) throw new Error('Bezier points must contain at least 2 values.')
  const pointType = getPointType(points)

  return {
    points: points,
    pointType: pointType,
    dimensions: pointType === 'float_single' ? 0 : points[0].length,
    permutations: getPermutations(points.length - 1),
    tangentPermutations: getPermutations(points.length - 2)
  }
}

const getPointType = function (points) {
  let firstPointType = null
  points.forEach((point) => {
    let pType = ''
    if (Number.isFinite(point)) {
      pType = 'float_single'
    } else if (Array.isArray(point)) {
      point.forEach((val) => {
        if (!Number.isFinite(val)) throw new Error('Bezier point values must all be numbers.')
      })
      pType = 'float_' + point.length
    } else throw new Error('Bezier points must all be numbers or arrays of number.')
    if (firstPointType == null) {
      firstPointType = pType
    } else {
      if (firstPointType !== pType) {
        throw new Error('Bezier points must be either all numbers or all arrays of numbers of the same size.')
      }
    }
  })
  return firstPointType
}

const getPermutations = function (c) {
  const permutations = []
  for (let i = 0; i <= c; i++) {
    permutations.push(factorial(c) / (factorial(i) * factorial(c - i)))
  }
  return permutations
}

const factorial = function (b) {
  let out = 1
  for (let i = 2; i <= b; i++) {
    out *= i
  }
  return out
}

module.exports = create