UNPKG

total-serialism

Version:

A set of methods for the generation and transformation of number sequences useful in algorithmic composition

540 lines (499 loc) 15.9 kB
//==================================================================== // utility.js // part of 'total-serialism' Package // by Timo Hoogland (@t.mo / @tmhglnd), www.timohoogland.com // MIT License // // Utility functions //==================================================================== const chart = require('asciichart'); const HALF_PI = Math.PI / 2.0; const TWO_PI = Math.PI * 2.0; const PI = Math.PI; exports.HALF_PI = HALF_PI; exports.TWO_PI = TWO_PI; exports.PI = PI; // check if the value is an array or not // if not transform into an array and output // // @param {Value} -> input to be checked // @return {Array} -> the input as an array // function toArray(a){ return Array.isArray(a) ? a : [a]; } exports.toArray = toArray; // check if the value is an array or not // if it is an array output the first value // // @param {Value} -> intput to be checked // @param {Int+} -> index to return from Array (optional, default=0) // @return {Value} -> single value output // function fromArray(a, i=0){ return Array.isArray(a) ? a[i] : a; } exports.fromArray = fromArray; // Return the length/size of an array if the argument is an array // if argument is a number return the number as integer // if argument is not a number return 1 // The method can be used to input arrays as arguments for other functions // // @param {Value/Array} -> input value to check // @return {Int} -> the array length // function length(a){ if (Array.isArray(a)){ // return array length if argument is array return a.length; } // else return 1 if NaN or positive integer if Number return isNaN(a) ? 1 : Math.max(1, Math.floor(a)); } exports.length = length; exports.size = length; // Wrap a value between a low and high range // Similar to mod, expect the low range is also adjustable // // @param {Number/Array} -> input value // @param {Number} -> minimum value optional, (default=12) // @param {Number} -> maximum value optional, (default=0) // @return {Number} -> remainder after division // function wrap(a=0, lo=12, hi=0){ // swap if lo > hi if (lo > hi){ var t=lo, lo=hi, hi=t; } // calculate range and wrap the values if (!Array.isArray(a)){ return _wrap(a, lo, hi); } return a.map(x => wrap(x, lo, hi)); } exports.wrap = wrap; function _wrap(a, lo, hi){ let r = hi - lo; return ((((a - lo) % r) + r) % r) + lo; } // Constrain a value between a low and high range // // @param {Number/Array} -> number to constrain // @param {Number} -> minimum value (optional, default=12) // @param {Number} -> maximum value (optional, default=0) // @return {Number} -> constrained value // function constrain(a=0, lo=12, hi=0){ // swap if lo > hi if (lo > hi){ var t=lo, lo=hi, hi=t; } // constrain the values if (!Array.isArray(a)){ return Math.min(hi, Math.max(lo, a)); } return a.map(x => constrain(x, lo, hi)); } exports.constrain = constrain; exports.bound = constrain; exports.clip = constrain; exports.clamp = constrain; // Fold a between a low and high range // When the value exceeds the range it is folded inwards // Has the effect of "bouncing" against the boundaries // // @param {Number/Array} -> number to fold // @param {Number} -> minimum value (optional, default=12) // @param {Number} -> maximum value (optional, default=0) // @return {Number} -> folder value // function fold(a=0, lo=12, hi=0){ // swap if lo > hi if (lo > hi){ var t=lo, lo=hi, hi=t; } // fold the values if (!Array.isArray(a)){ return _fold(a, lo, hi); } return a.map(x => fold(x, lo, hi)); } exports.fold = fold; exports.bounce = fold; function _fold(a, lo, hi){ a = _map(a, lo, hi, -1, 1); a = Math.asin(Math.sin(a * HALF_PI)) / HALF_PI; return _map(a, -1, 1, lo, hi); } // Map/scale a value or array from one input-range // to a given output-range // // @param {Number/Array} -> value to be scaled // @param {Number} -> input low // @param {Number} -> input high // @param {Number} -> output low // @param {Number} -> output high // @param {Number} -> exponent (optional, default=1) // @return {Number/Array} // function map(a=0, ...params){ if (!Array.isArray(a)){ return _map(a, ...params); } return a.map(x => map(x, ...params)); } exports.map = map; exports.scale = map; function _map(a, inLo=0, inHi=1, outLo=0, outHi=1, exp=1){ a = (a - inLo) / (inHi - inLo); if (exp != 1){ var sign = (a >= 0.0) ? 1 : -1; a = Math.pow(Math.abs(a), exp) * sign; } return a * (outHi - outLo) + outLo; } // Lerp (Linear interpolation) two values or arrays // Both sides can be a single value or an array // Set the interpolation factor as third argument // // @param {Number/Array} -> input 1 to be mixed // @param {Number/Array} -> input 2 to be mixed // @param {Number} -> interpolation factor (optional, default=0.5) // @return {Number/Array} // function lerp(a=0, v=0, f=0.5){ return arrayCalc(a, v, (a, b) => { return a * (1 - f) + b * f }); } exports.lerp = lerp; exports.mix = lerp; // add 1 or more values to an array, // preserves listlength of first argument // arguments are applied sequentially // // @param {Number/Array} -> input to be added to // @param {Number/Array} -> value to add // @return {Number/Array} // function add(a=0, v=0){ return arrayCalc(a, v, (a, b) => { return a + b }); } exports.add = add; // subtract 1 or more values from an array // preserves listlength of first argument // arguments are applied sequentially // // @param {Number/Array} -> input to be subtracted from // @param {Number/Array} -> value to subtract // @return {Number/Array} // function subtract(a=0, v=0){ return arrayCalc(a, v, (a, b) => { return a - b }); } exports.subtract = subtract; exports.sub = subtract; // multiply 1 or more values from an array // preserves listlength of first argument // arguments are applied sequentially // // @param {Number/Array} -> input to be multiplied // @param {Number/Array} -> value to multiply with // @return {Number/Array} // function multiply(a=0, v=1){ return arrayCalc(a, v, (a, b) => { return a * b }); } exports.multiply = multiply; exports.mult = multiply; exports.mul = multiply; // divide 1 or more values from an array // preserves listlength of first argument // arguments are applied sequentially // // @param {Number/Array} -> input to be divided // @param {Number/Array} -> value to divide with // @return {Number/Array} // function divide(a=0, v=1){ return arrayCalc(a, v, (a, b) => { return a / b }); } exports.divide = divide; exports.div = divide; // Return the remainder after division // also works in the negative direction, so wrap starts at 0 // // @param {Int/Array} -> input value // @param {Int/Array} -> divisor (optional, default=12) // @return {Int/Array} -> remainder after division // function mod(a=0, v=12){ return arrayCalc(a, v, (a, b) => { return ((a % b) + b) % b }); } exports.mod = mod; // Raise a value of one array to the power of the value // from the right hand array // // @param {Number/Array} -> base // @param {Number/Array} -> exponent // @return {Number/Array} -> result from function // function pow(a=0, v=1){ return arrayCalc(a, v, (a, b) => { return Math.pow(a, b) }); } exports.pow = pow; // Return the squareroot of an array of values // // @param {Number/Array} -> values // @return {Number/Array} -> result // function sqrt(a=0){ return arrayCalc(a, 0, (a) => { return Math.sqrt(a) }); } exports.sqrt = sqrt; // Evaluate a function for a multi-dimensional array // // @params {Array|Number} -> left hand input array // @params {Array|Number} -> right hand input array // @params {Function} -> function to evaluate // @return {Array|Number} -> result of evaluation // function arrayCalc(a=0, v=0, func=()=>{return a;}){ // if righthand side is array if (Array.isArray(v)){ a = toArray(a); let l1 = a.length, l2 = v.length, r = []; let l = Math.max(l1, l2); for (let i=0; i<l; i++){ r[i] = arrayCalc(a[i % l1], v[i % l2], func); } return r; } // if both are single values if (!Array.isArray(a)){ let r = func(a, v); if (!isNaN(a) && !isNaN(v)){ return isNaN(r)? 0 : r; } return r; } // if lefthand side is array return a.map(x => arrayCalc(x, v, func)); } exports.arrayCalc = arrayCalc; // Call a list function with provided arguments // The difference is that first all the possible combinations of the arrays // are calculated allowing arrays as arguments to generate // multiple versions of the function and joining them together // function multiCall(func, ...a){ // calculate the array combinations let args = arrayCombinations(...a); // call the function for all the argument combinations args = args.map((a) => func(...a)); // combine into a single list but preserving multi-dimensional arrays let out = flatten(args, 1); return out; } exports.multiCall = multiCall; // Alternate through 2 or multiple lists consecutively // The output length is the lowest common denominator of the input lists // so that every combination of consecutive values is included // This function is used to allow arrays as input for Generators // And for the step function for algorithmic composition // // @param {Array0, Array1, ..., Array-n} -> arrays to interleave // @return {Array} -> outputs a 2D array of the results // function arrayCombinations(...arrs){ // make sure all items are an array of at least 1 item arrs = arrs.map(a => toArray(a)); // get the lengths, but remove duplicate lengths let sizes = unique(arrs.map(a => a.length)); // multiply to get total of possible iterations let iters = 1; sizes.forEach((l) => iters *= l); // iterate over the total amount pushing the items to array let arr = []; for (let i=0; i<iters; i++){ arr.push(arrs.map((e) => { return e[i % e.length] })); } return arr; } exports.arrayCombinations = arrayCombinations; // flatten a multidimensional array. Optionally set the depth // for the flattening // // @param {Array} -> array to flatten // @param {Number} -> depth of flatten // @return {Array} -> flattened array // function flatten(a=[0], depth=Infinity){ return toArray(a).flat(depth); } exports.flatten = flatten; exports.flat = flatten; // Truncate all the values in an array towards 0, // sometimes referred to as rounding down // // @param {Number/Array} -> input value // @return {Int/Array} -> trucated value function truncate(a=[0]){ if (!Array.isArray(a)){ return Math.trunc(a); } return a.map(x => truncate(x)); } exports.truncate = truncate; exports.trunc = truncate; exports.int = truncate; // Return the sum of all values in the array // Ignore all non numeric values // Works with multidimensional arrays by flattening first // // @param {Array} -> input array // @return {Number} -> summed array // function sum(a=[0]){ let s = 0; flatten(toArray(a)).forEach((v) => { s += isNaN(v) ? 0 : v; }); return s; } exports.sum = sum; // Return the biggest value from an array // // @param {NumberArray} -> input array // @return {Number} -> biggest value // function maximum(a=[0]){ if (!Array.isArray(a)) { return a; } return Math.max(...flatten(a)); } exports.maximum = maximum; exports.max = maximum; // Return the lowest value from an array // // @param {NumberArray} -> input array // @return {Number} -> lowest value // function minimum(a=[0]){ if (!Array.isArray(a)) { return a; } return Math.min(...flatten(a)); } exports.minimum = minimum; exports.min = minimum; // Normalize all the values in an array between 0. and 1. // The highest value will be 1, the lowest value will be 0. // // @param {Number/Array} -> input values // @return {Number/Array} -> normalized values // function normalize(a=[0]){ // get minimum and maximum let min = minimum(a); let range = maximum(a) - min; // if range 0 then range = min and min = 0 if (!range) { range = min, min = 0; } // normalize and return return divide(subtract(a, min), range); } exports.normalize = normalize; exports.norm = normalize; // Signed Normalize all the values in an array between -1. and 1. // The highest value will be 1, the lowest value will be -1. // // @param {Number/Array} -> input values // @return {Number/Array} -> signed normalized values // function signedNormalize(a=[0]){ return subtract(multiply(normalize(a), 2), 1); } exports.signedNormalize = signedNormalize; exports.snorm = signedNormalize; // filter duplicate items from an array // does not account for 2-dimensional arrays in the array // // @param {Array} -> array to filter // @return {Array} // function unique(a=[0]){ return [...new Set(toArray(a))]; } exports.unique = unique; // Plot an array of values to the console in the form of an // ascii chart and return chart from function. If you just want the // chart returned as text and not log to console set { log: false }. // Using the asciichart package by x84. // // @param {Number/Array/String} -> value to plot // @param {Object} -> { log: false } don't log to console and only return // -> { data: true } log the original array data // -> { decimals: 2 } adjust the number of decimals // -> { height: 10 } set a fixed chart line-height // -> other preferences for padding, colors, offset // See the asciichart documentation // function plot(a=[0], prefs){ // if a is not an Array a = toArray(a); // empty object if no preferences prefs = (typeof prefs !== 'undefined') ? prefs : {}; prefs.log = (typeof prefs.log !== 'undefined') ? prefs.log : true; prefs.data = (typeof prefs.data !== 'undefined') ? prefs.data : false; prefs.decimals = (typeof prefs.decimals !== 'undefined') ? prefs.decimals : 2; let p = chart.plot(a, prefs); if (prefs.data){ console.log('chart data: [', a.map(x => x.toFixed(prefs.decimals)).join(", "), "]\n"); } if (prefs.log){ console.log(chart.plot(a, prefs), "\n"); } return p; } exports.plot = plot; // Draw a 2D-array of values to the console in the form of an // ascii gray-scaleimage and return chart from function. // If you just want the chart returned as text and not log to console // set { log: false }. If you want to print using a characterset under // ascii-code 256 use { extend: false }. // // @param {Array/2D-Array} -> values to plot // @param {Object} -> { log: false } don't log to console and only return // -> { extend: true } use extended ascii characters // -> { error: false } use error character for error reporting // function draw(a=[0], prefs){ // if a is not an array a = toArray(a); // if a is not an 2d-array a = (Array.isArray(a[0])) ? a : [a]; // empty object if no preferences prefs = (typeof prefs !== 'undefined') ? prefs : {}; prefs.log = (typeof prefs.log !== 'undefined') ? prefs.log : true; prefs.extend = (typeof prefs.extend !== 'undefined') ? prefs.extend : true; prefs.error = (typeof prefs.error !== 'undefined') ? prefs.error : false; // when using extended ascii set let chars = (prefs.extend) ? ' ░▒▓█'.split('') : ' .-=+#'.split(''); // when flagging NaN values let err = (prefs.error) ? ((prefs.extend) ? '�' : '?') : ' '; // get the lowest and highest value from input and calculate range let min = Infinity, max = -Infinity; for (let i in a){ for (let j in a[i]){ min = (a[i][j] < min)? a[i][j] : min; max = (a[i][j] > max)? a[i][j] : max; } } let range = max - min; // lookup a grayscale ascii value based on normalized array value // use whitespace if value is NaN or 'X' if error flag is true let p = ''; for (let i in a){ for (let j in a[i]){ let grey = Math.trunc((a[i][j] - min) / range * (chars.length-1)); let char = (isNaN(grey)) ? err : chars[grey]; p += char; } // add linebreak if multiple lines must be printed if (a.length > 1) { p += '\n'; } } if (prefs.log){ console.log(p); } return p; } exports.draw = draw;