Source: syngen/utility.js

/**
 * A collection of useful utility methods.
 * @namespace
 */
syngen.utility = {}

/**
 * Adds a musical `interval` to a `frequency`, in Hertz.
 * @param {Number} frequency
 * @param {Number} interval
 *   Each integer multiple represents an octave.
 *   For example, `2` raises the frequency by two octaves.
 *   Likewise, `-1/12` lowers by one half-step, whereas `-1/100` lowers by one cent.
 * @static
 */
syngen.utility.addInterval = (frequency, interval) => frequency * (2 ** interval)

/**
 * Returns whether `value` is between `min` and `max` (inclusive).
 * @param {Number} value
 * @param {Number} min
 * @param {Number} max
 * @returns {Boolean}
 * @static
 */
syngen.utility.between = (value, min, max) => value >= min && value <= max

/**
 * Calculates the geometric center of variadic vectors or vector-likes.
 * @param {syngen.utility.vector3d[]|syngen.utility.vector2d[]|Object[]} vectors
 * @returns {syngen.utility.vector3d}
 * @static
 */
syngen.utility.centroid = (vectors = []) => {
  // NOTE: Returns origin if empty set
  if (!vectors.length) {
    return syngen.utility.vector3d.create()
  }

  let xSum = 0,
    ySum = 0,
    zSum = 0

  for (const vector of vectors) {
    xSum += vector.x || 0
    ySum += vector.y || 0
    zSum += vector.z || 0
  }

  return syngen.utility.vector3d.create({
    x: xSum / vectors.length,
    y: ySum / vectors.length,
    z: zSum / vectors.length,
  })
}

/**
 * Returns the element of `options` at the index determined by percentage `value`.
 * @param {Array} options
 * @param {Number} [value=0]
 *   Float within `[0, 1]`.
 * @returns {*}
 * @static
 */
syngen.utility.choose = (options = [], value = 0) => {
  value = syngen.utility.clamp(value, 0, 1)

  const index = Math.round(value * (options.length - 1))
  return options[index]
}

/**
 * Splices and returns the element of `options` at the index determined by percentage `value`.
 * Beward that this mutates the passed array.
 * @param {Array} options
 * @param {Number} [value=0]
 *   Float within `[0, 1]`.
 * @returns {*}
 * @static
 */
syngen.utility.chooseSplice = (options = [], value = 0) => {
  value = syngen.utility.clamp(value, 0, 1)

  const index = Math.round(value * (options.length - 1))
  return options.splice(index, 1)[0]
}

/**
 * Returns the element of `options` at the index determined by weighted percentage `value`.
 * @param {Array} options
 *   Each element is expected to have a `weight` key which is a positive number.
 *   Higher weights are more likely to be chosen.
 *   Beware that elements are not sorted by weight before selection.
 * @param {Number} [value=0]
 *   Float within `[0, 1]`.
 * @returns {*}
 * @static
 */
syngen.utility.chooseWeighted = (options = [], value = 0) => {
  // SEE: https://medium.com/@peterkellyonline/weighted-random-selection-3ff222917eb6
  value = syngen.utility.clamp(value, 0, 1)

  const totalWeight = options.reduce((total, option) => {
    return total + (option.weight || 0)
  }, 0)

  let weight = value * totalWeight

  for (const option of options) {
    weight -= option.weight || 0

    if (weight <= 0) {
      return option
    }
  }
}

/**
 * Returns `value` clamped between `min` and `max`.
 * @param {Number} value
 * @param {Number} min
 * @param {Number} max
 * @returns {Number}
 * @static
 */
syngen.utility.clamp = (value, min, max) => {
  if (value > max) {
    return max
  }

  if (value < min) {
    return min
  }

  return value
}

/**
 * Returns whichever value, `a` or `b`, that is closer to `x`.
 * @param {Number} x
 * @param {Number} a
 * @param {Number} b
 * @returns {Number}
 * @static
 */
syngen.utility.closer = (x, a, b) => {
  return Math.abs(x - a) <= Math.abs(x - b) ? a : b
}

/**
 * Returns the closest value to `x` in the array `values`.
 * @param {Number} x
 * @param {Number[]} values
 * @returns {Number}
 * @static
 * @todo Improve performance with a version for pre-sorted arrays
 */
syngen.utility.closest = function (x, values = []) {
  return values.reduce((closest, value) => syngen.utility.closer(x, closest, value))
}

/**
 * Instantiates `octaves` noise generators of `type` with `seed` and returns a wrapper object that calculates their combined values.
 * @param {Object} options
 * @param {Number} [options.octaves=2]
 * @param {*} options.seed
 * @param {syngen.utility.perlin1d|syngen.utility.perlin2d|syngen.utility.perlin3d|syngen.utility.perlin4d|syngen.utility.simplex2d|syngen.utility.simplex3d|syngen.utility.simplex4d} options.type
 *   Must be reference to a noise utility, and not a factory method or an instance.
 * @returns {Object}
 * @static
 * @todo Port into individual noise utilities for clarity and simplicity
 */
syngen.utility.createNoiseWithOctaves = ({
  seed,
  octaves = 0,
  type,
} = {}) => {
  if (!type || !type.create || !type.prototype || !type.prototype.reset || !type.prototype.value) {
    throw new Error('Incorrect type. Please pass a noise utility by reference, e.g. syngen.utility.simplex4d.')
  }

  octaves = Math.round(octaves)

  if (octaves < 2) {
    return type.create(seed)
  }

  const compensation = 1 / (1 - (2 ** -octaves)),
    layers = []

  if (!Array.isArray(seed)) {
    seed = [seed]
  }

  for (let i = 0; i < octaves; i += 1) {
    layers.push(
      type.create(...seed, 'octave', i)
    )
  }

  return {
    layer: layers,
    reset: function () {
      for (let layer of this.layer) {
        layer.reset()
      }
      return this
    },
    value: function (...args) {
      let amplitude = 1/2,
        frequency = 1,
        sum = 0

      for (let layer of this.layer) {
        // XXX: Assumes up to four arguments (4D noise) for optimal performance
        sum += layer.value(args[0] * frequency, args[1] * frequency, args[2] * frequency, args[3] * frequency) * amplitude
        amplitude /= 2
        frequency *= 2
      }

      sum *= compensation

      return sum
    },
  }
}

/**
 * Instantiates `octaves` noise generators of `type` with `seed` and returns a wrapper object that calculates their combined values.
 * @deprecated Replaced with {@link syngen.utility.createNoiseWithOctaves}.
 * @param {syngen.utility.perlin1d|syngen.utility.perlin2d|syngen.utility.perlin3d|syngen.utility.perlin4d|syngen.utility.simplex2d|syngen.utility.simplex3d|syngen.utility.simplex4d} type
 *   Must be reference to a noise utility, and not a factory method or an instance.
 * @param {*} seed
 * @param {Number} [octaves=2]
 * @returns {Object}
 * @static
 */
syngen.utility.createPerlinWithOctaves = (type, seed, octaves) => {
  const generator = syngen.utility.createNoiseWithOctaves({
    octaves,
    seed,
    type,
  })

  generator.perlins = generator.layers

  return generator
}

/**
 * Converts `degrees` to radians.
 * @param {Number} degrees
 * @returns {Number}
 * @static
 */
syngen.utility.degreesToRadians = (degrees) => degrees * Math.PI / 180

/**
 * Adds a musical interval to `frequency` in `cents`.
 * @param {Number} frequency
 * @param {Number} [cents=0]
 *   Every 1200 represents an octave.
 *   For example, `2400` raises the frequency by two octaves.
 *   Likewise, `-100` lowers by one half-step, whereas `-1` lowers by one cent.
 * @returns {Number}
 * @static
 */
syngen.utility.detune = (frequency, cents = 0) => frequency * (2 ** (cents / 1200))

/**
 * Calculates the distance between two vectors or vector-likes.
 * @param {syngen.utility.vector2d|syngen.utility.vector3d|Object} a
 * @param {syngen.utility.vector2d|syngen.utility.vector3d|Object} b
 * @returns {Number}
 * @static
 */
syngen.utility.distance = (a, b) => Math.sqrt(syngen.utility.distance2(a, b))

/**
 * Calculates the squared distance between two vectors or vector-likes.
 * @param {syngen.utility.vector2d|syngen.utility.vector3d|Object} a
 * @param {syngen.utility.vector2d|syngen.utility.vector3d|Object} b
 * @returns {Number}
 * @static
 */
syngen.utility.distance2 = ({
  x: x1 = 0,
  y: y1 = 0,
  z: z1 = 0,
} = {}, {
  x: x2 = 0,
  y: y2 = 0,
  z: z2 = 0,
} = {}) => ((x2 - x1) ** 2) + ((y2 - y1) ** 2) + ((z2 - z1) ** 2)

/**
 * Calculated the gain for a sound source `distance` meters away, normalized to zero decibels.
 * The distance model is determined by the values of several constants.
 * Importantly, it is a combination of inverse-squared and linear functions.
 * @param {Number} [distance=0]
 * @returns {Number}
 * @see syngen.const.distancePower
 * @see syngen.const.distancePowerHorizon
 * @see syngen.const.distancePowerHorizonExponent
 * @see syngen.streamer.getRadius
 * @static
 * @todo Move to dedicated distance models
 */
syngen.utility.distanceToPower = (distance = 0) => {
  // XXX: One is added so all distances yield sensible values
  distance = Math.max(1, distance + 1)

  const distancePower = distance ** -syngen.const.distancePower
  let horizonPower = 1

  if (syngen.const.distancePowerHorizon) {
    // XXX: One is added because of above
    const distancePowerHorizon = syngen.streamer.getRadius() + 1
    horizonPower = Math.max(0, distancePowerHorizon - distance) / distancePowerHorizon
    horizonPower **= syngen.const.distancePowerHorizonExponent
  }

  return distancePower * horizonPower
}

/**
 * Converts `frequency`, in Hertz, to its corresponding MIDI note number.
 * The returned value is not rounded.
 * @param {Number} frequency
 * @returns {Number}
 * @see syngen.const.midiReferenceFrequency
 * @see syngen.const.midiReferenceNote
 * @static
 */
syngen.utility.frequencyToMidi = (frequency) => (Math.log2(frequency / syngen.const.midiReferenceFrequency) * 12) + syngen.const.midiReferenceNote

/**
 * Converts `decibels` to its equivalent gain value.
 * @param {Number} decibels
 * @returns {Number}
 * @static
 */
syngen.utility.fromDb = (decibels) => 10 ** (decibels / 10)

/**
 * Converts `value` to an integer via the Jenkins hash function.
 * @param {String} value
 * @returns {Number}
 * @static
 */
syngen.utility.hash = (value) => {
  value = String(value)

  let hash = 0,
    i = value.length

  while (i--) {
    hash += value.charCodeAt(i)
    hash += hash << 10
    hash ^= hash >> 6
  }

  hash += (hash << 3)
  hash ^= (hash >> 11)
  hash += (hash << 15)

	return Math.abs(hash)
}

/**
 * Adds a random value to `baseValue` within the range of negative to positive `amount`.
 * @param {Number} baseValue
 * @param {Number} amount
 * @returns {Number}
 * @static
 */
syngen.utility.humanize = (baseValue = 1, amount = 0) => {
  return baseValue + syngen.utility.random.float(-amount, amount)
}

/**
 * Adds a random gain to `baseGain` within the range of negative to positive `decibels`, first converted to gain.
 * @param {Number} baseGain
 * @param {Number} decibels
 * @returns {Number}
 * @static
 */
syngen.utility.humanizeDb = (baseGain = 1, decibels = 0) => {
  const amount = syngen.utility.fromDb(decibels)
  return baseGain * syngen.utility.random.float(1 - amount, 1 + amount)
}

/**
 * Returns whether rectangular prisms `a` and `b` intersect.
 * A rectangular prism has a bottom-left vertex with coordinates `(x, y, z)` and `width`, `height`, and `depth` along those axes respectively.
 * An intersection occurs if their faces intersect, they share vertices, or one is contained within the other.
 * This function works for one- and two-dimensional shapes as well.
 * @param {Object} a
 * @param {Object} b
 * @returns {Boolean}
 * @static
 * @todo Define a rectangular prism utility or type
 */
syngen.utility.intersects = ({
  depth: depth1 = 0,
  height: height1 = 0,
  width: width1 = 0,
  x: x1 = 0,
  y: y1 = 0,
  z: z1 = 0,
} = {}, {
  depth: depth2 = 0,
  height: height2 = 0,
  width: width2 = 0,
  x: x2 = 0,
  y: y2 = 0,
  z: z2 = 0,
} = {}) => {
  const between = syngen.utility.between

  const xOverlap = between(x1, x2, x2 + width2)
    || between(x2, x1, x1 + width1)

  const yOverlap = between(y1, y2, y2 + height2)
    || between(y2, y1, y1 + height1)

  const zOverlap = between(z1, z2, z2 + depth2)
    || between(z2, z1, z1 + depth1)

  return xOverlap && yOverlap && zOverlap
}

/**
 * Linearly interpolates between `min` and `max` with `value`.
 * @param {Number} min
 * @param {Number} max
 * @param {Number} [value=0]
 *   Float within `[0, 1]`.
 * @returns {Number}
 * @static
 */
syngen.utility.lerp = (min, max, value = 0) => (min * (1 - value)) + (max * value)

/**
 * Linearly interpolates between `min` and `max` with `value` raised to `power`.
 * @param {Number} min
 * @param {Number} max
 * @param {Number} [value=0]
 *   Float within `[0, 1]`.
 * @param {Number} [power=2]
 * @returns {Number}
 * @static
 */
syngen.utility.lerpExp = (min, max, value = 0, power = 2) => {
  return syngen.utility.lerp(min, max, value ** power)
}

/**
 * Returns a random value within the range where the lower bound is the interpolated value within `[lowMin, highMin]`, the upper bound is the interpolated value within `[lowMax, highMax]`.
 * Values are interpolated with {@link syngen.utility.lerpExpRandom|lerpExpRandom}.
 * @param {Number[]} lowRange
 *   Expects `[lowMin, lowMax]`.
 * @param {Number[]} highRange
 *   Expects `[highMin, highMax]`.
 * @param {Number} [value]
 * @param {Number} [power]
 * @returns {Number}
 * @see syngen.utility.lerpExp
 * @static
 */
syngen.utility.lerpExpRandom = ([lowMin, lowMax], [highMin, highMax], value, power) => {
  return syngen.utility.random.float(
    syngen.utility.lerpExp(lowMin, highMin, value, power),
    syngen.utility.lerpExp(lowMax, highMax, value, power),
  )
}

/**
 * Linearly interpolates between `min` and `max` with `value` logarithmically with `base`.
 * @param {Number} min
 * @param {Number} max
 * @param {Number} [value=0]
 *   Float within `[0, 1]`.
 * @param {Number} [base=2]
 * @returns {Number}
 * @static
 */
syngen.utility.lerpLog = (min, max, value = 0, base = 2) => {
  value *= base - 1
  return syngen.utility.lerp(min, max, Math.log(1 + value) / Math.log(base))
}

/**
 * Linearly interpolates between `min` and `max` with `value` logarithmically with `base`.
 * This function is shorthand for `{@link syngen.utility.lerpLog|lerpLog}(min, max, 1 - value, 1 / base)` which results in curve that inversely favors larger values.
 * This is similar to but distinct from {@link syngen.utility.lerpExp|lerpExp}.
 * @param {Number} min
 * @param {Number} max
 * @param {Number} [value=0]
 *   Float within `[0, 1]`.
 * @param {Number} [base=2]
 * @returns {Number}
 * @see syngen.utility.lerpLog
 * @static
 */
syngen.utility.lerpLogi = (min, max, value, base) => {
  return syngen.utility.lerpLog(max, min, 1 - value, base)
}

/**
 * Returns a random value within the range where the lower bound is the interpolated value within `[lowMin, highMin]`, the upper bound is the interpolated value within `[lowMax, highMax]`.
 * Values are interpolated with {@link syngen.utility.lerpLogi|lerpLogi}.
 * @param {Number[]} lowRange
 *   Expects `[lowMin, lowMax]`.
 * @param {Number[]} highRange
 *   Expects `[highMin, highMax]`.
 * @param {Number} [value]
 * @param {Number} [power]
 * @returns {Number}
 * @see syngen.utility.lerpLogi
 * @static
 */
syngen.utility.lerpLogiRandom = ([lowMin, lowMax], [highMin, highMax], value) => {
  return syngen.utility.random.float(
    syngen.utility.lerpLogi(lowMin, highMin, value),
    syngen.utility.lerpLogi(lowMax, highMax, value),
  )
}

/**
 * Returns a random value within the range where the lower bound is the interpolated value within `[lowMin, highMin]`, the upper bound is the interpolated value within `[lowMax, highMax]`.
 * Values are interpolated with {@link syngen.utility.lerpLog|lerpLog}.
 * @param {Number[]} lowRange
 *   Expects `[lowMin, lowMax]`.
 * @param {Number[]} highRange
 *   Expects `[highMin, highMax]`.
 * @param {Number} [value]
 * @param {Number} [base]
 * @returns {Number}
 * @see syngen.utility.lerpLog
 * @static
 */
syngen.utility.lerpLogRandom = ([lowMin, lowMax], [highMin, highMax], value, base) => {
  return syngen.utility.random.float(
    syngen.utility.lerpLog(lowMin, highMin, value, base),
    syngen.utility.lerpLog(lowMax, highMax, value, base),
  )
}

/**
 * Returns a random value within the range where the lower bound is the interpolated value within `[lowMin, highMin]`, the upper bound is the interpolated value within `[lowMax, highMax]`.
 * Values are interpolated with {@link syngen.utility.lerp|lerp}.
 * @param {Number[]} lowRange
 *   Expects `[lowMin, lowMax]`.
 * @param {Number[]} highRange
 *   Expects `[highMin, highMax]`.
 * @param {Number} [value]
 * @param {Number} [base]
 * @returns {Number}
 * @see syngen.utility.lerp
 * @static
 */
syngen.utility.lerpRandom = ([lowMin, lowMax], [highMin, highMax], value) => {
  return syngen.utility.random.float(
    syngen.utility.lerp(lowMin, highMin, value),
    syngen.utility.lerp(lowMax, highMax, value),
  )
}

/**
 * Converts a MIDI `note` number to its frequency, in Hertz.
 * @param {Number} note
 * @returns {Number}
 * @see syngen.const.midiReferenceFrequency
 * @see syngen.const.midiReferenceNote
 * @static
 */
syngen.utility.midiToFrequency = (note) => {
  return syngen.const.midiReferenceFrequency * Math.pow(2, (note - syngen.const.midiReferenceNote) / 12)
}

/**
 * Normalizes `angle` within therange of `[0, 2π]`.
 * @param {Number} angle
 * @returns {Number}
 * @static
 */
syngen.utility.normalizeAngle = (angle = 0) => {
  const tau = Math.PI * 2

  if (angle > tau) {
    angle %= tau
  } else if (angle < 0) {
    angle %= tau
    angle += tau
  }

  return angle
}

/**
 * Normalizes `angle` within the range of `[-π, +π]`.
 * @param {Number} angle
 * @returns {Number}
 * @static
 */
syngen.utility.normalizeAngleSigned = (angle) => {
  const tau = 2 * Math.PI

  angle %= tau

  if (angle > Math.PI) {
    angle -= tau
  }

  if (angle < -Math.PI) {
    angle += tau
  }

  return angle
}

/**
 * Calculates the real solutions to the quadratic equation with coefficients `a`, `b`, and `c`.
 * @param {Number} a
 * @param {Number} b
 * @param {Number} c
 * @returns {Number[]}
 *   Typically there are two real solutions; however, implementations must check for imaginary solutions with {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isNaN|isNaN}.
 * @static
 */
syngen.utility.quadratic = (a, b, c) => {
  return [
    (-1 * b + Math.sqrt(Math.pow(b, 2) - (4 * a * c))) / (2 * a),
    (-1 * b - Math.sqrt(Math.pow(b, 2) - (4 * a * c))) / (2 * a),
  ]
}

/**
 * Converts `radians` to degrees.
 * @param {Number} radians
 * @returns {Number}
 * @static
 */
syngen.utility.radiansToDegrees = (radians) => radians * 180 / Math.PI

/**
 * Calculates the interior angle of a regular polygon with `sides`.
 * @param {Number} sides
 * @returns {Number}
 * @static
 */
syngen.utility.regularPolygonInteriorAngle = (sides) => (sides - 2) * Math.PI / sides

/**
 * Rounds `value` to `precision` places.
 * Beward that `precision` is an inverse power of ten.
 * For example, `3` rounds to the nearest thousandth, whereas `-3` rounds to the nearest thousand.
 * @param {Number} value
 * @param {Number} precision
 * @returns {Number}
 * @static
 */
syngen.utility.round = (value, precision = 0) => {
  precision = 10 ** precision
  return Math.round(value * precision) / precision
}

/**
 * Scales `value` within the range `[min, max]` to an equivalent value between `[a, b]`.
 * @param {Number} value
 * @param {Number} min
 * @param {Number} max
 * @param {Number} a
 * @param {Number} b
 * @returns {Number}
 * @static
 */
syngen.utility.scale = (value, min, max, a, b) => ((b - a) * (value - min) / (max - min)) + a

/**
 * Returns a shuffled shallow copy of `array` using `random` algorithm.
 * For example, implementations could leverage {@link syngen.utility.srand|srand()} to produce the same results each time given the same seed value.
 * @param {Array} array
 * @param {Function} [random=Math.random]
 * @static
 */
syngen.utility.shuffle = (array, random = Math.random) => {
  array = [].slice.call(array)

  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]]
  }

  return array
}

/**
 * Returns the sign of `value` as positive or negative `1`.
 * @param {Number} value
 * @returns {Number}
 * @static
 */
syngen.utility.sign = (value) => value >= 0 ? 1 : -1

/**
 * Returns a pseudo-random, linear congruential, seeded random number generator with variadic `seeds`.
 * Seeds are prepended with the global {@link syngen.seed} and concatenated with {@link syngen.const.seedSeparator}.
 * @param {...String} [...seeds]
 * @returns {syngen.utility.srandGenerator}
 * @static
 */
syngen.utility.srand = (...seeds) => {
  const increment = 1,
    modulus = 34359738337,
    multiplier = 185852,
    rotate = (seed) => ((seed * multiplier) + increment) % modulus

  let seed = syngen.utility.hash(
    syngen.seed.concat(...seeds)
  )

  seed = rotate(seed)

  /**
   * A pseudo-random, linear congruential, seeded random number generator that returns a value within `[min, max]`.
   * @param {Number} [min=0]
   * @param {Number} [max=1]
   * @returns {Number}
   * @type {Function}
   * @typedef syngen.utility.srandGenerator
   */
  const generator = (min = 0, max = 1) => {
    seed = rotate(seed)
    return min + ((seed / modulus) * (max - min))
  }

  return generator
}

/**
 * Calculates the musical interval between two frequencies, in cents.
 * @param {Number} a
 * @param {Number} b
 * @returns {Number}
 * @static
 */
syngen.utility.toCents = (a, b) => (b - a) / a * 1200

/**
 * Converts `gain` to its equivalent decibel value.
 * @param {Number} gain
 * @returns {Number}
 * @static
 */
syngen.utility.toDb = (gain) => 10 * Math.log10(gain)

/**
 * Scales `frequency` by integer multiples so it's an audible frequency within the sub-bass range.
 * @param {Number} frequency
 * @param {Number} [subFrequency={@link syngen.const.subFrequency}]
 * @param {Number} [minFrequency={@link syngen.const.minFrequency}]
 * @returns {Number}
 * @static
 */
syngen.utility.toSubFrequency = (frequency, subFrequency = syngen.const.subFrequency, minFrequency = syngen.const.minFrequency) => {
  while (frequency > subFrequency) {
    frequency /= 2
  }

  while (frequency < minFrequency) {
    frequency *= 2
  }

  return frequency
}

/**
 * Generates a universally unique identifier.
 * @returns {String}
 * @static
 */
syngen.utility.uuid = () => {
  // SEE: https://stackoverflow.com/a/2117523
  return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
    (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
  )
}

/**
 * Wraps `value` around the range `[min, max)` with modular arithmetic.
 * Beware that `min` is congruent to `max`, so returned values will approach the limit of `max` before wrapping back to `min`.
 * A way to visualize this operation is that the range repeats along the number line.
 * @param {Number} value
 * @param {Number} [min=0]
 * @param {Number} [max=1]
 * @returns {Number}
 * @static
 */
syngen.utility.wrap = (value, min = 0, max = 1) => {
  const range = max - min

  if (value >= max) {
    return min + ((value - min) % range)
  }

  if (value < min) {
    return min + ((value + max) % range)
  }

  return value
}

/**
 * Maps `value` to an alternating oscillation of the range `[min, max]`.
 * A way to visualize this operation is that the range repeats alternately along the number line, such that `min` goes to `max` back to `min`.
 * @param {Number} value
 * @param {Number} [min=0]
 * @param {Number} [max=1]
 * @returns {Number}
 * @static
 */
syngen.utility.wrapAlternate = (value, min = 0, max = 1) => {
  const range = max - min
  const period = range * 2

  if (value > max) {
    value -= min

    if (value % period < range) {
      return min + (value % range)
    }

    return max - (value % range)
  }

  if (value < min) {
    if (Math.abs(value % period) < range) {
      return max - range + Math.abs(value % range)
    }

    return min + range - Math.abs(value % range)
  }

  return value
}