forked from osuushi/Smooth.js
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
358 lines (279 loc) · 11.7 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
/*
* decaffeinate suggestions:
* DS001: Remove Babel/TypeScript constructor workaround
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS202: Simplify dynamic range loops
* DS203: Remove `|| {}` from converted for-own loops
* DS205: Consider reworking code to avoid use of IIFEs
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/*
Smooth.js version 0.1.7
Turn arrays into smooth functions.
Copyright 2012 Spencer Cohen
Licensed under MIT license (see "Smooth.js MIT license.txt")
*/
/*Constants (these are accessible by Smooth.WHATEVER in user space)*/
const Enum = {
/*Interpolation methods*/
METHOD_NEAREST: 'nearest', //Rounds to nearest whole index
METHOD_LINEAR: 'linear',
METHOD_CUBIC: 'cubic', // Default: cubic interpolation
METHOD_LANCZOS: 'lanczos',
METHOD_SINC: 'sinc',
/*Input clipping modes*/
CLIP_CLAMP: 'clamp', // Default: clamp to [0, arr.length-1]
CLIP_ZERO: 'zero', // When out of bounds, clip to zero
CLIP_PERIODIC: 'periodic', // Repeat the array infinitely in either direction
CLIP_MIRROR: 'mirror', // Repeat infinitely in either direction, flipping each time
/* Constants for control over the cubic interpolation tension */
CUBIC_TENSION_DEFAULT: 0, // Default tension value
CUBIC_TENSION_CATMULL_ROM: 0
};
const defaultConfig = {
method: Enum.METHOD_CUBIC, //The interpolation method
cubicTension: Enum.CUBIC_TENSION_DEFAULT, //The cubic tension parameter
clip: Enum.CLIP_CLAMP, //The clipping mode
scaleTo: 0, //The scale-to value (0 means don't scale) (can also be a range)
sincFilterSize: 2, //The size of the sinc filter kernel (must be an integer)
sincWindow: undefined //The window function for the sinc filter
};
/*Index clipping functions*/
const clipClamp = (i, n) => Math.max(0, Math.min(i, n - 1));
const clipPeriodic = function(i, n) {
i = i % n; //wrap
if (i < 0) { i += n; } //if negative, wrap back around
return i;
};
const clipMirror = function(i, n) {
const period = 2*(n - 1); //period of index mirroring function
i = clipPeriodic(i, period);
if (i > (n - 1)) { i = period - i; } //flip when out of bounds
return i;
};
/*
Abstract scalar interpolation class which provides common functionality for all interpolators
Subclasses must override interpolate().
*/
class AbstractInterpolator {
constructor(array, config) {
this.array = array.slice(0); //copy the array
this.length = this.array.length; //cache length
//Set the clipping helper method
if (!(this.clipHelper = {
clamp: this.clipHelperClamp,
zero: this.clipHelperZero,
periodic: this.clipHelperPeriodic,
mirror: this.clipHelperMirror
}[config.clip])) { throw `Invalid clip: ${config.clip}`; }
}
// Get input array value at i, applying the clipping method
getClippedInput(i) {
//Normal behavior for indexes within bounds
if (0 <= i && i < this.length) {
return this.array[i];
} else {
return this.clipHelper(i);
}
}
clipHelperClamp(i) { return this.array[clipClamp(i, this.length)]; }
clipHelperZero(i) { return 0; }
clipHelperPeriodic(i) { return this.array[clipPeriodic(i, this.length)]; }
clipHelperMirror(i) { return this.array[clipMirror(i, this.length)]; }
interpolate(t) { throw 'Subclasses of AbstractInterpolator must override the interpolate() method.'; }
}
//Nearest neighbor interpolator (round to whole index)
class NearestInterpolator extends AbstractInterpolator {
interpolate(t) { return this.getClippedInput(Math.round(t)); }
}
//Linear interpolator (first order Bezier)
class LinearInterpolator extends AbstractInterpolator {
interpolate(t) {
const k = Math.floor(t);
//Translate t to interpolate between k and k+1
t -= k;
return ((1-t)*this.getClippedInput(k)) + ((t)*this.getClippedInput(k+1));
}
}
class CubicInterpolator extends AbstractInterpolator {
constructor(array, config){
//clamp cubic tension to [0,1] range
{
// Hack: trick Babel/TypeScript into allowing this before super.
if (false) { super(); }
let thisFn = (() => { return this; }).toString();
let thisName = thisFn.match(/return (?:_assertThisInitialized\()*(\w+)\)*;/)[1];
eval(`${thisName} = this;`);
}
this.tangentFactor = 1 - Math.max(0, Math.min(1, config.cubicTension));
super(...arguments);
}
// Cardinal spline with tension 0.5)
getTangent(k) { return (this.tangentFactor*(this.getClippedInput(k + 1) - this.getClippedInput(k - 1)))/2; }
interpolate(t) {
const k = Math.floor(t);
const m = [(this.getTangent(k)), (this.getTangent(k+1))]; //get tangents
const p = [(this.getClippedInput(k)), (this.getClippedInput(k+1))]; //get points
//Translate t to interpolate between k and k+1
t -= k;
const t2 = t*t; //t^2
const t3 = t*t2; //t^3
//Apply cubic hermite spline formula
return ((((2*t3) - (3*t2)) + 1)*p[0]) + (((t3 - (2*t2)) + t)*m[0]) + (((-2*t3) + (3*t2))*p[1]) + ((t3 - t2)*m[1]);
}
}
const {sin, PI} = Math;
//Normalized sinc function
const sinc = function(x) { if (x === 0) { return 1; } else { return sin(PI*x)/(PI*x); } };
//Make a lanczos window function for a given filter size 'a'
const makeLanczosWindow = a => x => sinc(x/a);
//Make a sinc kernel function by multiplying the sinc function by a window function
const makeSincKernel = window => x => sinc(x)*window(x);
class SincFilterInterpolator extends AbstractInterpolator {
constructor(array, config) {
super(...arguments);
//Create the lanczos kernel function
this.a = config.sincFilterSize;
//Cannot make sinc filter without a window function
if (!config.sincWindow) { throw 'No sincWindow provided'; }
//Window the sinc function to make the kernel
this.kernel = makeSincKernel(config.sincWindow);
}
interpolate(t) {
const k = Math.floor(t);
//Convolve with Lanczos kernel
let sum = 0;
for (let start = (k - this.a) + 1, n = start, end = k + this.a, asc = start <= end; asc ? n <= end : n >= end; asc ? n++ : n--) { sum += this.kernel(t - n)*this.getClippedInput(n); }
return sum;
}
}
//Extract a column from a two dimensional array
const getColumn = (arr, i) => Array.from(arr).map((row) => row[i]);
//Take a function with one parameter and apply a scale factor to its parameter
const makeScaledFunction = function(f, baseScale, scaleRange) {
if (scaleRange.join === '0,1') {
return f; //don't wrap the function unecessarily
} else {
const scaleFactor = baseScale/(scaleRange[1] - scaleRange[0]);
const translation = scaleRange[0];
return t => f(scaleFactor*(t - translation));
}
};
const getType = x => Object.prototype.toString.call(x).slice(('[object '.length), -1);
//Throw exception if input is not a number
const validateNumber = function(n) {
if (isNaN(n)) { throw 'NaN in Smooth() input'; }
if (getType(n) !== 'Number') { throw 'Non-number in Smooth() input'; }
if (!isFinite(n)) { throw 'Infinity in Smooth() input'; }
};
//Throw an exception if input is not a vector of numbers which is the correct length
const validateVector = function(v, dimension) {
if (getType(v) !== 'Array') { throw 'Non-vector in Smooth() input'; }
if (v.length !== dimension) { throw 'Inconsistent dimension in Smooth() input'; }
for (let n of Array.from(v)) { validateNumber(n); }
};
const isValidNumber = n => (getType(n) === 'Number') && isFinite(n) && !isNaN(n);
const normalizeScaleTo = function(s) {
const invalidErr = "scaleTo param must be number or array of two numbers";
switch (getType(s)) {
case 'Number':
if (!isValidNumber(s)) { throw invalidErr; }
s = [0, s];
break;
case 'Array':
if (s.length !== 2) { throw invalidErr; }
if (!isValidNumber(s[0]) || !isValidNumber(s[1])) { throw invalidErr; }
break;
default: throw invalidErr;
}
return s;
};
const shallowCopy = function(obj) {
const copy = {};
for (let k of Object.keys(obj || {})) { const v = obj[k]; copy[k] = v; }
return copy;
};
var Smooth = function(arr, config) {
//Properties to copy to the function once it is created
let baseDomainEnd, interpolatorClass, k;
let v;
if (config == null) { config = {}; }
const properties = {};
//Make a copy of the config object to modify
config = shallowCopy(config);
//Make another copy of the config object to save to the function
properties.config = shallowCopy(config);
//Alias 'period' to 'scaleTo'
if (config.scaleTo == null) { config.scaleTo = config.period; }
//Alias lanczosFilterSize to sincFilterSize
if (config.sincFilterSize == null) { config.sincFilterSize = config.lanczosFilterSize; }
for (k of Object.keys(defaultConfig || {})) { v = defaultConfig[k]; if (config[k] == null) { config[k] = v; } } //fill in defaults
//Get the interpolator class according to the configuration
if (!(interpolatorClass = {
nearest: NearestInterpolator,
linear: LinearInterpolator,
cubic: CubicInterpolator,
lanczos: SincFilterInterpolator, //lanczos is a specific case of sinc filter
sinc: SincFilterInterpolator
}[config.method])) { throw `Invalid method: ${config.method}`; }
if (config.method === 'lanczos') {
//Setup lanczos window
config.sincWindow = makeLanczosWindow(config.sincFilterSize);
}
//Make sure there's at least one element in the input array
if (arr.length < 2) { throw 'Array must have at least two elements'; }
//save count property
properties.count = arr.length;
//See what type of data we're dealing with
let smoothFunc = (() => { let dimension;
switch (getType(arr[0])) {
case 'Number': //scalar
properties.dimension = 'scalar';
//Validate all input if deep validation is on
if (Smooth.deepValidation) { for (let n of Array.from(arr)) { validateNumber(n); } }
//Create the interpolator
var interpolator = new interpolatorClass(arr, config);
//make function that runs the interpolator
return t => interpolator.interpolate(t);
case 'Array': // vector
properties.dimension = (dimension = arr[0].length);
if (!dimension) { throw 'Vectors must be non-empty'; }
//Validate all input if deep validation is on
if (Smooth.deepValidation) { for (v of Array.from(arr)) { validateVector(v, dimension); } }
//Create interpolator for each column
var interpolators = (__range__(0, dimension, false).map((i) => new interpolatorClass(getColumn(arr, i), config)));
//make function that runs the interpolators and puts them into an array
return t => (() => {
const result = [];
for (interpolator of Array.from(interpolators)) { result.push(interpolator.interpolate(t));
}
return result;
})() ;
default: throw `Invalid element type: ${getType(arr[0])}`;
} })();
// Determine the end of the original function's domain
if (config.clip === 'periodic') { baseDomainEnd = arr.length; //after last element for periodic
} else { baseDomainEnd = arr.length - 1; } //at last element for non-periodic
if (!config.scaleTo) { config.scaleTo = baseDomainEnd; } //default scales to the end of the original domain for no effect
properties.domain = normalizeScaleTo(config.scaleTo);
smoothFunc = makeScaledFunction(smoothFunc, baseDomainEnd, properties.domain);
properties.domain.sort();
/*copy properties*/
for (k of Object.keys(properties || {})) { v = properties[k]; smoothFunc[k] = v; }
return smoothFunc;
};
//Copy enums to Smooth
for (let k of Object.keys(Enum || {})) { const v = Enum[k]; Smooth[k] = v; }
Smooth.deepValidation = true;
function __range__(left, right, inclusive) {
let range = [];
let ascending = left < right;
let end = !inclusive ? right : ascending ? right + 1 : right - 1;
for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) {
range.push(i);
}
return range;
}
export default Smooth;