ixfx
Version:
Bundle of ixfx libraries
1,658 lines (1,638 loc) • 84.2 kB
JavaScript
import { n as __exportAll } from "./chunk-CaR5F9JI.js";
import { C as numberTest, M as resultThrow, v as integerTest } from "./src-C_hvyftg.js";
import { n as zip, x as movingWindowWithContext } from "./src-CxEyGbiK.js";
//#region ../packages/numbers/src/apply-to-values.ts
/**
* Apples `fn` to every key of `obj` which is numeric.
* ```js
* const o = {
* name: 'john',
* x: 10,
* y: 20
* };
* const o2 = applyToValues(o, (v) => v * 2);
*
* // Yields: { name: 'john', x: 20, y: 40 }
* ```
* @param object
* @param apply
* @returns
*/
const applyToValues = (object, apply) => {
const o = { ...object };
for (const [key, value] of Object.entries(object)) if (typeof value === `number`) o[key] = apply(value);
else o[key] = value;
return o;
};
//#endregion
//#region ../packages/numbers/src/numeric-arrays.ts
/**
* Applies a function `fn` to the elements of an array, weighting them based on their relative position.
*
* ```js
* // Six items
* weight([1,1,1,1,1,1], Modulation.gaussian());
*
* // Yields:
* // [0.02, 0.244, 0.85, 0.85, 0.244, 0.02]
* ```
*
* `fn` is expected to map (0..1) => (0..1), such as an easing function. The input to the
* `fn` is the relative position of an element. Thus the first element will be 0, the middle 0.5 and so on.
* The output of `fn` is then multiplied by the original value.
*
* In the below example (which is also the default if `fn` is not specified), the relative position is
* how values are weighted:
*
* ```js
* weight([1,1,1,1,1,1], (relativePos) => relativePos);
* // Yields:
* // [0, 0.2, 0.4, 0.6, 0.8, 1]
* ```
*
* Throws TypeError if `data` is not an array or for any element not a number.
* @param data Array of numbers
* @param fn Returns a weighting based on the given relative position. If unspecified, `(x) => x` is used.
*/
const weight = (data, fn) => {
if (!Array.isArray(data)) throw new TypeError(`Param 'data' is expected to be an array. Got type: ${typeof data}`);
const weightingFunction = fn ?? ((x) => x);
return data.map((value, index) => {
if (typeof value !== `number`) throw new TypeError(`Param 'data' contains non-number at index: '${index}'. Type: '${typeof value}' value: '${value}'`);
const relativePos = index / (data.length - 1);
const weightForPosition = weightingFunction(relativePos);
if (typeof weightForPosition !== `number`) throw new TypeError(`Weighting function returned type '${typeof weightForPosition}' rather than number for input: '${relativePos}'`);
return value * weightForPosition;
});
};
/**
* Returns an array of all valid numbers from `data`
*
* @param data
* @returns
*/
const validNumbers = (data) => data.filter((d) => typeof d === `number` && !Number.isNaN(d));
/**
* Returns the dot product of arbitrary-sized arrays. Assumed they are of the same length.
* @param values
* @param nonNumber What to do if array contains an invalid number. Error: throw an exception, 'treat-as-zero' use as 0 instead, 'ignore', let math run with invalid number
* @returns
*/
const dotProduct = (values, nonNumber = `ignore`) => {
let r = 0;
const length = values[0].length;
for (let index = 0; index < length; index++) {
let t = 0;
for (const [p, value] of values.entries()) {
let v = value[index];
if (Number.isNaN(v) || !Number.isFinite(v)) {
if (nonNumber === `treat-as-zero`) v = 0;
else if (nonNumber === `error`) throw new TypeError(`Invalid number at index ${index},${p}`);
}
if (p === 0) t = v;
else t *= v;
}
r += t;
}
return r;
};
/**
* Calculates the average of all numbers in an array.
* Array items which aren't a valid number are ignored and do not factor into averaging.
*
* Use {@link numberArrayCompute} if you want min, max and total as well.
*
* @example
* ```js
* // Average of a list
* const avg = Numbers.average([1, 1.4, 0.9, 0.1]);
*
* // Average of a variable
* const data = [100,200];
* Numbers.average(data);
* ```
*
* @see {@link averageWeighted} To weight items based on position in array
* @param data Data to average.
* @returns Average of array
*/
const average = (data) => {
if (typeof data !== `object`) throw new Error(`Param 'data' should be an array. Got: ${typeof data}`);
if (!Array.isArray(data)) throw new TypeError(`Param 'data' is not an array`);
const valid = validNumbers(data);
return valid.reduce((accumulator, v) => accumulator + v, 0) / valid.length;
};
/**
* Returns the minimum number out of `data`.
* Undefined and non-numbers are silently ignored.
*
* ```js
* Numbers.min([10, 20, 0]); // Yields 0
* ```
* @param data
* @returns Minimum number
*/
const min = (data) => Math.min(...validNumbers(data));
/**
* Returns the index of the largest value.
* ```js
* const v = [ 10, 40, 5 ];
* Numbers.maxIndex(v); // Yields 1
* ```
* @param data Array of numbers
* @returns Index of largest value
*/
const maxIndex = (data) => data.reduce((bestIndex, value, index, array) => value > array[bestIndex] ? index : bestIndex, 0);
/**
* Returns the index of the smallest value.
*
* ```js
* const v = [ 10, 40, 5 ];
* Numbers.minIndex(v); // Yields 2
* ```
* @param data Array of numbers
* @returns Index of smallest value
*/
const minIndex = (data) => data.reduce((bestIndex, value, index, array) => value < array[bestIndex] ? index : bestIndex, 0);
/**
* Returns the maximum number out of `data`.
* Undefined and non-numbers are silently ignored.
*
* ```js
* Numbers.max(100, 200, 50); // 200
* ```
* @param data List of numbers
* @returns Maximum number
*/
const max = (data) => Math.max(...validNumbers(data));
/**
* Returns the total of `data`.
* Undefined and non-numbers are silently ignored.
*
* ```js
* Numbers.total([1, 2, 3]); // 6
* ```
* @param data Array of numbers
* @returns Total
*/
const total = (data) => data.reduce((previous, current) => {
if (typeof current !== `number`) return previous;
if (Number.isNaN(current)) return previous;
if (!Number.isFinite(current)) return previous;
return previous + current;
}, 0);
/**
* Returns the maximum out of `data` without pre-filtering for speed.
*
* For most uses, {@link max} should suffice.
*
* ```js
* Numbers.maxFast([ 10, 0, 4 ]); // 10
* ```
* @param data
* @returns Maximum
*/
const maxFast = (data) => {
let m = Number.MIN_SAFE_INTEGER;
for (const datum of data) m = Math.max(m, datum);
return m;
};
/**
* Returns the total of `data` without pre-filtering for speed.
*
* For most uses, {@link total} should suffice.
*
* ```js
* Numbers.totalFast([ 10, 0, 4 ]); // 14
* ```
* @param data
* @returns Maximum
*/
const totalFast = (data) => {
let m = 0;
for (const datum of data) m += datum;
return m;
};
/**
* Returns the maximum out of `data` without pre-filtering for speed.
*
* For most uses, {@link max} should suffice.
*
* ```js
* Numbers.minFast([ 10, 0, 100 ]); // 0
* ```
* @param data
* @returns Maximum
*/
const minFast = (data) => {
let m = Number.MAX_SAFE_INTEGER;
for (const datum of data) m = Math.min(m, datum);
return m;
};
//#endregion
//#region ../packages/numbers/src/average.ts
/**
* Calculate median value of an array of numbers
* @param data
* @returns
*/
const median = (data) => {
if (!Array.isArray(data)) throw new TypeError(`Param 'data' is expected to be an array`);
const n = data.length;
data.sort((a, b) => a - b);
let result = 0;
if (n % 2 === 0) result = Math.floor((data[n / 2] + data[n / 2 - 1]) / 2);
else result = data[Math.floor(n / 2)];
return result;
};
/**
* Calculate the mean of `array`.
* @param array
* @returns
*/
const mean = (array) => array.reduce((accumulator, value) => accumulator + value, 0) / array.length;
/**
* Computes an average of an array with a set of weights applied.
*
* Weights can be provided as an array, expected to be on 0..1 scale, with indexes
* matched up to input data. Ie. data at index 2 will be weighed by index 2 in the weightings array.
*
* ```js
* // All items weighted evenly
* averageWeighted([1,2,3], [1,1,1]); // 2
*
* // First item has full weight, second half, third quarter
* averageWeighted([1,2,3], [1, 0.5, 0.25]); // 1.57
*
* // With reversed weighting of [0.25,0.5,1] value is 2.42
* ```
*
* A function can alternatively be provided to compute the weighting based on array index, via {@link weight}.
*
* ```js
* averageWeighted[1,2,3], Random.gaussian()); // 2.0
* ```
*
* This is the same as:
*
* ```js
* const data = [ 1, 2, 3 ];
* const w = weight(data, Random.gaussian());
* const avg = averageWeighted(data, w); // 2.0
* ```
* @param data Data to average
* @param weightings Array of weightings that match up to data array, or an easing function
* @see {@link average} Compute averages without weighting.
*/
const averageWeighted = (data, weightings) => {
if (typeof weightings === `function`) weightings = weight(data, weightings);
const [totalV, totalW] = zip(data, weightings).reduce((accumulator, v) => [accumulator[0] + v[0] * v[1], accumulator[1] + v[1]], [0, 0]);
return totalV / totalW;
};
/**
* Returns a function that computes a weighted average of an array
*
* ```js
* const w = averageWeigher(v => Math.random() * v);
*
* // Give each array index a random
* w([1,2,3,4]);
* ```
* @param weigher
* @returns
*/
const averageWeigher = (weigher) => {
return (data) => averageWeighted(data, weigher);
};
//#endregion
//#region ../packages/numbers/src/clamp.ts
/**
* Clamps a value between min and max (both inclusive)
* Defaults to a 0-1 range, useful for percentages.
*
* @example Usage
* ```js
* // 0.5 - just fine, within default of 0 to 1
* clamp(0.5);
* // 1 - above default max of 1
* clamp(1.5);
* // 0 - below range
* clamp(-50, 0, 100);
* // 50 - within range
* clamp(50, 0, 50);
* ```
*
* For clamping integer ranges, consider {@link clampIndex }
* For clamping `{ x, y }` points, consider {@link https://api.ixfx.fun/_ixfx/geometry/Points/clamp/ @ixfx/geometry/Points.clamp}.
* For clamping bipolar values: {@link Bipolar.clamp}
* @param value Value to clamp
* @param min value (inclusive)
* @param max value (inclusive)
* @returns Clamped value
*/
function clamp$1(value, min = 0, max = 1) {
if (Number.isNaN(value)) throw new Error(`Param 'value' is NaN`);
if (Number.isNaN(min)) throw new Error(`Param 'min' is NaN`);
if (Number.isNaN(max)) throw new Error(`Param 'max' is NaN`);
if (value < min) return min;
if (value > max) return max;
return value;
}
/**
* Returns a function that clamps values.
*
* ```js
* const c = clamper(0,100);
* c(50); // 50
* c(101); // 100
* c(-5); // 0
* ```
* @param min Minimum value. Default: 0
* @param max Maximum value. Default: 1
*/
function clamper(min = 0, max = 1) {
if (Number.isNaN(min)) throw new Error(`Param 'min' is NaN`);
if (Number.isNaN(max)) throw new Error(`Param 'max' is NaN`);
return (v) => {
if (v > max) return max;
if (v < min) return min;
return v;
};
}
/**
* Clamps integer `v` between 0 (inclusive) and array length or length (exclusive).
* Returns value then will always be at least zero, and a valid array index.
*
* @example Usage
* ```js
* // Array of length 4
* const myArray = [`a`, `b`, `c`, `d`];
* clampIndex(0, myArray); // 0
* clampIndex(5, 3); // 2
* ```
*
* Throws an error if `v` is not an integer.
*
* For some data it makes sense that data might 'wrap around' if it exceeds the
* range. For example rotation angle. Consider using {@link wrap} for this.
*
* @param v Value to clamp (must be an interger)
* @param arrayOrLength Array, or length of bounds (must be an integer)
* @returns Clamped value, minimum will be 0, maximum will be one less than `length`.
*/
function clampIndex(v, arrayOrLength) {
if (!Number.isInteger(v)) throw new TypeError(`v parameter must be an integer (${v})`);
const length = Array.isArray(arrayOrLength) ? arrayOrLength.length : arrayOrLength;
if (!Number.isInteger(length)) throw new TypeError(`length parameter must be an integer (${length}, ${typeof length})`);
v = Math.round(v);
if (v < 0) return 0;
if (v >= length) return length - 1;
return v;
}
/**
* Returns the largest value, ignoring the sign of numbers
*
* ```js
* maxAbs(1, 5); // 5
* maxAbs(-10, 5); // -10 (since sign is ignored)
* maxAbs(arrayOfNumbers);
* ```
*
* Non-valid numbers are silently ignored.
* @param values
* @returns
*/
function maxAbs(...values) {
let maxA = Number.MIN_SAFE_INTEGER;
let max = Number.MIN_SAFE_INTEGER;
const checkV = (v) => {
if (!Number.isNaN(v) && Number.isFinite(v)) {
const va = Math.abs(v);
if (va > maxA) {
maxA = va;
max = v;
}
}
};
for (const v of values) if (typeof v === `number`) checkV(v);
else for (const subV of v) checkV(subV);
return max;
}
//#endregion
//#region ../packages/numbers/src/count.ts
/**
* Yields `amount` integers, counting by one from zero. If a negative amount is used,
* count decreases. If `offset` is provided, this is added to the return result.
* @example
* ```js
* const a = [...count(5)]; // Yields five numbers: [0,1,2,3,4]
* const b = [...count(-5)]; // Yields five numbers: [0,-1,-2,-3,-4]
* for (const v of count(5, 5)) {
* // Yields: 5, 6, 7, 8, 9
* }
* const c = [...count(5,1)]; // Yields [1,2,3,4,5]
* ```
*
* @example Used with forEach
* ```js
* // Prints `Hi` 5x
* forEach(count(5), () => // do something);
* ```
*
* If you want to accumulate return values, consider using Flow.repeat.
*
* @example Run some code every 100ms, 10 times:
* ```js
* import { interval } from '@ixfx/flow.js'
* import { count } from '@ixfx/numbers.js'
* const counter = count(10);
* for await (const v of interval(counter, { fixedIntervalMs: 100 })) {
* // Do something
* }
* ```
* @param amount Number of integers to yield
* @param offset Added to result
*/
function* count(amount, offset = 0) {
resultThrow(integerTest(amount, ``, `amount`), integerTest(offset, ``, `offset`));
if (amount === 0) return;
let index = 0;
do
yield amount < 0 ? -index + offset : index + offset;
while (index++ < Math.abs(amount) - 1);
}
//#endregion
//#region ../packages/numbers/src/difference.ts
/**
* Returns the difference from the `initial` value. Defaults to absolute difference.
* ```js
* const rel = differenceFromFixed(100);
* rel(100); // 0
* rel(150); // 50
* rel(50); // 50
* ```
*
* 'numerical' gives sign:
* ```js
* const rel = differenceFromFixed(100, `numerical`);
* rel(100); // 0
* rel(150); // 50
* rel(50); // -50
* ```
*
* 'relative' gives proportion to initial
* ```js
* const rel = differenceFromFixed(100, `relative`);
* rel(100); // 0
* rel(150); // 0.5
* rel(10); // 0.90
* ```
*
* Using 'relativeSigned', we get negative relative result
* when value is below the initial value.
*
* Use {@link differenceFromLast} to compare against the last value,
* rather than the same fixed value.
* @param {number} initial Value to compare against
* @returns Difference from initial value
*/
const differenceFromFixed = (initial, kind = `absolute`) => (value) => differenceFrom(kind, value, initial);
/**
* Returns a function which yields difference compared to last value.
*
* If no initial value is provided, the first difference will be returned as 0.
*
* Difference can be returned in various formats:
* * 'absolute': numerical difference, without sign
* * 'numerical': numerical difference, with sign, so you can see if difference is higher or lower
* * 'relative': difference divided by last value, giving a proportional difference. Unsigned.
* * 'relativeSigned': as above, but with sign
*
* Use {@link differenceFromFixed} to compare against a fixed value instead of the last value.
*
* ```js
* let d = differenceFromLast(`absolute`);
* d(10); // 0
* d(11); // 1
* d(10); // 1
* ```
*
* ```js
* let d = differenceFromLast(`numerical`);
* d(10); // 0
* d(11); // 1
* d(10); // -1
* ```
*
* ```js
* let d = differenceFromLast(`relative`);
* d(10); // 0
* d(11); // 0.1
* d(10); // 0.1
* ```
* ```js
* let d = differenceFromLast(`relativeSigned`);
* d(10); // 0
* d(11); // 0.1
* d(10); // -0.1
* ```
*
* An initial value can be provided, eg:
* ```js
* let d = differenceFromLast(`absolute`, 10);
* d(11); // 1
* ```
* @param kind Kind of output value
* @param initialValue Optional initial value
* @returns
*/
const differenceFromLast = (kind = `absolute`, initialValue = NaN) => {
let lastValue = initialValue;
return (value) => {
const x = differenceFrom(kind, value, lastValue);
lastValue = value;
return x;
};
};
const differenceFrom = (kind = `absolute`, value, from) => {
if (Number.isNaN(from)) return 0;
const d = value - from;
let r = 0;
if (kind === `absolute`) r = Math.abs(d);
else if (kind === `numerical`) r = d;
else if (kind === `relative`) r = Math.abs(d / from);
else if (kind === `relativeSigned`) r = d / from;
else throw new TypeError(`Unknown kind: '${kind}' Expected: 'absolute', 'relative', 'relativeSigned' or 'numerical'`);
return r;
};
//#endregion
//#region ../packages/numbers/src/guard.ts
/**
* Returns true if `possibleNumber` is a number and not NaN
* @param possibleNumber
* @returns
*/
const isValid = (possibleNumber) => {
if (typeof possibleNumber !== `number`) return false;
if (Number.isNaN(possibleNumber)) return false;
return true;
};
//#endregion
//#region ../packages/numbers/src/filter.ts
/**
* Filters an iterator of values, only yielding
* those that are valid numbers
*
* ```js
* const data = [true, 10, '5', { x: 5 }];
* for (const n of Numbers.filterIterable(data)) {
* // 10
* }
* ```
* @param it
*/
function* filterIterable(it) {
for (const v of it) if (isValid(v)) yield v;
}
/**
* Returns a function that yields _true_ if a value
* is at least `threshold`
* ```js
* const t = thresholdAtLeast(50);
* t(50); // true
* t(0); // false
* t(55); // true
* ```
* @param threshold
* @returns
*/
const thresholdAtLeast = (threshold) => {
return (v) => {
return v >= threshold;
};
};
/**
* Returns a function that yields _true_
* if a number is at least _min_ and no greater than _max_
*
* ```js
* const t = rangeInclusive(50, 100);
* t(40); // false
* t(50); // true
* t(60); // true
* t(100); // true
* t(101); // false
* ```
* @param min
* @param max
* @returns
*/
const rangeInclusive = (min, max) => {
return (v) => {
return v >= min && v <= max;
};
};
//#endregion
//#region ../packages/numbers/src/flip.ts
/**
* Flips a percentage-scale number: `1 - v`.
*
* The utility of this function is that it sanity-checks
* that `v` is in 0..1 scale.
*
* ```js
* flip(1); // 0
* flip(0.5); // 0.5
* flip(0); // 1
* ```
* @param v
* @returns
*/
const flip = (v) => {
if (typeof v === `function`) v = v();
resultThrow(numberTest(v, `percentage`, `v`));
return 1 - v;
};
//#endregion
//#region ../packages/numbers/src/generate.ts
/**
* Generates a range of numbers, starting from `start` and counting by `interval`.
* If `end` is provided, generator stops when reached
*
* Unlike {@link numericRange}, numbers might contain rounding errors
*
* ```js
* for (const c of numericRangeRaw(10, 100)) {
* // 100, 110, 120 ...
* }
* ```
*
* Get results as an array
* ```js
* const c = [...numericRangeRaw(1,0,5)]; // [0,1,2,3,4]
* ```
* @param interval Interval between numbers
* @param start Start
* @param end End (if undefined, range never ends). Inclusive.
*/
const numericRangeRaw = function* (interval, start = 0, end, repeating = false) {
if (interval <= 0) throw new Error(`Interval is expected to be above zero`);
if (typeof end === `undefined`) end = Number.MAX_SAFE_INTEGER;
let v = start;
do
while (v <= end) {
yield v;
v += interval;
}
while (repeating);
};
/**
* Generates a range of numbers, with a given interval.
*
* @example For-loop
* ```
* let loopForever = numericRange(0.1); // By default starts at 0 and counts upwards forever
* for (v of loopForever) {
* console.log(v);
* }
* ```
*
* @example If you want more control over when/where incrementing happens...
* ```js
* let percent = numericRange(0.1, 0, 1);
*
* let percentResult = percent.next().value;
* ```
*
* Note that computations are internally rounded to avoid floating point math issues. So if the `interval` is very small (eg thousandths), specify a higher rounding
* number.
*
* @param interval Interval between numbers
* @param start Start. Defaults to 0
* @param end End (if undefined, range never ends). Inclusive.
* @param repeating Range loops from start indefinately. Default _false_
* @param rounding A rounding that matches the interval avoids floating-point math hikinks. Eg if the interval is 0.1, use a rounding of 10
*/
const numericRange = function* (interval, start = 0, end, repeating = false, rounding) {
resultThrow(numberTest(interval, `nonZero`));
const negativeInterval = interval < 0;
if (end === void 0) {} else {
if (negativeInterval && start < end) throw new Error(`Interval of ${interval.toString()} will never go from ${start.toString()} to ${end.toString()}`);
if (!negativeInterval && start > end) throw new Error(`Interval of ${interval.toString()} will never go from ${start.toString()} to ${end.toString()}`);
}
rounding = rounding ?? 1e3;
if (end === void 0) end = Number.MAX_SAFE_INTEGER;
else end *= rounding;
interval = interval * rounding;
do {
let v = start * rounding;
while (!negativeInterval && v <= end || negativeInterval && v >= end) {
yield v / rounding;
v += interval;
}
} while (repeating);
};
/**
* Yields numeric range between 0.0-1.0.
*
* ```
* // Yields: [0, 0.2, 0.4, 0.6, 0.8, 1]
* const a = [...numericPercent(0.2)];
*
* // Repeating flag set to true:
* for (const v of numericPercent(0.2, true)) {
* // Infinite loop. V loops back to 0 after hitting 1
* }
* ```
*
* If `repeating` is true, it loops back to 0 after reaching 1
* @param interval Interval (default: 0.01, ie. 1%)
* @param repeating Whether generator should loop (default: false)
* @param start Start (default: 0)
* @param end End (default: 1)
* @returns
*/
const numericPercent = function(interval = .01, repeating = false, start = 0, end = 1) {
resultThrow(numberTest(interval, `percentage`, `interval`), numberTest(start, `percentage`, `start`), numberTest(end, `percentage`, `end`));
return numericRange(interval, start, end, repeating);
};
//#endregion
//#region ../packages/numbers/src/is-approx.ts
/**
* Checks if a value is within range of a base value
*
* ```js
* // Check if 101 is within 10% of 100
* isApprox(0.1, 100, 101);
*
* // Gets a function to compare some value of 10% range to 100
* const c = isApprox(0.1,100);
* c(101);
*
* // Gets a function to compare some base value and value to 10% range
* const c = isApprox(0.1);
* c(100, 101);
* ```
*
* Throws an error if range or base values are NaN.
* If value being checked is NaN or infinity, _false_ is returned.
* @param rangePercent
* @param baseValue
* @param v
* @returns
*/
function isApprox(rangePercent, baseValue, v) {
resultThrow(numberTest(rangePercent, `percentage`, `rangePercent`));
const range = Math.floor(rangePercent * 100);
const test = (base, value) => {
try {
if (typeof value !== `number`) return false;
if (Number.isNaN(value)) return false;
if (!Number.isFinite(value)) return false;
const diff = Math.abs(value - base);
return (base === 0 ? Math.floor(diff * 100) : Math.floor(diff / base * 100)) <= range;
} catch {
return false;
}
};
if (baseValue === void 0) return test;
resultThrow(numberTest(baseValue, ``, `baseValue`));
if (v === void 0) return (value) => test(baseValue, value);
else return test(baseValue, v);
}
/**
* Yields a function that checks if a value is close to any target value
* ```js
* const c = isCloseToAny(1, 10, 20, 30, 40);
* c(11); // True - within 1 range of 10
* c(19); // True - within 1 range of 20
* c(0); // False
* ```
*
* Returned function accepts multiple values, returning
* _true_ if any of them are within range
* ```js
* c(0, 1, 11); // Would return true based on 11
* ```
* @param allowedRangeAbsolute
* @param targets
* @returns
*/
const isCloseToAny = (allowedRangeAbsolute, ...targets) => {
const targetsMin = targets.map((t) => t - allowedRangeAbsolute);
const targetsMax = targets.map((t) => t + allowedRangeAbsolute);
return (...values) => {
for (const v of values) for (let index = 0; index < targets.length; index++) if (v >= targetsMin[index] && v <= targetsMax[index]) return true;
return false;
};
};
//#endregion
//#region ../packages/numbers/src/kalman.ts
/**
* KalmanFilter
*
* author: Wouter Bulten
* see {@link http://github.com/wouterbulten/kalmanjs}
* version Version: 1.0.0-beta
* copyright Copyright 2015-2018 Wouter Bulten
* license MIT License
*/
var Kalman1dFilter = class {
R;
Q;
A;
C;
B;
cov;
x;
/**
* Create 1-dimensional kalman filter
*/
constructor(options = {}) {
this.R = options.r ?? 1;
this.Q = options.q ?? 1;
this.A = options.a ?? 1;
this.C = options.c ?? 1;
this.B = options.b ?? 0;
this.cov = NaN;
this.x = NaN;
}
/**
* Filter a new value
* @param {Number} z Measurement
* @param {Number} u Control
* @return {Number}
*/
filter(z, u = 0) {
if (isNaN(this.x)) {
this.x = 1 / this.C * z;
this.cov = 1 / this.C * this.Q * (1 / this.C);
} else {
const predX = this.predict(u);
const predCov = this.uncertainty();
const K = predCov * this.C * (1 / (this.C * predCov * this.C + this.Q));
this.x = predX + K * (z - this.C * predX);
this.cov = predCov - K * this.C * predCov;
}
return this.x;
}
/**
* Predict next value
* @param {Number} [u] Control
* @return {Number}
*/
predict(u = 0) {
return this.A * this.x + this.B * u;
}
/**
* Return uncertainty of filter
* @return {Number}
*/
uncertainty() {
return this.A * this.cov * this.A + this.R;
}
/**
* Return the last filtered measurement
* @return {Number}
*/
lastMeasurement() {
return this.x;
}
/**
* Set measurement noise Q
* @param {Number} noise
*/
setMeasurementNoise(noise) {
this.Q = noise;
}
/**
* Set the process noise R
* @param {Number} noise
*/
setProcessNoise(noise) {
this.R = noise;
}
};
/**
* Returns a function that performs 1D Kalman filtering.
*
* ```js
* const f = kalman1dFilter();
* f(10); // 10
* ```
*
* Under the hood creates a {@link Kalman1dFilter} instance and returns its `filter` method.
* @param options
* @returns
*/
const kalman1dFilter = (options = {}) => {
const f = new Kalman1dFilter(options);
return f.filter.bind(f);
};
//#endregion
//#region ../packages/numbers/src/bipolar.ts
var bipolar_exports = /* @__PURE__ */ __exportAll({
clamp: () => clamp,
fromScalar: () => fromScalar,
immutable: () => immutable,
scale: () => scale$1,
scaleUnclamped: () => scaleUnclamped,
toScalar: () => toScalar,
towardZero: () => towardZero
});
/**
* Wrapper for bipolar-based values. Immutable.
* All functions will clamp to keep it in legal range.
*
* ```js
* let v = immutable(); // Starts with 0 by default
* v = v.add(0.1); // v.value is 0.1
* v = v.inverse(); // v.value is -0.1
* v = v.multiply(0.2); // v.value is -0.02
*
* v = immutable(1);
* v = v.towardZero(0.1); // 0.9
* v = v.interpolate(0.1, 1);
* ```
*
* Wrapped values can be coerced into number:
* ```js
* const v = immutable(1);
* const x = +v+10;
* // x = 11
* ```
* @param startingValueOrBipolar Initial numeric value or BipolarWrapper instance
* @throws {TypeError} If start value is out of bipolar range or invalid
* @returns
*/
const immutable = (startingValueOrBipolar = 0) => {
const startingValue = typeof startingValueOrBipolar === `number` ? startingValueOrBipolar : startingValueOrBipolar.value;
if (startingValue > 1) throw new TypeError(`Start value cannot be larger than 1`);
if (startingValue < -1) throw new TypeError(`Start value cannot be smaller than -1`);
if (Number.isNaN(startingValue)) throw new TypeError(`Start value is NaN`);
const v = startingValue;
return {
[Symbol.toPrimitive](hint) {
if (hint === `number` || hint === `default`) return v;
else if (hint === `string`) return v.toString();
return true;
},
value: v,
towardZero: (amount) => {
return immutable(towardZero(v, amount));
},
add: (amount) => {
return immutable(clamp(v + amount));
},
multiply: (amount) => {
return immutable(clamp(v * amount));
},
inverse: () => {
return immutable(-v);
},
interpolate: (amount, target) => {
return immutable(clamp(interpolate(amount, v, target)));
},
asScalar: (max = 1, min = 0) => {
return toScalar(v, max, min);
}
};
};
/**
* Converts bipolar value to a scalar. That is, converts from
* -1..1 range to 0..1.
*
* ```js
* Bipolar.toScalar(-1); // 0.0
* Bipolar.toScalar( 0); // 0.5
* Bipolar.toScalar( 1); // 1.0
* ```
*
* Range can be changed:
* ```js
* Bipolar.toScalar(0, 100); // Uses 0..100 scale, so output is 50
* Bipolar.toScalar(0, 100, 50); // Uses 50..1000 scale, so output is 75
* ```
*
* Throws an error if `bipolarValue` is not a number or NaN
* @param bipolarValue Value to convert to scalar
* @returns Scalar value on 0..1 range.
*/
const toScalar = (bipolarValue, max = 1, min = 0) => {
if (typeof bipolarValue !== `number`) throw new Error(`Param 'bipolarValue' to be a number. Got: ${typeof bipolarValue}`);
if (Number.isNaN(bipolarValue)) throw new Error(`Param 'bipolarValue' is NaN`);
return scale(bipolarValue, -1, 1, min, max);
};
/**
* Makes a scalar into a bipolar value.
*
* That is, input range is 0..1, output range is -1...1
*
* ```js
* Bipolar.fromScalar(1); // 1
* Bipolar.fromScalar(0); // -1
* Bipolar.fromScalar(0.5); // 0
* ```
*
* Throws an error if `scalarValue` is outside 0..1 scale.
* @param scalarValue Scalar value to convert
* @returns Bipolar value on -1..1 scale
*/
const fromScalar = (scalarValue) => {
resultThrow(numberTest(scalarValue, `percentage`, `v`));
return scalarValue * 2 - 1;
};
/**
* Scale & clamp value to bipolar range (-1..1).
* ```js
* // Scale 100 on 0..100 scale
* Bipolar.scale(100, 0, 100); // 1
* Bipolar.scale(50, 0, 100); // 0
* Bipolar.scale(0, 0, 100); // -1
* ```
*
* Return value is clamped.
* @param inputValue Value to scale
* @param inMin Minimum of scale
* @param inMax Maximum of scale
* @returns Bipolar value on -1..1 scale
*/
const scale$1 = (inputValue, inMin, inMax) => {
return clamp(scaler(inMin, inMax, -1, 1)(inputValue));
};
/**
* Scale a number to bipolar range (-1..1). Not clamped, so we might exceed range.
*
* ```js
* // Scale 100 on 0..100 scale
* Bipolar.scaleUnclamped(100, 0, 100); // 1
* Bipolar.scaleUnclamped(50, 0, 100); // 0
* Bipolar.scaleUnclamped(0, 0, 100); // -1
* ```
*
* @param inputValue Value to scale
* @param inMin Minimum of scale
* @param inMax Maximum of scale
* @returns Bipolar value on -1..1 scale
*/
const scaleUnclamped = (inputValue, inMin, inMax) => {
return scaler(inMin, inMax, -1, 1)(inputValue);
};
/**
* Clamp a bipolar value
* ```js
* Bipolar.clamp(-1); // -1
* Bipolar.clamp(-1.1); // -1
* ```
*
* Throws an error if `bipolarValue` is not a number or NaN.
* @param bipolarValue Value to clamp
* @returns Clamped value on -1..1 scale
*/
const clamp = (bipolarValue) => {
if (typeof bipolarValue !== `number`) throw new Error(`Param 'bipolarValue' must be a number. Got: ${typeof bipolarValue}`);
if (Number.isNaN(bipolarValue)) throw new Error(`Param 'bipolarValue' is NaN`);
if (bipolarValue > 1) return 1;
if (bipolarValue < -1) return -1;
return bipolarValue;
};
/**
* Pushes a bipolar value toward zero by `amount`.
* Return value is clamped on bipolar range of -1..1
*
* ```js
* Bipolar.towardZero(-1, 0.1); // -0.9
* Bipolar.towardZero( 1, 0.1); // 0.9
* Bipolar.towardZero( 0, 0.1); // 0.0
* Bipolar.towardZero( 1, 1.1); // 0.0
* ```
*
* If `amount` is greater than 1, 0 is returned.
* Throws an error if `bipolarValue` or `amount` are not numbers.
* Throws an error if `amount` is below zero.
* @param bipolarValue Bipolar value to nudge toward zero
* @param amount Amount to nudge by
* @returns Bipolar value -1...1
*/
const towardZero = (bipolarValue, amount) => {
if (typeof bipolarValue !== `number`) throw new Error(`Parameter 'bipolarValue' must be a number. Got: ${typeof bipolarValue}`);
if (typeof amount !== `number`) throw new Error(`Parameter 'amount' must be a number. Got: ${typeof amount}`);
if (amount < 0) throw new Error(`Parameter 'amount' must be positive`);
if (bipolarValue < 0) {
bipolarValue += amount;
if (bipolarValue > 0) bipolarValue = 0;
} else if (bipolarValue > 0) {
bipolarValue -= amount;
if (bipolarValue < 0) bipolarValue = 0;
}
return bipolarValue;
};
//#endregion
//#region ../packages/numbers/src/wrap.ts
/**
* Wraps an integer number within a specified range, defaulting to degrees (0-360). Use {@link wrap} for floating-point wrapping.
*
* This is useful for calculations involving degree angles and hue, which wrap from 0-360.
* Eg: to add 200 to 200, we don't want 400, but 40.
*
* ```js
* const v = wrapInteger(200+200, 0, 360); // 40
* ```
*
* Or if we minus 100 from 10, we don't want -90 but 270
* ```js
* const v = wrapInteger(10-100, 0, 360); // 270
* ```
*
* `wrapInteger` uses 0-360 as a default range, so both of these
* examples could just as well be:
*
* ```js
* wrapInteger(200+200); // 40
* wrapInteger(10-100); // 270
* ```
*
* Non-zero starting points can be used. A range of 20-70:
* ```js
* const v = wrapInteger(-20, 20, 70); // 50
* ```
*
* Note that the minimum value is inclusive, while the maximum is _exclusive_.
* So with the default range of 0-360, 360 is never reached:
*
* ```js
* wrapInteger(360); // 0
* wrapInteger(361); // 1
* ```
*
* If you just want to lock values to a range without wrapping, consider {@link clamp}.
*
* @param v Value to wrap
* @param min Integer minimum of range (default: 0). Inclusive
* @param max Integer maximum of range (default: 360). Exlusive
* @returns
*/
const wrapInteger = (v, min = 0, max = 360) => {
resultThrow(integerTest(v, void 0, `v`), integerTest(min, void 0, `min`), integerTest(max, void 0, `max`));
if (v === min) return min;
if (v === max) return min;
if (v > 0 && v < min) v += min;
v -= min;
max -= min;
v = v % max;
if (v < 0) v = max - Math.abs(v) + min;
return v + min;
};
/**
* Wraps floating point numbers to be within a range (default: 0..1). Use {@link wrapInteger} if you want to wrap integer values.
*
* This logic makes sense for some things like rotation angle.
*
* If you just want to lock values to a range without wrapping, consider {@link clamp}.
*
* ```js
* wrap(1.2); // 0.2
* wrap(2); // 1.0
* wrap(-0.2); // 0.8
* ```
*
* A range can be provided too:
* ```js
* wrap(30, 20, 50); // 30
* wrap(60, 20, 50); // 30
* ```
* @param v
* @param min
* @param max
* @returns
*/
const wrap = (v, min = 0, max = 1) => {
resultThrow(numberTest(v, ``, `min`), numberTest(min, ``, `min`), numberTest(max, ``, `max`));
if (v === min) return min;
if (v === max) return min;
while (v <= min || v >= max) {
if (v === max) break;
if (v === min) break;
if (v > max) v = min + (v - max);
else if (v < min) v = max - (min - v);
}
return v;
};
/**
* Performs a calculation within a wrapping number range. This is a lower-level function.
* See also: {@link wrapInteger} for simple wrapping within a range.
*
* `min` and `max` define the start and end of the valid range, inclusive. Eg for hue degrees it'd be 0, 360.
* `a` and `b` is the range you want to work in.
*
* For example, let's say you want to get the middle point between a hue of 30 and a hue of 330 (ie warmer colours):
* ```js
* wrapRange(0,360, (distance) => {
* // for a:0 and b:330, distance would be 90 from 30 degrees to 330 (via zero)
* return distance * 0.5; // eg return middle point
* }, 30, 330);
* ```
*
* The return value of the callback should be in the range of 0-distance. `wrapRange` will subsequently
* conform it to the `min` and `max` range before it's returned to the caller.
*
* @param a Output start (eg. 60)
* @param b Output end (eg 300)
* @param min Range start (eg 0)
* @param max Range end (eg 360)
* @param fn Returns a computed value from 0 to `distance`.
* @returns
*/
const wrapRange = (min, max, fn, a, b) => {
let r = 0;
const distF = Math.abs(b - a);
const distFwrap = Math.abs(max - a + b);
const distBWrap = Math.abs(a + (360 - b));
const distMin = Math.min(distF, distFwrap, distBWrap);
if (distMin === distBWrap) r = a - fn(distMin);
else if (distMin === distFwrap) r = a + fn(distMin);
else if (a > b) r = a - fn(distMin);
else r = a + fn(distMin);
return wrapInteger(r, min, max);
};
//#endregion
//#region ../packages/numbers/src/pi-pi.ts
const piPi = Math.PI * 2;
//#endregion
//#region ../packages/numbers/src/interpolate.ts
/**
* Interpolates between `a` and `b` by `amount`. Aka `lerp`.
*
* [ixfx Guide on Interpolation](https://ixfx.fun/data/interpolation/overview/)
*
* @example Get the halfway point between 30 and 60
* ```js
* interpolate(0.5, 30, 60);
* ```
*
* See also {@link interpolatorStepped} and {@link https://api.ixfx.fun/_ixfx/modulation/interpolatorInterval/} for functions
* which help to manage progression from A->B over steps or interval.
*
* Usually interpolation amount is on a 0...1 scale, inclusive. What is the interpolation result
* if this scale is exceeded? By default it is clamped to 0..1, so the return value is always between `a` and `b` (inclusive).
*
* Alternatively, set the `limits` option to process `amount`:
* * 'wrap': wrap amount, eg 1.5 is the same as 0.5, 2 is the same as 1
* * 'ignore': allow exceeding values. eg 1.5 will yield b*1.5.
* * 'clamp': default behaviour of clamping interpolation amount to 0..1
*
* Interpolation can be non-linear using 'easing' option or 'transform' funciton.
* ```js
* interpolate(0.1, 0, 100, { easing: `quadIn` });
* ```
* There are a few variations when calling `interpolate`, depending on what parameters are fixed.
* * `interpolate(amount)`: returns a function that needs a & b
* * `interpolate(a, b)`: returns a function that needs the interpolation amount
*/
function interpolate(pos1, pos2, pos3, pos4) {
let amountProcess;
let limits = `clamp`;
const handleAmount = (amount) => {
if (amountProcess) amount = amountProcess(amount);
if (limits === void 0 || limits === `clamp`) amount = clamp$1(amount);
else if (limits === `wrap`) {
if (amount > 1) amount = amount % 1;
else if (amount < 0) amount = 1 + amount % 1;
}
return amount;
};
const doTheEase = (_amt, _a, _b) => {
resultThrow(numberTest(_a, ``, `a`), numberTest(_b, ``, `b`), numberTest(_amt, ``, `amount`));
_amt = handleAmount(_amt);
return (1 - _amt) * _a + _amt * _b;
};
const readOpts = (o = {}) => {
if (o.transform !== void 0) {
if (typeof o.transform !== `function`) throw new Error(`Param 'transform' is expected to be a function. Got: ${typeof o.transform}`);
amountProcess = o.transform;
}
limits = o.limits ?? `clamp`;
};
const rawEase = (_amt, _a, _b) => (1 - _amt) * _a + _amt * _b;
if (typeof pos1 !== `number`) throw new TypeError(`First param is expected to be a number. Got: ${typeof pos1}`);
if (typeof pos2 === `number`) {
let a;
let b;
if (pos3 === void 0 || typeof pos3 === `object`) {
a = pos1;
b = pos2;
readOpts(pos3);
return (amount) => doTheEase(amount, a, b);
} else if (typeof pos3 === `number`) {
a = pos2;
b = pos3;
readOpts(pos4);
return doTheEase(pos1, a, b);
} else throw new Error(`Values for 'a' and 'b' not defined`);
} else if (pos2 === void 0 || typeof pos2 === `object`) {
const amount = handleAmount(pos1);
readOpts(pos2);
resultThrow(numberTest(amount, ``, `amount`));
return (aValue, bValue) => rawEase(amount, aValue, bValue);
}
}
/**
* Returns a function that interpolates from A to B.
* It steps through the interpolation with each call to the returned function.
* This means that the `incrementAmount` will hinge on the rate
* at which the function is called. Alternatively, consider {@link https://api.ixfx.fun/_ixfx/modulation/interpolatorInterval/}
* which steps on the basis of clock time.
*
* ```js
* // Interpolate from 0..1 by 0.01
* const v = interpolatorStepped(0.01, 100, 200);
* v(); // Each call returns a value closer to target
* // Eg: 100, 110, 120, 130 ...
* ```
*
* Under the hood, it calls `interpolate` with an amount that
* increases by `incrementAmount` each time.
*
* When calling `v()` to step the interpolator, you can also pass
* in new B and A values. Note that the order is swapped: the B (target) is provided first, and
* then optionally A.
*
* ```js
* const v = interpolatorStepped(0.1, 100, 200); // Interpolate 100->200
* v(300, 200); // Retarget to 200->300 and return result
* v(150); // Retarget 200->150 and return result
* ```
*
* This allows you to maintain the current interpolation progress.
* @param incrementAmount Amount to increment by
* @param a Start value. Default: 0
* @param b End value. Default: 1
* @param startInterpolationAt Starting interpolation amount. Default: 0
* @param options Options for interpolation
* @returns
*/
const interpolatorStepped = (incrementAmount, a = 0, b = 1, startInterpolationAt = 0, options) => {
let amount = startInterpolationAt;
return (retargetB, retargetA) => {
if (retargetB !== void 0) b = retargetB;
if (retargetA !== void 0) a = retargetA;
if (amount >= 1) return b;
const value = interpolate(amount, a, b, options);
amount += incrementAmount;
return value;
};
};
/**
* Interpolate between angles `a` and `b` by `amount`. Angles are in radians.
*
* ```js
* interpolateAngle(0.5, Math.PI, Math.PI/2);
* ```
* @param amount
* @param aRadians Start angle (radian)
* @param bRadians End angle (radian)
* @returns
*/
const interpolateAngle = (amount, aRadians, bRadians, options) => {
const t = wrap(bRadians - aRadians, 0, piPi);
return interpolate(amount, aRadians, aRadians + (t > Math.PI ? t - piPi : t), options);
};
//#endregion
//#region ../packages/numbers/src/iqr.ts
/**
* Calculate interquartile range.
*
* If `n` is unspecified, `data.length` is used.
* @param data
* @param n
* @returns
*/
const interquartileRange = (data, n) => {
return getQuantile(data, .75) - getQuantile(data, .25);
};
/**
* Returns a function which itself returns _true_ if a value is an outlier.
*
* This can be used for example to get a copy of an array without outliers:
* ```js
* const p = computeIsOutlier(someData);
* const someDataWithoutOutliers = someData.filter(value => !p(value));
* ```
*
* Outliers are defined as: "a point which falls more than 1.5 times the interquartile range above the third quartile or below the first quartile." [Wolfram](https://mathworld.wolfram.com/Outlier.html)
*
* If array length is less than 4, no value will be considered an outlier.
* @param data Data to filter
* @param multiplier Multiplier of Q3 Q1. Default: 1.5
* @returns
*/
const computeIsOutlier = (data, multiplier = 1.5) => {
if (data.length < 4) return (value) => false;
const values = data.toSorted((a, b) => a - b);
const q1 = getQuantile(values, .25, true);
const q3 = getQuantile(values, .75, true);
const iqr = q3 - q1;
const maxValue = q3 + iqr * multiplier;
const minValue = q1 - iqr * multiplier;
return (value) => value < minValue || value > maxValue;
};
/**
* Gets the value at a specific quantile
* ```js
* getQuantile(data, 25); // 1st quartile
* getQuantile(data, 75); // 3rd quartile
* ```
* @param data
* @param quantile
* @param presorted Pass _true_ if `data` is already sorted
* @returns
*/
const getQuantile = (data, quantile, presorted = false) => {
if (quantile > 1 || quantile < 0) throw new TypeError(`Param 'quantile' is expected to be in 0..1 range. Got: '${quantile}'`);
if (!Array.isArray(data)) throw new TypeError(`Param 'data' is expected to be an array. Got: ${typeof data}`);
const index = quantile * (data.length - 1);
if (!presorted) data = data.toSorted((a, b) => a - b);
if (quantile === 0) return data[0];
if (quantile === 1) return data[data.length - 1];
if (index % 1 === 0) return data[index];
const lowerIndex = Math.floor(index);
if (data[lowerIndex + 1] !== void 0) return (data[lowerIndex] + data[lowerIndex + 1]) / 2;
return data[lowerIndex];
};
//#endregion
//#region ../packages/numbers/src/round.ts
/**
* Rounds a number.
*
* If one parameter is given, it's the decimal places,
* and a rounding function is returned:
* ```js
* const r = round(2);
* r(10.12355); // 10.12
* ```
*
* If two parameters are given, the first is decimal places,
* the second the value to round.
* ```js
* round(2, 10.12355); // 10.12
* ```
* @param decimalPlaces
* @returns
*/
function round(a, b, roundUp) {
resultThrow(integerTest(a, `positive`, `decimalPlaces`));
const up = typeof b === `boolean` ? b : roundUp ?? false;
let rounder;
if (a === 0) rounder = Math.round;
else {
const p = Math.pow(10, a);
if (up) rounder = (v) => Math.ceil(v * p) / p;
else rounder = (v) => Math.floor(v * p) / p;
}
if (typeof b === `number`) return rounder(b);
return rounder;
}
//#endregion
//#region ../packages/numbers/src/linear-space.ts
/**
* Generates a `step`-length series of values between `start` and `end` (inclusive).
* Each value will be equally spaced.
*
* ```js
* for (const v of linearSpace(1, 5, 6)) {
* // Yields: [ 1, 1.8, 2.6, 3.4, 4.2, 5 ]
* }
* ```
*
* Numbers can be produced from large to small as well
* ```js
* const values = [...linearSpace(10, 5, 3)];
* // Yields: [10, 7.5, 5]
* ```
* @param start Start number (inclusive)
* @param end End number (inclusive)
* @param steps How many steps to make from start -> end
* @param precision Number of decimal points to round to
*/
function* linearSpace(start, end, steps, precision) {
resultThrow(numberTest(start, ``, `start`), numberTest(end, ``, `end`), numberTest(steps, ``, `steps`));
const r = precision ? round(precision) : (v) => v;
const step = (end - start) / (steps - 1);
resultThrow(numberTest(step, ``, `step`));
if (!Number.isFinite(step)) throw new TypeError(`Calculated step value is infinite`);
for (let index = 0; index < steps; index++) yield r(start + step * index);
}
//#endregion
//#region ../packages/numbers/src/moving-average.ts
const PiPi = Math.PI * 2;
/**
* A moving average calculator (exponential weighted moving average) which does not keep track of
* previous samples. Less accurate, but uses less system resources.
*
* The `scaling` parameter determines smoothing. A value of `1` means that
* the latest value is used as the average - that is, no smoothing. Higher numbers
* introduce progressively more smoothing by weighting the accumulated prior average more heavily.
*
* ```
* const ma = movingAverageLight(); // default scaling of 3
* ma(50); // 50
* ma(100); // 75
* ma(75); // 75
* ma(0); // 50
* ```
*
* Note that the final average of 50 is pretty far from the last value of 0. To make it more responsive,
* we could use a lower scaling factor: `movingAverageLight(2)`. This yields a final average of `37.5` instead.
*
* @param scaling Scaling factor. 1 is no smoothing. Default: 3
* @returns Function that adds to average.
*/
const movingAverageLight = (scaling = 3) => {
resultThrow(numberTest(scaling, `aboveZero`, `scaling`));
let average = 0;
let count = 0;
return (v) => {
if (numberTest(v, ``, `v`).success && v !== void 0) {
count++;
average = average + (v - average) / Math.min(count, scaling);
}
return average;
};
};
/**
* Creates a moving average for a set number of `samples`.
* It returns a function which in turn yields an average value.
*
* Moving average are useful for computing the average over a recent set of numbers.
* A lower number of samples produces a computed value that is lower-latency yet more jittery.
* A higher number of samples produces a smoother computed value which takes longer to respond to
* changes in data.
*
* Sample size is considered with respect to the level of latency/smoothness trade-off, and also
* the rate at which new data is added to the moving average.
*
*
* ```js
* const ma = movingAverage(10);
* ma(10); // 10
* ma(5); // 7.5
* ```
*
* A weighting function can be provided to shape how the average is
* calculated - eg privileging the most recent data over older data.
* It uses `Arrays.averageWeighted` under the hood.
*
* ```js
* import { movingAverage } from '@ixfx/numbers.js';
* import { gaussian } from '@ixfx/modulation.js';
*
* // Give more weight to data in middle of sampling window
* const ma = movingAverage(100, gaussian());
* ```
*
* Because it keeps track of `samples` previous data, there is a memory impact. A lighter version is {@link movingAverageLight} which does not keep a buffer of prior data, but can't be as easily fine-tuned.
* @param samplesOrOptions Number of samples to compute average from, or object of options
* @returns
*/
const movingAverage = (samplesOrOptions) => movingAverageWithContext(samplesOrOptions).seen;
const movingAverageWithContext = (samplesOrOptions) => {
const nanPolicy = typeof samplesOrOptions === `number` ? `ignore` : samplesOrOptions.nanPolicy ?? `ignore`;
const w = movingWindowWithContext(samplesOrOptions);
const averageFunction = typeof samplesOrOptions === `number` ? average : samplesOrOptions.weighter ? averageWeigher(samplesOrOptions.weighter) : average;
const seen = (value) => {
if (Number.isNaN(value)) {
if (nanPolicy === `throw`) throw new TypeError(`Value is NaN`);
if (nanPolicy === `ignore`) return w.data;
}
return averageFunction(w.seen(value));
};
return {
seen,
get data() {
return [...w.data];
},
get average() {
return averageFunction(w.data);
}
};
};
const smoothingFactor = (timeDelta, cutoff) => {
const r = PiPi * cutoff * timeDelta;
return r / (r + 1);
};
const exponentialSmoothing = (smoothingFactor, value, previous) => {
return smoothingFactor * value + (1 - smoothingFactor) * previous;
};
/**
* Noise filtering
*
* Algorithm: https://gery.casiez.net/1euro/
*
* Based on [Jaan Tollander de Balsch's implementation](https://jaantollander.com/post/noise-filtering-using-one-euro-filter/)
* @param cutoffMin Default: 1
* @param speedCoefficient Default: 0
* @param cutoffDefault Default: 1
*/
const noiseFilter = (cutoffMin = 1, speedCoefficient = 0, cutoffDefault = 1) => {
let previousValue = 0;
let derivativeLast