/* Smooth.js version 0.1.5 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) */ (function() { var AbstractInterpolator, CubicInterpolator, Enum, LinearInterpolator, NearestInterpolator, PI, SincFilterInterpolator, Smooth, clipClamp, clipMirror, clipPeriodic, defaultConfig, getColumn, getType, isValidNumber, k, makeLanczosWindow, makeScaledFunction, makeSincKernel, normalizeScaleTo, root, shallowCopy, sin, sinc, v, validateNumber, validateVector, __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; }; Enum = { /*Interpolation methods */ METHOD_NEAREST: 'nearest', METHOD_LINEAR: 'linear', METHOD_CUBIC: 'cubic', METHOD_LANCZOS: 'lanczos', METHOD_SINC: 'sinc', /*Input clipping modes */ CLIP_CLAMP: 'clamp', CLIP_ZERO: 'zero', CLIP_PERIODIC: 'periodic', CLIP_MIRROR: 'mirror', /* Constants for control over the cubic interpolation tension */ CUBIC_TENSION_DEFAULT: 0, CUBIC_TENSION_CATMULL_ROM: 0 }; defaultConfig = { method: Enum.METHOD_CUBIC, cubicTension: Enum.CUBIC_TENSION_DEFAULT, clip: Enum.CLIP_CLAMP, scaleTo: 0, sincFilterSize: 2, sincWindow: void 0 }; /*Index clipping functions */ clipClamp = function(i, n) { return Math.max(0, Math.min(i, n - 1)); }; clipPeriodic = function(i, n) { i = i % n; if (i < 0) i += n; return i; }; clipMirror = function(i, n) { var period; period = 2 * (n - 1); i = clipPeriodic(i, period); if (i > n - 1) i = period - i; return i; }; /* Abstract scalar interpolation class which provides common functionality for all interpolators Subclasses must override interpolate(). */ AbstractInterpolator = (function() { function AbstractInterpolator(array, config) { var clipHelpers; this.array = array.slice(0); this.length = this.array.length; clipHelpers = { clamp: this.clipHelperClamp, zero: this.clipHelperZero, periodic: this.clipHelperPeriodic, mirror: this.clipHelperMirror }; this.clipHelper = clipHelpers[config.clip]; if (this.clipHelper == null) throw "Invalid clip: " + config.clip; } AbstractInterpolator.prototype.getClippedInput = function(i) { if ((0 <= i && i < this.length)) { return this.array[i]; } else { return this.clipHelper(i); } }; AbstractInterpolator.prototype.clipHelperClamp = function(i) { return this.array[clipClamp(i, this.length)]; }; AbstractInterpolator.prototype.clipHelperZero = function(i) { return 0; }; AbstractInterpolator.prototype.clipHelperPeriodic = function(i) { return this.array[clipPeriodic(i, this.length)]; }; AbstractInterpolator.prototype.clipHelperMirror = function(i) { return this.array[clipMirror(i, this.length)]; }; AbstractInterpolator.prototype.interpolate = function(t) { throw 'Subclasses of AbstractInterpolator must override the interpolate() method.'; }; return AbstractInterpolator; })(); NearestInterpolator = (function(_super) { __extends(NearestInterpolator, _super); function NearestInterpolator() { NearestInterpolator.__super__.constructor.apply(this, arguments); } NearestInterpolator.prototype.interpolate = function(t) { return this.getClippedInput(Math.round(t)); }; return NearestInterpolator; })(AbstractInterpolator); LinearInterpolator = (function(_super) { __extends(LinearInterpolator, _super); function LinearInterpolator() { LinearInterpolator.__super__.constructor.apply(this, arguments); } LinearInterpolator.prototype.interpolate = function(t) { var a, b, k; k = Math.floor(t); a = this.getClippedInput(k); b = this.getClippedInput(k + 1); t -= k; return (1 - t) * a + t * b; }; return LinearInterpolator; })(AbstractInterpolator); CubicInterpolator = (function(_super) { __extends(CubicInterpolator, _super); function CubicInterpolator(array, config) { this.tangentFactor = 1 - Math.max(0, Math.min(1, config.cubicTension)); CubicInterpolator.__super__.constructor.apply(this, arguments); } CubicInterpolator.prototype.getTangent = function(k) { return this.tangentFactor * (this.getClippedInput(k + 1) - this.getClippedInput(k - 1)) / 2; }; CubicInterpolator.prototype.interpolate = function(t) { var k, m, p, t2, t3; k = Math.floor(t); m = [this.getTangent(k), this.getTangent(k + 1)]; p = [this.getClippedInput(k), this.getClippedInput(k + 1)]; t -= k; t2 = t * t; t3 = t * t2; return (2 * t3 - 3 * t2 + 1) * p[0] + (t3 - 2 * t2 + t) * m[0] + (-2 * t3 + 3 * t2) * p[1] + (t3 - t2) * m[1]; }; return CubicInterpolator; })(AbstractInterpolator); sin = Math.sin, PI = Math.PI; sinc = function(x) { if (x === 0) { return 1; } else { return sin(PI * x) / (PI * x); } }; makeLanczosWindow = function(a) { return function(x) { return sinc(x / a); }; }; makeSincKernel = function(window) { return function(x) { return sinc(x) * window(x); }; }; SincFilterInterpolator = (function(_super) { __extends(SincFilterInterpolator, _super); function SincFilterInterpolator(array, config) { var window; SincFilterInterpolator.__super__.constructor.apply(this, arguments); this.a = config.sincFilterSize; window = config.sincWindow; if (window == null) throw 'No sincWindow provided'; this.kernel = makeSincKernel(window); } SincFilterInterpolator.prototype.interpolate = function(t) { var k, n, sum, _ref, _ref2; k = Math.floor(t); sum = 0; for (n = _ref = k - this.a + 1, _ref2 = k + this.a; _ref <= _ref2 ? n <= _ref2 : n >= _ref2; _ref <= _ref2 ? n++ : n--) { sum += this.kernel(t - n) * this.getClippedInput(n); } return sum; }; return SincFilterInterpolator; })(AbstractInterpolator); getColumn = function(arr, i) { var row, _i, _len, _results; _results = []; for (_i = 0, _len = arr.length; _i < _len; _i++) { row = arr[_i]; _results.push(row[i]); } return _results; }; makeScaledFunction = function(f, baseScale, scaleRange) { var scaleFactor, translation; if (scaleRange.join === '0,1') { return f; } else { scaleFactor = baseScale / (scaleRange[1] - scaleRange[0]); translation = scaleRange[0]; return function(t) { return f(scaleFactor * (t - translation)); }; } }; getType = function(x) { return Object.prototype.toString.call(x).slice('[object '.length, -1); }; 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'; }; validateVector = function(v, dimension) { var n, _i, _len, _results; if (getType(v) !== 'Array') throw 'Non-vector in Smooth() input'; if (v.length !== dimension) throw 'Inconsistent dimension in Smooth() input'; _results = []; for (_i = 0, _len = v.length; _i < _len; _i++) { n = v[_i]; _results.push(validateNumber(n)); } return _results; }; isValidNumber = function(n) { return (getType(n) === 'Number') && isFinite(n) && !isNaN(n); }; normalizeScaleTo = function(s) { var invalidErr; 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; }; shallowCopy = function(obj) { var copy, k, v; copy = {}; for (k in obj) { if (!__hasProp.call(obj, k)) continue; v = obj[k]; copy[k] = v; } return copy; }; Smooth = function(arr, config) { var baseScale, dataType, dimension, i, interpolator, interpolatorClass, interpolatorClasses, interpolators, k, n, scaleRange, smoothFunc, v; if (config == null) config = {}; config = shallowCopy(config); if (config.scaleTo == null) config.scaleTo = config.period; if (config.sincFilterSize == null) { config.sincFilterSize = config.lanczosFilterSize; } for (k in defaultConfig) { if (!__hasProp.call(defaultConfig, k)) continue; v = defaultConfig[k]; if (config[k] == null) config[k] = v; } interpolatorClasses = { nearest: NearestInterpolator, linear: LinearInterpolator, cubic: CubicInterpolator, lanczos: SincFilterInterpolator, sinc: SincFilterInterpolator }; interpolatorClass = interpolatorClasses[config.method]; if (interpolatorClass == null) throw "Invalid method: " + config.method; if (config.method === 'lanczos') { config.sincWindow = makeLanczosWindow(config.sincFilterSize); } if (arr.length < 2) throw 'Array must have at least two elements'; dataType = getType(arr[0]); smoothFunc = (function() { var _i, _j, _len, _len2; switch (dataType) { case 'Number': if (Smooth.deepValidation) { for (_i = 0, _len = arr.length; _i < _len; _i++) { n = arr[_i]; validateNumber(n); } } interpolator = new interpolatorClass(arr, config); return function(t) { return interpolator.interpolate(t); }; case 'Array': dimension = arr[0].length; if (!dimension) throw 'Vectors must be non-empty'; if (Smooth.deepValidation) { for (_j = 0, _len2 = arr.length; _j < _len2; _j++) { v = arr[_j]; validateVector(v, dimension); } } interpolators = (function() { var _results; _results = []; for (i = 0; 0 <= dimension ? i < dimension : i > dimension; 0 <= dimension ? i++ : i--) { _results.push(new interpolatorClass(getColumn(arr, i), config)); } return _results; })(); return function(t) { var interpolator, _k, _len3, _results; _results = []; for (_k = 0, _len3 = interpolators.length; _k < _len3; _k++) { interpolator = interpolators[_k]; _results.push(interpolator.interpolate(t)); } return _results; }; default: throw "Invalid element type: " + dataType; } })(); if (config.scaleTo) { scaleRange = normalizeScaleTo(config.scaleTo); if (config.clip === Smooth.CLIP_PERIODIC) { baseScale = arr.length; } else { baseScale = arr.length - 1; } smoothFunc = makeScaledFunction(smoothFunc, baseScale, scaleRange); } return smoothFunc; }; for (k in Enum) { if (!__hasProp.call(Enum, k)) continue; v = Enum[k]; Smooth[k] = v; } Smooth.deepValidation = true; root = typeof exports !== "undefined" && exports !== null ? exports : window; root.Smooth = Smooth; }).call(this);