├── .babelrc ├── .gitignore ├── README.md ├── examples └── mouse-move.js ├── lib ├── create-store.js ├── index.js ├── samplers │ └── index.js └── utils │ └── index.js ├── package.json ├── src ├── create-store.js ├── index.js ├── samplers │ └── index.js └── utils │ └── index.js └── test └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # curve-store 2 | 3 | A store for dealing with continuous values. Useful for complex animations. 4 | Continue reading for background and example usage. [Click here for a demo](https://mathisonian.github.io/curve-store/). 5 | 6 | ## Motivation 7 | 8 | *The idea for this module came from discussions with [@mikolalysenko](https://github.com/mikolalysenko/). 9 | Credit largely goes to him. See [filtered-vector](https://github.com/mikolalysenko/filtered-vector) for 10 | prior art.* 11 | 12 | Curve store is a state container that intelligently deals with mapping discrete 13 | values to a continuous curve. Its primary intended use case is for dealing with 14 | complex animations over time, though there may be other applications. 15 | 16 | The idea is that animation can be defined as a series of positions over time: 17 | given an object at position `x, y` at time `t`, we should be able to define an 18 | animation by promising that the object will be at some other position `x', y'` at 19 | some future time `t'`, and infer the points along the path. 20 | 21 | This is what `curve-store` gives you, a way to define state at a given time: 22 | 23 | ```js 24 | store.set(currentTime, { x: x, y: y }); 25 | store.set(currentTime + 1000, { x: xprime, y: yprime }); 26 | ``` 27 | 28 | and a way to sample points in between: 29 | 30 | ```js 31 | store.sample(currentTime + 500); 32 | // give { x: xval, y: yval } interpolated based on the points set above 33 | ``` 34 | 35 | Users can define how they want the interpolation to be handled. There are a few 36 | built in helpers, for example: 37 | 38 | ```js 39 | import { createStore } from 'curve-store'; 40 | import { linear } from 'curve-store/samplers'; 41 | 42 | const store = createStore({ 43 | x: linear('x'), 44 | y: linear('y') 45 | }) 46 | ``` 47 | 48 | defines basic linear interpolation. There are also calculus functions to help 49 | build out more complicated curves: 50 | 51 | ```js 52 | import { createStore } from 'curve-store'; 53 | import { linear, derivative, integral } from 'curve-store/samplers'; 54 | 55 | const store = createStore({ 56 | position: { 57 | x: linear('x'), 58 | y: linear('y') 59 | }, 60 | velocity: { 61 | x: derivative('x'), 62 | y: derivative('y') 63 | }, 64 | acceleration: { 65 | x: derivative(derivative('x')), 66 | y: derivative(derivative('y')) 67 | }, 68 | distance: { 69 | x: integral('x'), 70 | y: integral('y') 71 | } 72 | }); 73 | ``` 74 | 75 | You can also provide custom sampling functions, to get e.g. different easing curves 76 | (see below for more details). 77 | 78 | ## Installation 79 | 80 | ``` 81 | $ npm install --save curve-store 82 | ``` 83 | 84 | ## Simple Example 85 | 86 | ```js 87 | import { createStore } from 'curve-store'; 88 | import { linear, derivative } from 'curve-store/samplers'; 89 | 90 | const store = createStore({ 91 | x: linear('x'), 92 | dx: derivative('x') 93 | }); 94 | 95 | store.set(0, { x: 0 }); 96 | store.set(1, { x: 1 }); 97 | 98 | store.sample(0.25); 99 | // --> { x: 0.25, dx: 1.0 } 100 | 101 | ``` 102 | 103 | 104 | ## API 105 | 106 | ### `createStore(samplers)` 107 | 108 | Creates a new `curve-store` that maps discrete input values onto a set 109 | of continuous output values. The samplers object defines this mapping and defines 110 | how to interpolate between points. 111 | 112 | Basic usage: 113 | 114 | ```js 115 | const store = createStore({ 116 | outputX: linear('inputX') 117 | }); 118 | ``` 119 | 120 | ### `store.set(time, values)` 121 | 122 | Set values at a particular point in time. 123 | 124 | Example: 125 | 126 | ```js 127 | store.set(0, { inputX: 0 }); 128 | store.set(1, { inputX: 0 }); 129 | ``` 130 | 131 | ### store.sample(time) 132 | 133 | Sample points at a particular time. 134 | 135 | Example: 136 | 137 | ```js 138 | store.sample(0.5); 139 | // -> outputs { outputX: 0.5 } 140 | ``` 141 | 142 | The way that sampling occurs is defined based on the samplers object passed 143 | to `createStore`. 144 | 145 | 146 | ### Custom sampling 147 | 148 | ```js 149 | 150 | import { createStore } from 'create-store'; 151 | import { getPointBefore, getPointAfter } from 'create-store/utils'; 152 | 153 | const store = createStore({ 154 | myKey: (t, state) => { 155 | const before = getPointBefore(state.myKey, t); 156 | // { time: 0, value: 0 } 157 | const after = getPointAfter(state.myKey, t); 158 | // { time: 1, value: 1} 159 | 160 | // Insert custom sampling code here 161 | return customVal; 162 | } 163 | }); 164 | 165 | store.set(0, { myKey: 0 }); 166 | store.set(1, { myKey: 1 }); 167 | 168 | store.sample(0.25); 169 | // { myKey: customVal } 170 | ``` 171 | 172 | ### Clearing values 173 | 174 | ```js 175 | // Empties the store. 176 | store.clear(); 177 | ``` 178 | 179 | ```js 180 | // Removes all values before time t 181 | store.clearBefore(t); 182 | ``` 183 | 184 | ```js 185 | // Removes all values after time t 186 | stores.clearAfter(t); 187 | ``` 188 | 189 | ## LICENSE 190 | 191 | MIT 192 | -------------------------------------------------------------------------------- /examples/mouse-move.js: -------------------------------------------------------------------------------- 1 | // Import libraries 2 | import { createStore } from '../src'; 3 | import { linear, derivative } from '../src/samplers'; 4 | import raf from 'raf'; 5 | 6 | // Boilerplate Setup 7 | const size = 100; 8 | document.body.style.padding = 0; 9 | document.body.style.margin = 0; 10 | const width = window.innerWidth; 11 | const height = window.innerHeight; 12 | const canvas = document.createElement('canvas'); 13 | canvas.width = width; 14 | canvas.height = height; 15 | canvas.style.display = 'block'; 16 | document.body.appendChild(canvas); 17 | const context = canvas.getContext('2d'); 18 | 19 | // The start of the interesting part 20 | let time = 0; 21 | const store = createStore({ 22 | x: linear('x'), 23 | y: linear('y'), 24 | dx: derivative('x'), 25 | dy: derivative('y') 26 | }); 27 | 28 | store.set(time, { 29 | x: (width - size) / 2, 30 | y: (height - size) / 2 31 | }); 32 | 33 | canvas.onmousemove = (e) => { 34 | store.set(time, store.sample(time)); 35 | store.set(time + 400, { 36 | x: e.clientX - size / 2, 37 | y: e.clientY - size / 2 38 | }); 39 | }; 40 | 41 | raf(function tick (t) { 42 | time = t; 43 | const { x, y, dx, dy } = store.sample(time); 44 | context.clearRect(0, 0, width, height); 45 | 46 | const count = 6; 47 | for (var i = count - 1; i >= 0; i--) { 48 | context.fillStyle = `rgba(145, 117, 240, ${1 - i / count})`; 49 | context.fillRect(x - i * 33 * dx, y - i * 33 * dy, size, size); 50 | } 51 | 52 | raf(tick); 53 | }); 54 | -------------------------------------------------------------------------------- /lib/create-store.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; 8 | 9 | var _utils = require('./utils'); 10 | 11 | var _samplers = require('./samplers'); 12 | 13 | var _lodash = require('lodash.isfunction'); 14 | 15 | var _lodash2 = _interopRequireDefault(_lodash); 16 | 17 | var _lodash3 = require('lodash.isobject'); 18 | 19 | var _lodash4 = _interopRequireDefault(_lodash3); 20 | 21 | var _lodash5 = require('lodash.isarray'); 22 | 23 | var _lodash6 = _interopRequireDefault(_lodash5); 24 | 25 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 26 | 27 | var runSampler = function runSampler(sampler, time, state, sample) { 28 | if ((0, _lodash2.default)(sampler)) { 29 | return sampler(time, state, sample); 30 | } else if ((0, _lodash6.default)(sampler)) { 31 | return sampler.map(function (s) { 32 | runSampler(s, time, state, sample); 33 | }); 34 | } else if ((0, _lodash4.default)(sampler)) { 35 | var _ret = function () { 36 | var retObj = {}; 37 | Object.keys(sampler).forEach(function (key) { 38 | retObj[key] = runSampler(sampler[key], time, state, sample); 39 | }); 40 | return { 41 | v: retObj 42 | }; 43 | }(); 44 | 45 | if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v; 46 | } 47 | }; 48 | 49 | exports.default = function (samplers) { 50 | var state = {}; 51 | 52 | var clear = function clear() { 53 | state = {}; 54 | }; 55 | 56 | var clearBefore = function clearBefore(t) { 57 | Object.keys(state).forEach(function (key) { 58 | state[key] = state[key].filter(function (_ref) { 59 | var time = _ref.time; 60 | return time >= t; 61 | }); 62 | }); 63 | }; 64 | 65 | var clearAfter = function clearAfter(t) { 66 | Object.keys(state).forEach(function (key) { 67 | state[key] = state[key].filter(function (_ref2) { 68 | var time = _ref2.time; 69 | return time <= t; 70 | }); 71 | }); 72 | }; 73 | 74 | var set = function set(time, values) { 75 | Object.keys(values).forEach(function (key) { 76 | var val = values[key]; 77 | if (!state.hasOwnProperty(key)) { 78 | state[key] = []; 79 | } 80 | (0, _utils.setAsLastPoint)(state[key], time, val); 81 | }); 82 | }; 83 | 84 | var sample = function sample(time, keys) { 85 | var ret = {}; 86 | 87 | if (typeof keys === 'string') { 88 | var s = (0, _lodash2.default)(samplers[keys]) ? samplers[keys] : (0, _samplers.linear)(keys); 89 | return runSampler(s, time, state); 90 | } 91 | 92 | var checkKeys = (0, _lodash6.default)(keys); 93 | 94 | Object.keys(samplers).forEach(function (samplerName) { 95 | if (!checkKeys || keys.indexOf(samplerName)) { 96 | var sampler = samplers[samplerName]; 97 | ret[samplerName] = runSampler(sampler, time, state, sample); 98 | } 99 | }); 100 | return ret; 101 | }; 102 | 103 | var getState = function getState() { 104 | return Object.assign({}, state); 105 | }; 106 | 107 | return { 108 | set: set, 109 | clear: clear, 110 | clearBefore: clearBefore, 111 | clearAfter: clearAfter, 112 | sample: sample, 113 | getState: getState 114 | }; 115 | }; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.createStore = undefined; 7 | 8 | var _createStore = require('./create-store'); 9 | 10 | var _createStore2 = _interopRequireDefault(_createStore); 11 | 12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 13 | 14 | exports.createStore = _createStore2.default; -------------------------------------------------------------------------------- /lib/samplers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.integral = exports.derivative = exports.linear = undefined; 7 | 8 | var _utils = require('../utils'); 9 | 10 | var _lodash = require('lodash.isfunction'); 11 | 12 | var _lodash2 = _interopRequireDefault(_lodash); 13 | 14 | var _lodash3 = require('lodash.memoize'); 15 | 16 | var _lodash4 = _interopRequireDefault(_lodash3); 17 | 18 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 19 | 20 | var linear = function linear(name) { 21 | return function (t, state) { 22 | var before = (0, _utils.getPointBefore)(state[name], t); 23 | var after = (0, _utils.getPointAfter)(state[name], t); 24 | 25 | if (before === null) { 26 | return after.value; 27 | } 28 | 29 | if (after === null) { 30 | return before.value; 31 | } 32 | 33 | return before.value + (t - before.time) * (after.value - before.value) / (after.time - before.time); 34 | }; 35 | }; 36 | 37 | var derivative = function derivative(name, delta) { 38 | delta = delta || 0.001; 39 | 40 | return function (t, state, sample) { 41 | var x1 = void 0; 42 | var x2 = void 0; 43 | if ((0, _lodash2.default)(name)) { 44 | x1 = name(t - delta, state, sample); 45 | x2 = name(t, state, sample); 46 | } else { 47 | x1 = sample(t - delta, name); 48 | x2 = sample(t, name); 49 | } 50 | return (x2 - x1) / delta; 51 | }; 52 | }; 53 | 54 | var integral = function integral(name, delta) { 55 | delta = delta || 0.01; 56 | 57 | var recursiveIntegral = (0, _lodash4.default)(function (t, state, sample) { 58 | if (t === 0) { 59 | return 0; 60 | } 61 | 62 | var snapped = (0, _utils.snap)(t, delta); 63 | if (snapped === t) { 64 | return delta * (sample(t, name) + sample(t - delta, name)) / 2 + recursiveIntegral(t - delta, state, sample); 65 | } 66 | 67 | return (t - snapped) * (sample(t, name) + sample(snapped, name)) / 2 + recursiveIntegral(snapped, state, sample); 68 | }); 69 | 70 | return recursiveIntegral; 71 | }; 72 | 73 | exports.linear = linear; 74 | exports.derivative = derivative; 75 | exports.integral = integral; -------------------------------------------------------------------------------- /lib/utils/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.getPointsBefore = exports.getPointsAfter = exports.getPointBefore = exports.getPointAfter = exports.setAsLastPoint = exports.snap = exports.set = undefined; 7 | 8 | var _lodash = require('lodash.sortedindexby'); 9 | 10 | var _lodash2 = _interopRequireDefault(_lodash); 11 | 12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 13 | 14 | var set = function set(array, time, value) { 15 | var arrayObj = { time: time, value: value }; 16 | var index = (0, _lodash2.default)(array, arrayObj, 'time'); 17 | array.splice(index, 0, value); 18 | }; 19 | 20 | var setAsLastPoint = function setAsLastPoint(array, time, value) { 21 | var arrayObj = { time: time, value: value }; 22 | var index = (0, _lodash2.default)(array, arrayObj, 'time'); 23 | array.splice(index, array.length - index, arrayObj); 24 | }; 25 | 26 | var getPointsBefore = function getPointsBefore(array, time, n) { 27 | var index = (0, _lodash2.default)(array, { time: time }, 'time'); 28 | return array.slice(Math.max(0, index - n), index); 29 | }; 30 | 31 | var getPointsAfter = function getPointsAfter(array, time, n) { 32 | var index = (0, _lodash2.default)(array, { time: time }, 'time'); 33 | return array.slice(index, index + n); 34 | }; 35 | 36 | var getPointBefore = function getPointBefore(array, time) { 37 | var pointArray = getPointsBefore(array, time, 1); 38 | return pointArray.length ? pointArray[0] : null; 39 | }; 40 | 41 | var getPointAfter = function getPointAfter(array, time) { 42 | var pointArray = getPointsAfter(array, time, 1); 43 | return pointArray.length ? pointArray[0] : null; 44 | }; 45 | 46 | var snap = function snap(t, delta) { 47 | var factor = 1; 48 | if (delta < 0) { 49 | factor = 1 / delta; 50 | } 51 | 52 | var scaledT = factor * t; 53 | var modT = scaledT % (delta * factor); 54 | if (modT === 0) { 55 | return t; 56 | } 57 | 58 | return (scaledT - modT) / factor; 59 | }; 60 | 61 | exports.set = set; 62 | exports.snap = snap; 63 | exports.setAsLastPoint = setAsLastPoint; 64 | exports.getPointAfter = getPointAfter; 65 | exports.getPointBefore = getPointBefore; 66 | exports.getPointsAfter = getPointsAfter; 67 | exports.getPointsBefore = getPointsBefore; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "curve-store", 3 | "version": "1.1.2", 4 | "description": "Redux-inspired store for dealing with continuous values", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "example": "budo examples/graphs.js --live -o -- -t [ babelify --presets [ es2015 ] ]", 8 | "build": "babel src --out-dir lib", 9 | "test": "semistandard src/** test/** && mocha --compilers js:babel-core/register", 10 | "syntax-fix": "semistandard --fix" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/mathisonian/curve-store.git" 15 | }, 16 | "author": "Matthew Conlen", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/mathisonian/curve-store/issues" 20 | }, 21 | "homepage": "https://github.com/mathisonian/curve-store#readme", 22 | "dependencies": { 23 | "lodash.isarray": "^4.0.0", 24 | "lodash.isfunction": "^3.0.8", 25 | "lodash.isobject": "^3.0.2", 26 | "lodash.memoize": "^4.1.2", 27 | "lodash.sortedindexby": "^4.6.0" 28 | }, 29 | "devDependencies": { 30 | "babel-cli": "^6.14.0", 31 | "babel-preset-es2015": "^6.13.2", 32 | "babelify": "^7.3.0", 33 | "budo": "^8.3.0", 34 | "expect": "^1.20.2", 35 | "mocha": "^3.0.1", 36 | "raf": "^3.2.0", 37 | "semistandard": "^8.0.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/create-store.js: -------------------------------------------------------------------------------- 1 | import { setAsLastPoint } from './utils'; 2 | import { linear } from './samplers'; 3 | import isFunction from 'lodash.isfunction'; 4 | import isObject from 'lodash.isobject'; 5 | import isArray from 'lodash.isarray'; 6 | 7 | const runSampler = (sampler, time, state, sample) => { 8 | if (isFunction(sampler)) { 9 | return sampler(time, state, sample); 10 | } else if (isArray(sampler)) { 11 | return sampler.map((s) => { runSampler(s, time, state, sample); }); 12 | } else if (isObject(sampler)) { 13 | const retObj = {}; 14 | Object.keys(sampler).forEach((key) => { 15 | retObj[key] = runSampler(sampler[key], time, state, sample); 16 | }); 17 | return retObj; 18 | } 19 | }; 20 | 21 | export default (samplers) => { 22 | let state = {}; 23 | 24 | const clear = () => { 25 | state = {}; 26 | }; 27 | 28 | const clearBefore = (t) => { 29 | Object.keys(state).forEach((key) => { 30 | state[key] = state[key].filter(({ time }) => { return time >= t; }); 31 | }); 32 | }; 33 | 34 | const clearAfter = (t) => { 35 | Object.keys(state).forEach((key) => { 36 | state[key] = state[key].filter(({ time }) => { return time <= t; }); 37 | }); 38 | }; 39 | 40 | const set = (time, values) => { 41 | Object.keys(values).forEach((key) => { 42 | const val = values[key]; 43 | if (!state.hasOwnProperty(key)) { 44 | state[key] = []; 45 | } 46 | setAsLastPoint(state[key], time, val); 47 | }); 48 | }; 49 | 50 | const sample = (time, keys) => { 51 | const ret = {}; 52 | 53 | if (typeof keys === 'string') { 54 | const s = isFunction(samplers[keys]) ? samplers[keys] : linear(keys); 55 | return runSampler(s, time, state); 56 | } 57 | 58 | const checkKeys = isArray(keys); 59 | 60 | Object.keys(samplers).forEach((samplerName) => { 61 | if (!checkKeys || keys.indexOf(samplerName)) { 62 | const sampler = samplers[samplerName]; 63 | ret[samplerName] = runSampler(sampler, time, state, sample); 64 | } 65 | }); 66 | return ret; 67 | }; 68 | 69 | const getState = () => { 70 | return Object.assign({}, state); 71 | }; 72 | 73 | return { 74 | set, 75 | clear, 76 | clearBefore, 77 | clearAfter, 78 | sample, 79 | getState 80 | }; 81 | }; 82 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import createStore from './create-store'; 2 | 3 | export { 4 | createStore 5 | }; 6 | -------------------------------------------------------------------------------- /src/samplers/index.js: -------------------------------------------------------------------------------- 1 | import { getPointBefore, getPointAfter, snap } from '../utils'; 2 | import isFunction from 'lodash.isfunction'; 3 | import memoize from 'lodash.memoize'; 4 | 5 | const linear = (name) => { 6 | return (t, state) => { 7 | let before = getPointBefore(state[name], t); 8 | let after = getPointAfter(state[name], t); 9 | 10 | if (before === null) { 11 | return after.value; 12 | } 13 | 14 | if (after === null) { 15 | return before.value; 16 | } 17 | 18 | return before.value + (t - before.time) * (after.value - before.value) / (after.time - before.time); 19 | }; 20 | }; 21 | 22 | const derivative = (name, delta) => { 23 | delta = delta || 0.001; 24 | 25 | return (t, state, sample) => { 26 | let x1; 27 | let x2; 28 | if (isFunction(name)) { 29 | x1 = name(t - delta, state, sample); 30 | x2 = name(t, state, sample); 31 | } else { 32 | x1 = sample(t - delta, name); 33 | x2 = sample(t, name); 34 | } 35 | return (x2 - x1) / delta; 36 | }; 37 | }; 38 | 39 | const integral = (name, delta) => { 40 | delta = delta || 0.01; 41 | 42 | const recursiveIntegral = memoize((t, state, sample) => { 43 | if (t === 0) { 44 | return 0; 45 | } 46 | 47 | const snapped = snap(t, delta); 48 | if (snapped === t) { 49 | return (delta * (sample(t, name) + sample(t - delta, name)) / 2) + recursiveIntegral(t - delta, state, sample); 50 | } 51 | 52 | return ((t - snapped) * (sample(t, name) + sample(snapped, name)) / 2) + recursiveIntegral(snapped, state, sample); 53 | }); 54 | 55 | return recursiveIntegral; 56 | }; 57 | 58 | export { 59 | linear, 60 | derivative, 61 | integral 62 | }; 63 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import sortedIndexBy from 'lodash.sortedindexby'; 2 | 3 | const set = (array, time, value) => { 4 | const arrayObj = { time, value }; 5 | const index = sortedIndexBy(array, arrayObj, 'time'); 6 | array.splice(index, 0, value); 7 | }; 8 | 9 | const setAsLastPoint = (array, time, value) => { 10 | const arrayObj = { time, value }; 11 | const index = sortedIndexBy(array, arrayObj, 'time'); 12 | array.splice(index, array.length - index, arrayObj); 13 | }; 14 | 15 | const getPointsBefore = (array, time, n) => { 16 | const index = sortedIndexBy(array, { time }, 'time'); 17 | return array.slice(Math.max(0, index - n), index); 18 | }; 19 | 20 | const getPointsAfter = (array, time, n) => { 21 | const index = sortedIndexBy(array, { time }, 'time'); 22 | return array.slice(index, index + n); 23 | }; 24 | 25 | const getPointBefore = (array, time) => { 26 | const pointArray = getPointsBefore(array, time, 1); 27 | return pointArray.length ? pointArray[0] : null; 28 | }; 29 | 30 | const getPointAfter = (array, time) => { 31 | const pointArray = getPointsAfter(array, time, 1); 32 | return pointArray.length ? pointArray[0] : null; 33 | }; 34 | 35 | const snap = (t, delta) => { 36 | let factor = 1; 37 | if (delta < 0) { 38 | factor = 1 / delta; 39 | } 40 | 41 | const scaledT = factor * t; 42 | const modT = scaledT % (delta * factor); 43 | if (modT === 0) { 44 | return t; 45 | } 46 | 47 | return (scaledT - modT) / factor; 48 | }; 49 | 50 | export { 51 | set, 52 | snap, 53 | setAsLastPoint, 54 | getPointAfter, 55 | getPointBefore, 56 | getPointsAfter, 57 | getPointsBefore 58 | }; 59 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /*global describe, it */ 2 | 3 | import expect from 'expect'; 4 | import { createStore } from '../src'; 5 | import { getPointBefore, getPointAfter, snap } from '../src/utils'; 6 | import { linear, derivative, integral } from '../src/samplers'; 7 | 8 | const epsilon = 0.00001; 9 | 10 | describe('curve-store tests', () => { 11 | it('should create a new store', () => { 12 | const store = createStore(); 13 | expect(store).toBeA(Object); 14 | }); 15 | 16 | it('should set a value at t=0', () => { 17 | const store = createStore(); 18 | store.set(0, { myKey: 'value' }); 19 | 20 | expect(store.getState()).toEqual({ 21 | myKey: [{ time: 0, value: 'value' }] 22 | }); 23 | }); 24 | 25 | it('should set a series of values', () => { 26 | const store = createStore(); 27 | store.set(0, { myKey: 0 }); 28 | store.set(1, { myKey: 1 }); 29 | 30 | expect(store.getState()).toEqual({ 31 | myKey: [{ time: 0, value: 0 }, { time: 1, value: 1 }] 32 | }); 33 | }); 34 | it('should clear future values', () => { 35 | const store = createStore(); 36 | store.set(1, { myKey: 1 }); 37 | 38 | store.set(0, { myKey: 0 }); 39 | 40 | expect(store.getState()).toEqual({ 41 | myKey: [{ time: 0, value: 0 }] 42 | }); 43 | }); 44 | 45 | it('should get point before correctly', () => { 46 | const store = createStore(); 47 | 48 | store.set(0, { myKey: 0 }); 49 | store.set(1, { myKey: 1 }); 50 | 51 | const state = store.getState(); 52 | expect(getPointBefore(state.myKey, 0.5)).toEqual({ 53 | time: 0, 54 | value: 0 55 | }); 56 | }); 57 | 58 | it('should get point after correctly', () => { 59 | const store = createStore(); 60 | 61 | store.set(0, { myKey: 0 }); 62 | store.set(1, { myKey: 1 }); 63 | 64 | const state = store.getState(); 65 | expect(getPointAfter(state.myKey, 0.5)).toEqual({ 66 | time: 1, 67 | value: 1 68 | }); 69 | }); 70 | 71 | it('should linearly sample values correctly between points', () => { 72 | const store = createStore({ 73 | myKey: linear('myKey') 74 | }); 75 | 76 | store.set(0, { myKey: 0 }); 77 | store.set(1, { myKey: 1 }); 78 | 79 | let sample = store.sample(0.25); 80 | expect(sample).toEqual({ myKey: 0.25 }); 81 | 82 | sample = store.sample(0.5); 83 | expect(sample).toEqual({ myKey: 0.5 }); 84 | 85 | sample = store.sample(0.75); 86 | expect(sample).toEqual({ myKey: 0.75 }); 87 | }); 88 | 89 | it('should handle nested samplers', () => { 90 | const store = createStore({ 91 | x: { 92 | position: linear('x'), 93 | velocity: derivative('x') 94 | } 95 | }); 96 | 97 | store.set(0, { x: 0 }); 98 | store.set(1, { x: 1 }); 99 | 100 | let sample = store.sample(0.25); 101 | expect(sample.x.position).toEqual(0.25); 102 | expect(Math.abs(sample.x.velocity - 1)).toBeLessThan(epsilon); 103 | }); 104 | 105 | it('should sample values correctly at points', () => { 106 | const store = createStore({ 107 | myKey: linear('myKey') 108 | }); 109 | 110 | store.set(0, { myKey: 0 }); 111 | store.set(1, { myKey: 1 }); 112 | 113 | let sample = store.sample(0); 114 | expect(sample).toEqual({ myKey: 0 }); 115 | 116 | sample = store.sample(1); 117 | expect(sample).toEqual({ myKey: 1 }); 118 | }); 119 | 120 | it('should get derivative correctly', () => { 121 | const store = createStore({ 122 | d: derivative('myKey') 123 | }); 124 | 125 | store.set(0, { myKey: 0 }); 126 | store.set(1, { myKey: 1 }); 127 | 128 | let sample = store.sample(0.5); 129 | expect(Math.abs(sample.d - 1)).toBeLessThan(epsilon); 130 | }); 131 | 132 | it('should get the second derivative correctly', () => { 133 | const store = createStore({ 134 | d: derivative(derivative('myKey')) 135 | }); 136 | 137 | store.set(0, { myKey: 0 }); 138 | store.set(1, { myKey: 1 }); 139 | 140 | let sample = store.sample(0.5); 141 | expect(sample).toEqual({ d: 0 }); 142 | }); 143 | 144 | it('should snap to a value correctly', () => { 145 | const t = 0.015; 146 | const s = snap(t, 0.01); 147 | expect(s).toEqual(0.01); 148 | }); 149 | 150 | it('should snap to a presnapped value correctly', () => { 151 | const t = 0.01; 152 | const s = snap(t, 0.01); 153 | expect(s).toEqual(0.01); 154 | }); 155 | 156 | it('should compute an integral correctly', () => { 157 | const store = createStore({ 158 | i: integral('myKey') 159 | }); 160 | 161 | store.set(0, { myKey: 0 }); 162 | store.set(1, { myKey: 1 }); 163 | 164 | let sample = store.sample(0.25); 165 | expect(Math.abs(sample.i - 0.03125)).toBeLessThan(epsilon); 166 | 167 | sample = store.sample(0.5); 168 | expect(Math.abs(sample.i - 0.125)).toBeLessThan(epsilon); 169 | 170 | sample = store.sample(1); 171 | expect(Math.abs(sample.i - 0.5)).toBeLessThan(epsilon); 172 | }); 173 | 174 | it('should clear values correctly', () => { 175 | const store = createStore({ 176 | d: derivative('myKey') 177 | }); 178 | 179 | store.set(0, { myKey: 0 }); 180 | store.set(1, { myKey: 1 }); 181 | 182 | store.clear(); 183 | let state = store.getState(); 184 | expect(state).toEqual({}); 185 | 186 | store.set(0, { myKey: 0 }); 187 | store.set(1, { myKey: 1 }); 188 | 189 | store.clearBefore(0.5); 190 | state = store.getState(); 191 | expect(state).toEqual({ 192 | myKey: [{ time: 1, value: 1 }] 193 | }); 194 | 195 | store.set(0, { myKey: 0 }); 196 | store.set(1, { myKey: 1 }); 197 | 198 | store.clearAfter(0.5); 199 | state = store.getState(); 200 | expect(state).toEqual({ 201 | myKey: [{ time: 0, value: 0 }] 202 | }); 203 | }); 204 | }); 205 | --------------------------------------------------------------------------------