Repository: osuushi/Smooth.js Branch: master Commit: 62233300e50f Files: 22 Total size: 36.7 KB Directory structure: gitextract_dud26c8f/ ├── ChangeLog.md ├── Readme.md ├── Smooth.coffee ├── Smooth.js MIT license.txt ├── bower.json ├── sindemo.coffee └── test/ ├── specs/ │ ├── clip-clamp.spec.coffee │ ├── clip-mirror.spec.coffee │ ├── clip-perodic.spec.coffee │ ├── clip-zero.spec.coffee │ ├── cubic.spec.coffee │ ├── exceptions.spec.coffee │ ├── lanczos.spec.coffee │ ├── linear.spec.coffee │ ├── misc.spec.coffee │ ├── nearest.spec.coffee │ ├── properties.spec.coffee │ ├── scale.spec.coffee │ ├── sinc.spec.coffee │ ├── util.coffee │ └── vector.spec.coffee └── tests.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: ChangeLog.md ================================================ #Smooth.js Change Log This is a log of changes to the library itself. Other changes, like tests, are omitted. ##0.1.7 * Added properties to smoothed functions: `config`, `domain`, `count`, and `dimension`. * Made code more concise. ##0.1.6 * Fixed bug where `Smooth()` would modify the config object passed to it, rather than working on a copy ##0.1.5 * Lanczos interpolation. See [Lanczos resampling](http://en.wikipedia.org/wiki/Lanczos_resampling). * Windowed sinc filter interpolation. See [sinc filter](http://en.wikipedia.org/wiki/Sinc_filter). * Deep input validation; input arrays are now thoroughly examined when calling `Smooth()`. Disable with `Smooth.deepValidation = false`. * `scaleTo` with ranges; you can now scale the function's domain to fit to a specific range. ##0.1.3 * New `scaleTo` config option scales the output function's domain. * Changed enums to string constants. For example, you can use either `Smooth.METHOD_CUBIC` or just `'cubic'`. ##0.1.2 * Fixed tension parameter bug in cubic splines. Catmull-Rom splines are now the default. ##0.1.0 (Initial release) ================================================ FILE: Readme.md ================================================ ![Smooth.js](images/logo-white.png) #### Table of Contents [        What is this for?](#rm-what)
[        How do I use it?](#rm-how)
[                Configuration](#rm-config)
[                        Interpolation Methods](#rm-method)
[                        Clipping Modes](#rm-clip)
[                        Scaling](#rm-scale)
[                        Validation](#rm-valid)
[                Interpolating Vectors](#rm-vec)
[                Function Properties](#rm-prop)
[        Future Plans](#rm-future)
# What is this for? Smooth.js takes an array of numbers or vectors and returns a parametric function that continuously interpolates that array. Smooth.js supports several interpolation methods, and flexible options for boundary behavior. Smooth.js is written in clean, easy-to-read CoffeeScript, and has no external dependencies. It is licensed under the permissive MIT license, so you can use it in just about any project. This [demo](http://osuushi.github.com/plotdemo016.html) (requires a modern browser) gives a visualization of the interpolation Smooth.js performs. # How do I use it? You can compile to javascript from the Smooth.coffee source file, or [download the latest compiled release](https://github.com/downloads/osuushi/Smooth.js/Smooth-0.1.7.js) Smooth.js exposes one public function, `Smooth`. The simplest use case is like this: ```js var s = Smooth([1, 2, 3, 4]); console.log(s(1)); // => 2 console.log(s(1.5)); // => 2.5 ``` The first line will make `s` a function that interpolates the array [1,2,3,4] as a cubic spline. the second line will print out index 1 of the array, which is 2. The third line *interpolates* halfway between indexes 1 and 2 of the array, yielding 2.5 ## Configuration The `Smooth` function can take an object as an optional second argument which specifies the configuration options described below. ### Interpolation Methods         (For visual illustrations of these interpolation methods see [the wiki](https://github.com/osuushi/Smooth.js/wiki/Interpolation-Methods)) The `method` config option specifies the interpolation method. There are three possible values for this option: #### Nearest Neighbor ```js Smooth.METHOD_NEAREST = 'nearest' ``` This interpolation method is like stair steps. The parameter is simply rounded to the nearest integer and that element of the array is returned. Time complexity to interpolate a point: O(1) #### Linear ```js Smooth.METHOD_LINEAR = 'linear' ``` Linear interpolation creates line segments between the input points and interpolates along those segments. While smoother than nearest neighbor, this interpolation method produces sharp corners where the parameter is an integer. Time complexity to interpolate a point: O(1) #### Cubic ```js Smooth.METHOD_CUBIC = 'cubic' ``` This is the default interpolation method, which turns the array into a [cubic Hermite spline](http://en.wikipedia.org/wiki/Cubic_Hermite_spline). This method is very smooth and will not produce sharp corners. The cubic Hermite spline used by Smooth.js is known as a [cardinal spline](http://en.wikipedia.org/wiki/Cubic_hermite_spline#Cardinal_spline). This kind of spline allows you to choose a "tension" parameter as the `cubicTension` field of the config object. Two constants are provided for this value: `Smooth.CUBIC_TENSION_DEFAULT` and `Smooth.CUBIC_TENSION_CATMULL_ROM`, but you can use any value between 0 and 1. `Smooth.CUBIC_TENSION_CATMULL_ROM` produces a [Catmull-Rom spline](http://en.wikipedia.org/wiki/Cubic_hermite_spline#Catmull.E2.80.93Rom_spline), which is commonly used for inbetweening keyframe animations. It is equal to a tension parameter of zero. `Smooth.CUBIC_TENSION_DEFAULT` is an alias for `CUBIC_TENSION_CATMULL_ROM`. Time complexity to interpolate a point: O(1) #### Windowed sinc filter ```js Smooth.METHOD_SINC = 'sinc' ``` Interpolate by applying a windowed version of the [sinc filter](http://en.wikipedia.org/wiki/Sinc_filter). You can specify the size of the window with the `sincFilterSize` config parameter. The window will extend by this value in either direction from the origin. This value must be a positive integer. The default is 2. You must also provide a window function via the `sincWindow` configuration option. This function should take one numeric parameter and return a numeric value. For example: ```js var s = Smooth([1, 2, 3], { method: 'sinc', sincFilterSize: 2 sincWindow: function(x) { return Math.exp(-x * x); } }); ``` will create a sinc filter with a Gaussian window function. The window function is implicitly further multiplied by a rectangular window determined by sincFilterSize, so ```js sincWindow: function(x) { return 1; } ``` will create a sinc filter with a simple rectangular window function. Time complexity to interpolate a point: O(N), where N = `sincFilterSize` (assuming your window function is O(1)) #### Lanczos ```js Smooth.METHOD_LANCZOS = 'lanczos' ``` Interpolate via [Lanczos resampling](http://en.wikipedia.org/wiki/Lanczos_resampling). Convolves the input array by a Lanczos kernel to produce intermediate points. The size of the Lanczos kernel can be specified via the `lanczosFilterSize` config parameter (default = 2). This parameter should be a positive integer. **Note:** This filter is actually a specific case of the sinc filter. The `lanczosFilterSize` config option is an alias for `sincFilterSize`, and the Lanczos window function is automatically created for you based on this parameter. Time complexity to interpolate a point: O(N), where N = `lanczosFilterSize` ### Clipping modes In addition to interpolating an array, Smooth.js allows you to specify the behavior of the output function when the parameter is outside the array's bounds. This also has an effect on cubic and sinc interpolation when interpolating near the array's bounds. The `clip` config option specifies the clipping mode, and can take the following values: #### Clamp ```js Smooth.CLIP_CLAMP = 'clamp' ``` The default clipping mode; the ends of the array are simply repeated to infinity. #### Zero ```js Smooth.CLIP_ZERO = 'zero' ``` Outside the array bounds, the value drops to zero. #### Periodic ```js Smooth.CLIP_PERIODIC = 'periodic' ``` The whole array repeats infinitely in both directions. This is useful, for example, if you want values for a looping animation. #### Mirror ```js Smooth.CLIP_MIRROR = 'mirror' ``` Repeats the array infinitely in both directions, reflecting each time. For example, if you applied this to `[1, 2, 3, 4]` then the result would be `[1, 2, 3, 4, 3, 2, 1, 2, 3, 4...]`. Useful for "loop back and forth" style animations, for example. ### Scaling The `scaleTo` config option allows you to scale the domain of the function. The default value is 0, which tells Smooth.js to leave the domain like the original array, so that for any integer `i`, `s(i) == arr[i]`. Setting the `scaleTo` option to non-zero will scale the domain to that value. For example: ```js var s = Smooth([1, 2, 3], { scaleTo: 1 }); console.log(s(0)); // => 1 console.log(s(1 / 2)); // => 2 console.log(s(1)); // => 3 ``` You can also provide a range for the `scaleTo` option, as an array of two numbers. This will scale the function to fit in that range. For example ```js var s = Smooth([1, 2, 3], { scaleTo: [10, 12] }); console.log(s(10)); // => 1 console.log(s(12)); // => 2 console.log(s(14)); // => 3 ``` When using `Smooth.CLIP_PERIODIC`, the behavior of the `scaleTo` option is slightly different; instead of scaling to place the end of the array at the value of `scaleTo`, the value is used as the *period* of the function. For the sake of readability, the `period` config option is aliased to `scaleTo`. Thus: ```js var s = Smooth([1, 2, 3], { period: 1, clip:Smooth.CLIP_PERIODIC }); console.log(s(0)); // => 1 console.log(s(1 / 3)); // => 2 console.log(s(2 / 3)); // => 3 console.log(s(1)); // => 1 ``` ### Validation By default the input array you pass to `Smooth` will be examined thoroughly to make sure that the input is valid, and exceptions will be thrown if any problems are found. This can be a performance consideration if you are dealing with large amounts of data. This deep validation behavior can be disabled globally like so: ```js Smooth.deepValidation = false; ``` This will cause the Smooth function to only validate the first element of each array, and only minimally. ## Interpolating Vectors So far all of the example code we've seen has used scalar arrays, but Smooth.js supports interpolation of vectors of arbitrary dimension. Simply supply the vectors as arrays. For example, this code: ```js var points = [ [0, 1], [4, 5], [5, 3], [2, 0] ]; var path = Smooth(points, { method: Smooth.METHOD_CUBIC, clip: Smooth.CLIP_PERIODIC, cubicTension: Smooth.CUBIC_TENSION_CATMULL_ROM }); ``` could be used to create a path function along which to animate a sprite in a loop. ## Function Properties The function returned by `Smooth()` has a few properties which provide information about it. **Changing these properties has no effect on the function.** `s.config` : a shallow copy of the config object you provided when creating the function. If you did not provide a config object, `s.config` will be an empty object. Note that this is a *shallow* copy, so any modifications you make to object properties of the config will be reflected by `s.config`, although the behavior of `s` itself will not be affected. `s.domain` : The interval on which the function is defined. Outside of this interval, the function's behavior is determined by the clipping mode. This property is affected by the `scaleTo` parameter. `s.count` : The number of elements in the input array. `s.dimension` : If the input array contains scalar numbers, `s.dimension` will be `'scalar'`. If the input array contains vectors, `s.dimension` will be the vector size. # Future Plans * Interpolation of non-uniform arrays (objects with arbitrary numeric indexes) * More interpolation methods * Custom interpolation methods (maybe) ================================================ FILE: Smooth.coffee ================================================ ### 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)### 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 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### clipClamp = (i, n) -> Math.max 0, Math.min i, n - 1 clipPeriodic = (i, n) -> i = i % n #wrap i += n if i < 0 #if negative, wrap back around i clipMirror = (i, n) -> period = 2*(n - 1) #period of index mirroring function i = clipPeriodic i, period i = period - i if i > n - 1 #flip when out of bounds i ### Abstract scalar interpolation class which provides common functionality for all interpolators Subclasses must override interpolate(). ### class AbstractInterpolator constructor: (array, config) -> @array = array.slice 0 #copy the array @length = @array.length #cache length #Set the clipping helper method throw "Invalid clip: #{config.clip}" unless @clipHelper = { clamp: @clipHelperClamp zero: @clipHelperZero periodic: @clipHelperPeriodic mirror: @clipHelperMirror }[config.clip] # Get input array value at i, applying the clipping method getClippedInput: (i) -> #Normal behavior for indexes within bounds if 0 <= i < @length @array[i] else @clipHelper i clipHelperClamp: (i) -> @array[clipClamp i, @length] clipHelperZero: (i) -> 0 clipHelperPeriodic: (i) -> @array[clipPeriodic i, @length] clipHelperMirror: (i) -> @array[clipMirror i, @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) -> @getClippedInput Math.round t #Linear interpolator (first order Bezier) class LinearInterpolator extends AbstractInterpolator interpolate: (t) -> k = Math.floor t #Translate t to interpolate between k and k+1 t -= k return (1-t)*@getClippedInput(k) + (t)*@getClippedInput(k+1) class CubicInterpolator extends AbstractInterpolator constructor: (array, config)-> #clamp cubic tension to [0,1] range @tangentFactor = 1 - Math.max 0, Math.min 1, config.cubicTension super # Cardinal spline with tension 0.5) getTangent: (k) -> @tangentFactor*(@getClippedInput(k + 1) - @getClippedInput(k - 1))/2 interpolate: (t) -> k = Math.floor t m = [(@getTangent k), (@getTangent k+1)] #get tangents p = [(@getClippedInput k), (@getClippedInput k+1)] #get points #Translate t to interpolate between k and k+1 t -= k t2 = t*t #t^2 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] {sin, PI} = Math #Normalized sinc function sinc = (x) -> if x is 0 then 1 else sin(PI*x)/(PI*x) #Make a lanczos window function for a given filter size 'a' makeLanczosWindow = (a) -> (x) -> sinc(x/a) #Make a sinc kernel function by multiplying the sinc function by a window function makeSincKernel = (window) -> (x) -> sinc(x)*window(x) class SincFilterInterpolator extends AbstractInterpolator constructor: (array, config) -> super #Create the lanczos kernel function @a = config.sincFilterSize #Cannot make sinc filter without a window function throw 'No sincWindow provided' unless config.sincWindow #Window the sinc function to make the kernel @kernel = makeSincKernel config.sincWindow interpolate: (t) -> k = Math.floor t #Convolve with Lanczos kernel sum = 0 sum += @kernel(t - n)*@getClippedInput(n) for n in [(k - @a + 1)..(k + @a)] sum #Extract a column from a two dimensional array getColumn = (arr, i) -> (row[i] for row in arr) #Take a function with one parameter and apply a scale factor to its parameter makeScaledFunction = (f, baseScale, scaleRange) -> if scaleRange.join is '0,1' f #don't wrap the function unecessarily else scaleFactor = baseScale/(scaleRange[1] - scaleRange[0]) translation = scaleRange[0] (t) -> f scaleFactor*(t - translation) getType = (x) -> Object::toString.call(x)[('[object '.length)...-1] #Throw exception if input is not a number validateNumber = (n) -> throw 'NaN in Smooth() input' if isNaN n throw 'Non-number in Smooth() input' unless getType(n) is 'Number' throw 'Infinity in Smooth() input' unless isFinite n #Throw an exception if input is not a vector of numbers which is the correct length validateVector = (v, dimension) -> throw 'Non-vector in Smooth() input' unless getType(v) is 'Array' throw 'Inconsistent dimension in Smooth() input' unless v.length is dimension validateNumber n for n in v return isValidNumber = (n) -> (getType(n) is 'Number') and isFinite(n) and not isNaN(n) normalizeScaleTo = (s) -> invalidErr = "scaleTo param must be number or array of two numbers" switch getType s when 'Number' throw invalidErr unless isValidNumber s s = [0, s] when 'Array' throw invalidErr unless s.length is 2 throw invalidErr unless isValidNumber(s[0]) and isValidNumber(s[1]) else throw invalidErr return s shallowCopy = (obj) -> copy = {} copy[k] = v for own k,v of obj copy Smooth = (arr, config = {}) -> #Properties to copy to the function once it is created 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' config.scaleTo ?= config.period #Alias lanczosFilterSize to sincFilterSize config.sincFilterSize ?= config.lanczosFilterSize config[k] ?= v for own k,v of defaultConfig #fill in defaults #Get the interpolator class according to the configuration throw "Invalid method: #{config.method}" unless interpolatorClass = { nearest: NearestInterpolator linear: LinearInterpolator cubic: CubicInterpolator lanczos: SincFilterInterpolator #lanczos is a specific case of sinc filter sinc: SincFilterInterpolator }[config.method] if config.method is 'lanczos' #Setup lanczos window config.sincWindow = makeLanczosWindow config.sincFilterSize #Make sure there's at least one element in the input array throw 'Array must have at least two elements' if arr.length < 2 #save count property properties.count = arr.length #See what type of data we're dealing with smoothFunc = switch getType arr[0] when 'Number' #scalar properties.dimension = 'scalar' #Validate all input if deep validation is on validateNumber n for n in arr if Smooth.deepValidation #Create the interpolator interpolator = new interpolatorClass arr, config #make function that runs the interpolator (t) -> interpolator.interpolate t when 'Array' # vector properties.dimension = dimension = arr[0].length throw 'Vectors must be non-empty' unless dimension #Validate all input if deep validation is on validateVector v, dimension for v in arr if Smooth.deepValidation #Create interpolator for each column interpolators = (new interpolatorClass(getColumn(arr, i), config) for i in [0...dimension]) #make function that runs the interpolators and puts them into an array (t) -> (interpolator.interpolate(t) for interpolator in interpolators) else throw "Invalid element type: #{getType arr[0]}" # Determine the end of the original function's domain if config.clip is 'periodic' then baseDomainEnd = arr.length #after last element for periodic else baseDomainEnd = arr.length - 1 #at last element for non-periodic 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### smoothFunc[k] = v for own k,v of properties return smoothFunc #Copy enums to Smooth Smooth[k] = v for own k,v of Enum Smooth.deepValidation = true (exports ? window).Smooth = Smooth ================================================ FILE: Smooth.js MIT license.txt ================================================ Smooth.js Copyright (c) 2012 Spencer Cohen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: bower.json ================================================ { "name": "smooth.js", "homepage": "https://github.com/osuushi/Smooth.js", "authors": [ "yournamehere " ], "description": "Smooth.js is an array interpolation library for Javascript", "main": "", "moduleType": [], "keywords": [ "interpolation", "javascript", "smoothjs" ], "license": "MIT", "ignore": [ "**/.*", "node_modules", "bower_components", "test", "tests" ] } ================================================ FILE: sindemo.coffee ================================================ ### sindemo Sample and cubic interpolate the sin function, then print out max and average error. ### {Smooth} = require './Smooth' s = (Math.sin 2*Math.PI*x for x in [0...1] by 1/8) #Scale the function smooth_sin = ((f) -> scaleVal = 0.5*s.length/Math.PI return (x) -> f x*scaleVal ) Smooth s, method:Smooth.METHOD_CUBIC, clip:Smooth.CLIP_PERIODIC totalError = 0 count = 0 maxError = 0 for x in [-10..10] by .001 error = Math.abs Math.sin(x) - smooth_sin(x) maxError = Math.max error, maxError totalError += error count++ console.log "Max Error:\t #{(100*maxError).toFixed(10)}%" console.log "Average Error:\t #{(100*totalError/count).toFixed(10)}%" ================================================ FILE: test/specs/clip-clamp.spec.coffee ================================================ {Smooth} = require '../../Smooth.coffee' describe "Clip Clamp", -> arr = [1,2,3,4] len = arr.length s = Smooth arr, clip:Smooth.CLIP_CLAMP it 'should extend first value to negative infinity', -> expect(s -1).toEqual arr[0] expect(s -5.8).toEqual arr[0] expect(s -100.3).toEqual arr[0] it 'should extend last value to positive infinity', -> expect(s len+1).toEqual arr[len-1] expect(s len+12.2).toEqual arr[len-1] expect(s len+1000).toEqual arr[len-1] it 'should leave in-bounds values untouched', -> expect(s 0).toEqual arr[0] expect(s 1).toEqual arr[1] expect(s 2).toEqual arr[2] ================================================ FILE: test/specs/clip-mirror.spec.coffee ================================================ {Smooth} = require '../../Smooth.coffee' describe "Clip Mirror", -> arr = [1,2,3,4] len = arr.length s = Smooth arr, clip:Smooth.CLIP_MIRROR it 'should reflect across the origin', -> expect(s -1).toEqual arr[1] expect(s -2).toEqual arr[2] expect(s -3).toEqual arr[3] it 'should produce a predictable pattern', -> mirrorArray = arr.concat (arr[i] for i in [len-2...0]) #create pattern to repeat expect( (s i for i in [0...20]).join() ) .toBe (mirrorArray[i%mirrorArray.length] for i in [0...20]).join() it 'should leave in-bounds values untouched', -> expect(s 0).toEqual arr[0] expect(s 1).toEqual arr[1] expect(s 3).toEqual arr[3] ================================================ FILE: test/specs/clip-perodic.spec.coffee ================================================ {Smooth} = require '../../Smooth.coffee' describe "Clip Periodic", -> arr = [1,2,3,4] len = arr.length s = Smooth arr, clip:Smooth.CLIP_PERIODIC it 'should repeat the same value for a shift by any integer multiple of the array length', -> expect(s 0 - 98*len).toEqual arr[0] expect(s 1 + 12*len).toEqual arr[1] expect(s 2 - 13*len).toEqual arr[2] expect(s 3 + 47*len).toEqual arr[3] it 'should leave in-bounds values untouched', -> expect(s 0).toEqual arr[0] expect(s 1).toEqual arr[1] expect(s 3).toEqual arr[3] ================================================ FILE: test/specs/clip-zero.spec.coffee ================================================ {Smooth} = require '../../Smooth.coffee' describe "Clip Zero", -> arr = [1,2,3,4] len = arr.length s = Smooth arr, clip:Smooth.CLIP_ZERO it 'should be zero when out of bounds', -> expect(s -1).toEqual 0 expect(s -100.3).toEqual 0 expect(s len+1).toEqual 0 expect(s len+12.2).toEqual 0 expect(s len+1000).toEqual 0 it 'should leave in-bounds values untouched', -> expect(s 0).toEqual arr[0] expect(s 1).toEqual arr[1] expect(s 2).toEqual arr[2] ================================================ FILE: test/specs/cubic.spec.coffee ================================================ {Smooth} = require '../../Smooth.coffee' {deriv} = require './util.coffee' describe 'Cubic Interpolator', -> describe 'Catmull-Rom', -> arr = [4,6,-2,3] s = Smooth arr, method:Smooth.METHOD_CUBIC, cubicTension: Smooth.CATMULL_ROM it 'should match integer indexes', -> expect(s 0).toEqual arr[0] expect(s 1).toEqual arr[1] expect(s 2).toEqual arr[2] expect(s 3).toEqual arr[3] it 'should have neighbor-slope tangent at integers', -> expect(deriv(s) 1).toBeCloseTo (arr[2]-arr[0])/2 expect(deriv(s) 2).toBeCloseTo (arr[3]-arr[1])/2 it 'should repeat when periodic', -> p = Smooth arr, method:'cubic', cubicTension: Smooth.CATMULL_ROM, clip:'periodic', scaleTo: 1 for i in [-2..2] by 1/16 expect(p i).toEqual p i - Math.floor i describe 'Tension=1', -> arr = [8,2,-4,9] s = Smooth arr, method:Smooth.METHOD_CUBIC, cubicTension: 1 it 'should match integer indexes', -> expect(s 0).toEqual arr[0] expect(s 1).toEqual arr[1] expect(s 2).toEqual arr[2] expect(s 3).toEqual arr[3] it 'should have zero derivatives at integers', -> expect(deriv(s) 0).toBeCloseTo 0 expect(deriv(s) 1).toBeCloseTo 0 expect(deriv(s) 2).toBeCloseTo 0 expect(deriv(s) 3).toBeCloseTo 0 ================================================ FILE: test/specs/exceptions.spec.coffee ================================================ {Smooth} = require '../../Smooth.coffee' describe 'Exceptions', -> it 'should throw for invalid methods', -> expect(-> Smooth [1,2], method: 'lanscoz').toThrow "Invalid method: lanscoz" expect(-> Smooth [1,2], method: 'Cubic').toThrow "Invalid method: Cubic" it 'should throw for invalid clipping mode', -> expect(-> Smooth [1,2], clip: 'mirorr').toThrow "Invalid clip: mirorr" expect(-> Smooth [1,2], clip: 'Linear').toThrow "Invalid clip: Linear" it 'should throw for invalid arrays', -> expect(-> Smooth []).toThrow 'Array must have at least two elements' expect(-> Smooth [0]).toThrow 'Array must have at least two elements' it 'should throw for bad input', -> expect(-> Smooth ['a','b']).toThrow 'Invalid element type: String' expect(-> Smooth [(->), (->)]).toThrow 'Invalid element type: Function' expect(-> Smooth [[], []]).toThrow 'Vectors must be non-empty' it 'should throw for sinc filter with no window', -> expect(-> Smooth [1,2], method:'sinc').toThrow 'No sincWindow provided' it 'should throw for invalid scaleTo', -> expect(-> Smooth [1,2], scaleTo:[1]).toThrow 'scaleTo param must be number or array of two numbers' expect(-> Smooth [1,2], scaleTo:[1,2,3]).toThrow 'scaleTo param must be number or array of two numbers' expect(-> Smooth [1,2], scaleTo:'a').toThrow 'scaleTo param must be number or array of two numbers' expect(-> Smooth [1,2], scaleTo:[1,'x']).toThrow 'scaleTo param must be number or array of two numbers' expect(-> Smooth [1,2], scaleTo:Infinity).toThrow 'scaleTo param must be number or array of two numbers' describe 'With deep validation on...', -> it 'should throw for bad input deep inside the input', -> Smooth.deepValidation = true expect(-> Smooth [1,2,'a']).toThrow 'NaN in Smooth() input' expect(-> Smooth [1,2,'3']).toThrow 'Non-number in Smooth() input' expect(-> Smooth [1,2, Infinity]).toThrow 'Infinity in Smooth() input' expect(-> Smooth [[1],[2],['a']]).toThrow 'NaN in Smooth() input' expect(-> Smooth [[1],[2],['3']]).toThrow 'Non-number in Smooth() input' expect(-> Smooth [[1],[2], [Infinity]]).toThrow 'Infinity in Smooth() input' expect(-> Smooth [[1], 1]).toThrow 'Non-vector in Smooth() input' expect(-> Smooth [[1], [1,2]]).toThrow 'Inconsistent dimension in Smooth() input' describe 'With deep validation off...', -> it 'should not throw for bad input deep inside the input', -> Smooth.deepValidation = false expect(Smooth [1,2,'a']).toBeTruthy() expect(Smooth [1,2,'3']).toBeTruthy() expect(Smooth [1,2, Infinity]).toBeTruthy() expect(Smooth [[1],[2],['a']]).toBeTruthy() expect(Smooth [[1],[2],['3']]).toBeTruthy() expect(Smooth [[1],[2], [Infinity]]).toBeTruthy() expect(Smooth [[1], 1]).toBeTruthy() expect(Smooth [[1], [1,2]]).toBeTruthy() ================================================ FILE: test/specs/lanczos.spec.coffee ================================================ {Smooth} = require '../../Smooth.coffee' {deriv} = require './util.coffee' describe 'Lanczos Interpolator', -> arr = [1,3,-2,8] s = Smooth arr, method: 'lanczos' it 'should be close to array at integer indexes', -> for i in [0...arr.length] expect(s i).toBeCloseTo arr[i] it 'should be continuous everywhere', -> delta = 0.00001 for i in [-1..arr.length] by 1/64 expect(s i).toBeCloseTo s(i-delta), 0 it 'should be differentiable everywhere', -> delta = 0.00001 ds = deriv s for i in [-1..arr.length] by 1/64 expect(ds i).toBeCloseTo ds(i-delta), 0 it 'should repeat when periodic', -> p = Smooth arr, method:'lanczos', clip: 'periodic', scaleTo: 1 for i in [-2..2] by 1/16 expect(p i).toEqual p i - Math.floor i ================================================ FILE: test/specs/linear.spec.coffee ================================================ {Smooth} = require '../../Smooth.coffee' {deriv} = require './util.coffee' describe 'Linear Interpolator', -> arr = [1,4,3,8] s = Smooth arr, method:Smooth.METHOD_LINEAR it 'should match integer indexes', -> expect(s 0).toEqual arr[0] expect(s 1).toEqual arr[1] expect(s 2).toEqual arr[2] expect(s 3).toEqual arr[3] it 'should have arithmetic mean for midpoints', -> expect(s 0.5).toBeCloseTo (arr[0]+arr[1])/2 expect(s 1.5).toBeCloseTo (arr[1]+arr[2])/2 expect(s 2.5).toBeCloseTo (arr[2]+arr[3])/2 it 'should have derivatives equal to point differences', -> expect(deriv(s) 0.2).toBeCloseTo arr[1] - arr[0] expect(deriv(s) 0.8).toBeCloseTo arr[1] - arr[0] expect(deriv(s) 1.4).toBeCloseTo arr[2] - arr[1] expect(deriv(s) 2.7).toBeCloseTo arr[3] - arr[2] it 'should repeat when periodic', -> p = Smooth arr, method:'linear', clip: 'periodic', scaleTo: 1 for i in [-2..2] by 1/16 expect(p i).toEqual p i - Math.floor i ================================================ FILE: test/specs/misc.spec.coffee ================================================ {Smooth} = require '../../Smooth.coffee' util = require './util' describe "Misc...", -> it 'should not modify the original config object', -> config = lanczosFilterSize: 2 configCopy = util.shallowCopy config s = Smooth [1,2,3], config #check object equality expect(config).toEqual configCopy ================================================ FILE: test/specs/nearest.spec.coffee ================================================ {Smooth} = require '../../Smooth.coffee' {deriv} = require './util.coffee' describe 'Nearest Neighbor Interpolator', -> arr = [1,2,3,4] s = Smooth arr, method:Smooth.METHOD_NEAREST it 'should match integer indexes', -> expect(s 0).toEqual arr[0] expect(s 1).toEqual arr[1] expect(s 2).toEqual arr[2] expect(s 3).toEqual arr[3] it 'should round fractional parameter', -> expect(s 0.1).toEqual arr[0] expect(s 1.9).toEqual arr[2] expect(s 2.2).toEqual arr[2] expect(s 2.8).toEqual arr[3] it 'should have zero derivatives where fraction != .5', -> expect(deriv(s) 1).toBeCloseTo 0 expect(deriv(s) 1.1).toBeCloseTo 0 expect(deriv(s) 1.9).toBeCloseTo 0 expect(deriv(s) 3.2).toBeCloseTo 0 it 'should repeat when periodic', -> p = Smooth arr, method:'nearest', clip: 'periodic', scaleTo: 1 for i in [-2..2] by 1/16 expect(p i).toEqual p i - Math.floor i ================================================ FILE: test/specs/properties.spec.coffee ================================================ {Smooth} = require '../../Smooth.coffee' util = require './util' describe "Smooth function properties...", -> it 'should save a shallow copy of the config passed in by the user', -> config = cubicTension: 1, method: 'cubic', clip: 'zero' s = Smooth [1,2,3], config expect(s.config).toEqual config it 'should save the domain correctly', -> expect((Smooth [1,1,1]).domain).toEqual [0, 2] expect((Smooth [1,1,1,1], clip:'periodic').domain).toEqual [0, 4] expect((Smooth [1,1,1], scaleTo:2).domain).toEqual [0, 2] expect((Smooth [1,1,1], scaleTo:[1,5]).domain).toEqual [1, 5] expect((Smooth [1,1,1], clip:'periodic', scaleTo:[1,5]).domain).toEqual [1, 5] expect((Smooth [1,1,1], scaleTo:[5,1]).domain).toEqual [1, 5] it 'should save the count correctly', -> expect(Smooth([1,1,3]).count).toBe 3 expect(Smooth([1,2,3], scaleTo:5).count).toBe 3 it 'should save the dimension correctly', -> expect(Smooth([1,2,3]).dimension).toBe 'scalar' expect(Smooth([[1],[2],[3]]).dimension).toBe 1 expect(Smooth([[1,2],[2,3],[3,4]]).dimension).toBe 2 ================================================ FILE: test/specs/scale.spec.coffee ================================================ {Smooth} = require '../../Smooth.coffee' describe "Scale to...", -> arr = [1,2,3,4] it 'should scale to [0,1]', -> s = Smooth arr, scaleTo: 1 expect(s 0).toBeCloseTo arr[0] expect(s 1/3).toBeCloseTo arr[1] expect(s 2/3).toBeCloseTo arr[2] expect(s 1).toBeCloseTo arr[3] it 'should scale to [0, length-1] with no change from unscaled', -> s_noscale = Smooth arr s_scale = Smooth arr, scaleTo: arr.length-1 expect(s_scale 0).toBeCloseTo s_noscale 0 expect(s_scale 2).toBeCloseTo s_noscale 2 expect(s_scale 2.5).toBeCloseTo s_noscale 2.5 expect(s_scale 3).toBeCloseTo s_noscale 3 it 'should scale to [0, 9]', -> s = Smooth arr, scaleTo: 9 expect(s 0).toBeCloseTo arr[0] expect(s 3).toBeCloseTo arr[1] expect(s 6).toBeCloseTo arr[2] expect(s 9).toBeCloseTo arr[3] it 'should reflect when scaling to -(length-1)', -> s = Smooth arr, scaleTo: -(arr.length - 1) expect(s 0).toBeCloseTo arr[0] expect(s -1).toBeCloseTo arr[1] expect(s -2).toBeCloseTo arr[2] expect(s -3).toBeCloseTo arr[3] it 'should scale to the next cycle for periodic functions', -> s = Smooth arr, scaleTo: 1, clip:Smooth.CLIP_PERIODIC expect(s 0).toBeCloseTo s 1 expect(s 0.5).toBeCloseTo s 2.5 expect(s 3.8).toBeCloseTo s -9.2 describe 'range scaling', -> it 'should scale to [-1, 1]', -> s = Smooth arr, scaleTo: [-1, 1] expect(s -1).toBeCloseTo arr[0] expect(s -1/3).toBeCloseTo arr[1] expect(s 1/3).toBeCloseTo arr[2] expect(s 1).toBeCloseTo arr[3] it 'should scale periodic to [0.5, 1.5]', -> s = Smooth arr, scaleTo: [0.5, 1.5], clip:'periodic' expect(s 0).toBeCloseTo s 1 expect(s 0.5).toBeCloseTo s 2.5 expect(s 3.8).toBeCloseTo s -9.2 ================================================ FILE: test/specs/sinc.spec.coffee ================================================ {Smooth} = require '../../Smooth.coffee' {deriv} = require './util.coffee' describe 'Sinc Filter Interpolator', -> arr = [1,3,-2,8] describe 'Gaussian window', -> s = Smooth arr, method: 'sinc', sincWindow: (x) -> Math.exp -x*x it 'should be close to array at integer indexes', -> for i in [0...arr.length] expect(s i).toBeCloseTo arr[i] it 'should be continuous everywhere', -> delta = 0.00001 for i in [-1..arr.length] by 1/64 expect(s i).toBeCloseTo s(i-delta), 0 it 'should be differentiable everywhere', -> delta = 0.00001 ds = deriv s for i in [-1..arr.length] by 1/64 expect(ds i).toBeCloseTo ds(i-delta), 0 it 'should repeat when periodic', -> p = Smooth arr, method: 'sinc', scaleTo: 1, clip: 'periodic', sincWindow: (x) -> Math.exp -x*x for i in [-2..2] by 1/16 expect(p i).toEqual p i - Math.floor i describe 'Circular window', -> s = Smooth arr, method: 'sinc', sincWindow: (x) -> Math.sqrt(1 - x*x/4) it 'should be close to array at integer indexes', -> for i in [0...arr.length] expect(s i).toBeCloseTo arr[i] it 'should be continuous everywhere', -> delta = 0.00001 for i in [-1..arr.length] by 1/64 expect(s i).toBeCloseTo s(i-delta), 0 it 'should be differentiable everywhere', -> delta = 0.00001 ds = deriv s for i in [-1..arr.length] by 1/64 expect(ds i).toBeCloseTo ds(i-delta), 0 it 'should repeat when periodic', -> p = Smooth arr, method: 'sinc', scaleTo: 1, clip: 'periodic', sincWindow: (x) -> Math.sqrt(1 - x*x/4) for i in [-2..2] by 1/16 expect(p i).toEqual p i - Math.floor i ================================================ FILE: test/specs/util.coffee ================================================ ### Helper code for tests ### #Approximate derivative function with some delta value exports.deriv = (f, delta = 0.0001) -> (t) -> ((f t+delta) - (f t))/delta exports.distance = (a, b) -> sqDist = 0 l = a.length sqDist += Math.pow a[i]-b[i], 2 for i in [0...l] return Math.sqrt sqDist exports.shallowCopy = (obj) -> copy = {} copy[k] = v for own k,v of obj copy ================================================ FILE: test/specs/vector.spec.coffee ================================================ {Smooth} = require '../../Smooth.coffee' {distance} = require './util.coffee' describe 'Vector', -> it 'should approximate a unit circle', -> {sin, cos, PI} = Math circle_points = ([cos(PI*t), sin(PI*t)] for t in [0...2] by 1/6) #Make into a function circle = Smooth circle_points, period:1, clip:Smooth.CLIP_PERIODIC #Integrate arc length l = 0 start = [1,0] for t in [0..1] by .0001 end = circle t l += distance start, end start = end #Result length should be approximately 2*pi expect(l).toBeCloseTo 2*Math.PI ================================================ FILE: test/tests.sh ================================================ #!/usr/bin/env bash # (requires jasmine-node) jasmine-node --coffee --verbose `dirname $0`/specs