curve-store
Advanced tools
| // Import libraries | ||
| import { createStore } from '../src'; | ||
| import { getPointAfter, getPointBefore } from '../src/utils'; | ||
| import { linear, derivative } from '../src/samplers'; | ||
@@ -9,3 +8,2 @@ import raf from 'raf'; | ||
| const size = 100; | ||
| let time = 0; | ||
| document.body.style.padding = 0; | ||
@@ -22,4 +20,4 @@ document.body.style.margin = 0; | ||
| // The start of the interesting part | ||
| let time = 0; | ||
| const store = createStore({ | ||
@@ -26,0 +24,0 @@ x: linear('x'), |
+5
-3
| { | ||
| "name": "curve-store", | ||
| "version": "0.0.1", | ||
| "version": "1.0.0", | ||
| "description": "Redux-inspired store for dealing with continuous values", | ||
@@ -8,3 +8,4 @@ "main": "src/index.js", | ||
| "example": "budo examples/graphs.js --live -o -- -t [ babelify --presets [ es2015 ] ]", | ||
| "test": "semistandard && mocha --compilers js:babel-core/register" | ||
| "test": "semistandard && mocha --compilers js:babel-core/register", | ||
| "syntax-fix": "semistandard --fix" | ||
| }, | ||
@@ -22,6 +23,7 @@ "repository": { | ||
| "dependencies": { | ||
| "lodash.sortedindexby": "^4.5.1" | ||
| "lodash": "^4.15.0" | ||
| }, | ||
| "devDependencies": { | ||
| "babel-cli": "^6.11.4", | ||
| "babel-preset-es2015": "^6.13.2", | ||
| "babelify": "^7.3.0", | ||
@@ -28,0 +30,0 @@ "budo": "^8.3.0", |
+120
-9
| # curve-store | ||
| ## Examples | ||
| A store for dealing with continuous values. Useful for complex animations. | ||
| Continue reading for background and example usage. [Click here for a demo](https://mathisonian.github.io/curve-store/). | ||
| ### Simple Usage | ||
| ## Motivation | ||
| *The idea for this module came from discussions with [@mikolalysenko](https://github.com/mikolalysenko/). | ||
| Credit largely goes to him. See [filtered-vector](https://github.com/mikolalysenko/filtered-vector) for | ||
| prior art.* | ||
| Curve store is a state container that intelligently deals with mapping discrete | ||
| values to a continuous curve. Its primary intended use case is for dealing with | ||
| complex animations over time, though there may be other applications. | ||
| The idea is that animation can be defined as a series of positions over time: | ||
| given an object at position `x, y` at time `t`, we should be able to define an | ||
| animation by promising that the object will be at some other position `x', y'` at | ||
| some future time `t'`, and infer the points along the path. | ||
| This is what `curve-store` gives you, a way to define state at a given time: | ||
| ```js | ||
| store.set(currentTime, { x: x, y: y}); | ||
| store.set(currentTime + 1000, { x: xprime, y: yprime}); | ||
| ``` | ||
| and a way to sample points in between: | ||
| ```js | ||
| store.sample(currentTime + 500); | ||
| // give { x: xval, y: yval } interpolated based on the points set above | ||
| ``` | ||
| Users can define how they want the interpolation to be handled. There are a few | ||
| built in helpers, for example: | ||
| ```js | ||
| import { createStore } from 'curve-store'; | ||
| import { linear } from 'curve-store/samplers'; | ||
| const store = createStore({ | ||
| x: linear('x'), | ||
| y: linear('y') | ||
| }) | ||
| ``` | ||
| defines basic linear interpolation. There are also calculus functions to help | ||
| build out more complicated curves: | ||
| ```js | ||
| import { createStore } from 'curve-store'; | ||
| import { linear, derivative, integral } from 'curve-store/samplers'; | ||
| const store = createStore({ | ||
| position: { | ||
| x: linear('x'), | ||
| y: linear('y') | ||
| }, | ||
| velocity: { | ||
| x: derivative('x'), | ||
| y: derivative('y') | ||
| }, | ||
| distance: { | ||
| x: integral('x'), | ||
| y: integral('y') | ||
| } | ||
| }); | ||
| ``` | ||
| You can also provide custom sampling functions, to get e.g. different easing curves | ||
| (see below for more details). | ||
| ## Installation | ||
| ``` | ||
| $ npm install --save curve-store | ||
| ``` | ||
| ## Simple Example | ||
| ```js | ||
| import { createStore } from 'curve-store'; | ||
| import { linear, derivative } from 'curve-store/samplers'; | ||
@@ -24,2 +99,45 @@ | ||
| ## API | ||
| ### `createStore(samplers)` | ||
| Creates a new `curve-store` that maps discrete input values onto a set | ||
| of continuous output values. The samplers object defines this mapping and defines | ||
| how to interpolate between points. | ||
| Basic usage: | ||
| ```js | ||
| const store = createStore({ | ||
| outputX: linear('inputX') | ||
| }); | ||
| ``` | ||
| ### `store.set(time, values)` | ||
| Set values at a particular point in time. | ||
| Example: | ||
| ```js | ||
| store.set(0, { inputX: 0 }); | ||
| store.set(1, { inputX: 0 }); | ||
| ``` | ||
| ### store.sample(time) | ||
| Sample points at a particular time. | ||
| Example: | ||
| ```js | ||
| store.sample(0.5); | ||
| // -> outputs { outputX: 0.5 } | ||
| ``` | ||
| The way that sampling occurs is defined based on the samplers object passed | ||
| to `createStore`. | ||
| ### Custom sampling | ||
@@ -51,11 +169,4 @@ | ||
| ## Installation | ||
| ``` | ||
| $ npm install --save curve-store | ||
| ``` | ||
| ## LICENSE | ||
| MIT |
+21
-3
| import { setAsLastPoint } from './utils'; | ||
| import { linear } from './samplers'; | ||
| import { isArray, isObject, isFunction } from 'lodash'; | ||
| const runSampler = (sampler, time, state, sample) => { | ||
| if (isFunction(sampler)) { | ||
| return sampler(time, state, sample); | ||
| } else if (isArray(sampler)) { | ||
| return sampler.map((s) => { runSampler(s, time, state, sample); }); | ||
| } else if (isObject(sampler)) { | ||
| const retObj = {}; | ||
| Object.keys(sampler).forEach((key) => { | ||
| retObj[key] = runSampler(sampler[key], time, state, sample); | ||
| }); | ||
| return retObj; | ||
| } | ||
| }; | ||
| export default (samplers) => { | ||
@@ -20,10 +36,12 @@ const state = {}; | ||
| if (typeof keys === 'string') { | ||
| return samplers[keys](time, state); | ||
| const s = isFunction(samplers[keys]) ? samplers[keys] : linear(keys); | ||
| return runSampler(s, time, state); | ||
| } | ||
| const checkKeys = Array.isArray(keys); | ||
| const checkKeys = isArray(keys); | ||
| Object.keys(samplers).forEach((samplerName) => { | ||
| if (!checkKeys || keys.indexOf(samplerName)) { | ||
| ret[samplerName] = samplers[samplerName](time, state, sample); | ||
| const sampler = samplers[samplerName]; | ||
| ret[samplerName] = runSampler(sampler, time, state, sample); | ||
| } | ||
@@ -30,0 +48,0 @@ }); |
@@ -1,2 +0,3 @@ | ||
| import { getPointBefore, getPointAfter } from '../utils'; | ||
| import { getPointBefore, getPointAfter, snap } from '../utils'; | ||
| import { isFunction, memoize } from 'lodash'; | ||
@@ -21,8 +22,14 @@ const linear = (name) => { | ||
| const derivative = (name, delta) => { | ||
| delta = delta || 0.001; | ||
| return (t, state, sample) => { | ||
| const delta = delta || 0.0001; | ||
| const x1 = sample(t - delta, name); | ||
| const x2 = sample(t, name); | ||
| let x1; | ||
| let x2; | ||
| if (isFunction(name)) { | ||
| x1 = name(t - delta, state, sample); | ||
| x2 = name(t, state, sample); | ||
| } else { | ||
| x1 = sample(t - delta, name); | ||
| x2 = sample(t, name); | ||
| } | ||
| return (x2 - x1) / delta; | ||
@@ -32,5 +39,25 @@ }; | ||
| const integral = (name, delta) => { | ||
| delta = delta || 0.01; | ||
| const recursiveIntegral = memoize((t, state, sample) => { | ||
| if (t === 0) { | ||
| return 0; | ||
| } | ||
| const snapped = snap(t, delta); | ||
| if (snapped === t) { | ||
| return (delta * (sample(t, name) + sample(t - delta, name)) / 2) + recursiveIntegral(t - delta, state, sample); | ||
| } | ||
| return ((t - snapped) * (sample(t, name) + sample(snapped, name)) / 2) + recursiveIntegral(snapped, state, sample); | ||
| }); | ||
| return recursiveIntegral; | ||
| }; | ||
| export { | ||
| linear, | ||
| derivative | ||
| derivative, | ||
| integral | ||
| }; |
+21
-5
@@ -1,6 +0,6 @@ | ||
| import sortedIndex from 'lodash.sortedindexby'; | ||
| import { sortedIndexBy } from 'lodash'; | ||
| const set = (array, time, value) => { | ||
| const arrayObj = { time, value }; | ||
| const index = sortedIndex(array, arrayObj, 'time'); | ||
| const index = sortedIndexBy(array, arrayObj, 'time'); | ||
| array.splice(index, 0, value); | ||
@@ -11,3 +11,3 @@ }; | ||
| const arrayObj = { time, value }; | ||
| const index = sortedIndex(array, arrayObj, 'time'); | ||
| const index = sortedIndexBy(array, arrayObj, 'time'); | ||
| array.splice(index, array.length - index, arrayObj); | ||
@@ -17,3 +17,3 @@ }; | ||
| const getPointsBefore = (array, time, n) => { | ||
| const index = sortedIndex(array, { time }, 'time'); | ||
| const index = sortedIndexBy(array, { time }, 'time'); | ||
| return array.slice(Math.max(0, index - n), index); | ||
@@ -23,3 +23,3 @@ }; | ||
| const getPointsAfter = (array, time, n) => { | ||
| const index = sortedIndex(array, { time }, 'time'); | ||
| const index = sortedIndexBy(array, { time }, 'time'); | ||
| return array.slice(index, index + n); | ||
@@ -38,4 +38,20 @@ }; | ||
| const snap = (t, delta) => { | ||
| let factor = 1; | ||
| if (delta < 0) { | ||
| factor = 1 / delta; | ||
| } | ||
| const scaledT = factor * t; | ||
| const modT = scaledT % (delta * factor); | ||
| if (modT === 0) { | ||
| return t; | ||
| } | ||
| return (scaledT - modT) / factor; | ||
| }; | ||
| export { | ||
| set, | ||
| snap, | ||
| setAsLastPoint, | ||
@@ -42,0 +58,0 @@ getPointAfter, |
+74
-2
@@ -5,5 +5,7 @@ /*global describe, it */ | ||
| import { createStore } from '../src'; | ||
| import { getPointBefore, getPointAfter } from '../src/utils'; | ||
| import { linear } from '../src/samplers'; | ||
| import { getPointBefore, getPointAfter, snap } from '../src/utils'; | ||
| import { linear, derivative, integral } from '../src/samplers'; | ||
| const epsilon = 0.00001; | ||
| describe('curve-store tests', () => { | ||
@@ -88,2 +90,18 @@ it('should create a new store', () => { | ||
| it('should handle nested samplers', () => { | ||
| const store = createStore({ | ||
| x: { | ||
| position: linear('x'), | ||
| velocity: derivative('x') | ||
| } | ||
| }); | ||
| store.set(0, { x: 0 }); | ||
| store.set(1, { x: 1 }); | ||
| let sample = store.sample(0.25); | ||
| expect(sample.x.position).toEqual(0.25); | ||
| expect(Math.abs(sample.x.velocity - 1)).toBeLessThan(epsilon); | ||
| }); | ||
| it('should sample values correctly at points', () => { | ||
@@ -103,2 +121,56 @@ const store = createStore({ | ||
| }); | ||
| it('should get derivative correctly', () => { | ||
| const store = createStore({ | ||
| d: derivative('myKey') | ||
| }); | ||
| store.set(0, { myKey: 0 }); | ||
| store.set(1, { myKey: 1 }); | ||
| let sample = store.sample(0.5); | ||
| expect(Math.abs(sample.d - 1)).toBeLessThan(epsilon); | ||
| }); | ||
| it('should get the second derivative correctly', () => { | ||
| const store = createStore({ | ||
| d: derivative(derivative('myKey')) | ||
| }); | ||
| store.set(0, { myKey: 0 }); | ||
| store.set(1, { myKey: 1 }); | ||
| let sample = store.sample(0.5); | ||
| expect(sample).toEqual({ d: 0 }); | ||
| }); | ||
| it('should snap to a value correctly', () => { | ||
| const t = 0.015; | ||
| const s = snap(t, 0.01); | ||
| expect(s).toEqual(0.01); | ||
| }); | ||
| it('should snap to a presnapped value correctly', () => { | ||
| const t = 0.01; | ||
| const s = snap(t, 0.01); | ||
| expect(s).toEqual(0.01); | ||
| }); | ||
| it('should compute an integral correctly', () => { | ||
| const store = createStore({ | ||
| i: integral('myKey') | ||
| }); | ||
| store.set(0, { myKey: 0 }); | ||
| store.set(1, { myKey: 1 }); | ||
| let sample = store.sample(0.25); | ||
| expect(Math.abs(sample.i - 0.03125)).toBeLessThan(epsilon); | ||
| sample = store.sample(0.5); | ||
| expect(Math.abs(sample.i - 0.125)).toBeLessThan(epsilon); | ||
| sample = store.sample(1); | ||
| expect(Math.abs(sample.i - 0.5)).toBeLessThan(epsilon); | ||
| }); | ||
| }); |
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
15144
69.47%331
47.77%0
-100%171
185%8
14.29%+ Added
+ Added
- Removed
- Removed