├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | test 7 | test.js 8 | demo/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2014 Matt DesLauriers 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # keyframes 2 | 3 | [![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges) 4 | 5 | Keyframe utils for a generic timeline. Imagine the low-level structure of After Effect's keyframes for a single control. The control (such as image position, color, etc) might use a 2-component vector (x, y), a color, or some other tweenable value. 6 | 7 | When a keyframe is added, the list is sorted to ensure the time stamps go from low to high. 8 | 9 | ```js 10 | var keyframes = require('keyframes')() 11 | 12 | keyframes.add({ time: 0, value: 0 }) 13 | keyframes.add({ time: 1.5, value: 50 }) 14 | keyframes.add({ time: 4, value: 100 }) 15 | 16 | //get the interpolated value at the given time stamp 17 | var eased = keyframes.value( timeStamp ) 18 | 19 | //get the closest keyframe within given (time) radius 20 | var closest = keyframes.nearest( timeStamp, radius ) 21 | 22 | //get the keyframe that matches the given time stamp 23 | var match = keyframes.get( timeStamp ) 24 | 25 | //the underlying array 26 | console.log( keyframes.frames ) 27 | ``` 28 | 29 | This does not make any assumptions about the unit of time. The `value` for a keyframe is generally a number or array of numbers, but you could also use a custom `interpolation` function (see below) if you want to support color strings or some other data type. 30 | 31 | ## Usage 32 | 33 | [![NPM](https://nodei.co/npm/keyframes.png)](https://nodei.co/npm/keyframes/) 34 | 35 | #### `var keys = require('keyframes')([frames][, sorted])` 36 | 37 | Creates a new set of keyframes, optionally with some frames to use as default. The frames will then be sorted in order of their time stamp. 38 | 39 | It's assumed the list of keyframes is unsorted; but if it already has been, you can pass `false` for `sorted` and it won't perform another sort. 40 | 41 | #### `keys.nearest(timeStamp[, radius])` 42 | 43 | Gets the nearest keyframe to the specified time stamp. If `radius` is not specified, this will return the closest keyframe. If `radius` is a number, this only returns the closest result within that distance; otherwise returns null. 44 | 45 | If radius is zero, this function behaves the same as `keys.get()`. 46 | 47 | If it can't find any keyframes, null is returned. 48 | 49 | #### `keys.nearestIndex(timeStamp[, radius])` 50 | 51 | Like `nearest()`, but returns an index to the `frames` array instead of a keyframe object. 52 | 53 | #### `keys.get(timeStamp)` 54 | #### `keys.getIndex(timeStamp)` 55 | 56 | Convenience methods to get the keyframe or index exactly at the given time stamp. Same as `keys.nearest(timeStamp, 0)` and `keys.nearestIndex(timeStamp, 0)`. 57 | 58 | #### `keys.value(timeStamp[, interpolator][, out])` 59 | 60 | Determines the value at the given time stamp, based on keyframe interpolation. 61 | 62 | If the time stamp falls on a keyframe, the value of that keyframe will be returned. If the time stamp falls between two keyframes, we use linear interpolation between the two. The result is clamped to the first and last keyframes; so if your first keyframe is at `2.5` and you're querying the value at `0.0`, the returned value will be of the keyframe at `2.5`. 63 | 64 | Here, `out` will get passed to the interpolator function. The default interpolator function is [lerp-array](https://www.npmjs.com/package/lerp-array) – this allows you to re-use arrays instead of creating new ones per frame. 65 | 66 | You can also pass your own `interpolator` function for custom features and easings. This will get called with `(startFrame, endFrame, t, out)`, which you can operate to return a value. This may be useful if your values are, for example, color strings and need a custom easing. 67 | 68 | Example: 69 | 70 | ```js 71 | var expoOut = require('eases/expo-out') 72 | var lerpArray = require('lerp-array') 73 | 74 | var keys = [ 75 | { time: 0, value: [ 0, 0 ] }, 76 | { time: 1, value: [ 10, 5 ] } 77 | ] 78 | 79 | // get interpolated value at time stamp 0.5 80 | var result = timeline.value(0.5) 81 | //=> [ 5, 2.5 ] 82 | 83 | // get the value with custom interpolation 84 | var result2 = timeline.value(0.5, function (a, b, t) { 85 | t = expoOut(t) // remap time 86 | return lerpArray(a, b, t) 87 | }) 88 | ``` 89 | 90 | #### `keys.next(timeStamp)` 91 | #### `keys.previous(timeStamp)` 92 | 93 | This is useful for jumping left and right in a timeline editor. From the given time stamp, it will return the next keyframe to the left or right. If none exist (i.e. we are at the bounds already) then null will be returned. 94 | 95 | #### `keys.add(frame)` 96 | 97 | Adds a "keyframe" object (which has `time` and `value` properties). When a new frame is added, the list is re-sorted. For bulk adds, you may want to access the `frames` object directly. 98 | 99 | #### `keys.splice(index, howmany, item1, ..., itemX)` 100 | 101 | Similar to `Array.splice`, this allows you to remove or insert keyframes within the array. If new keyframes have been inserted, the list will be re-sorted. 102 | 103 | #### `keys.sort()` 104 | 105 | To be called when you manually change the underlying `frames` structure (i.e. after a bulk add). 106 | 107 | #### `keys.clear()` 108 | 109 | Clears the list of keyframes. 110 | 111 | #### `keys.frames` 112 | 113 | The underlying array that holds keyframes. 114 | 115 | #### `keys.count` 116 | 117 | A getter for `keys.frames.length`. 118 | 119 | #### `keys.interpolation(time)` 120 | 121 | This is a more advanced method that returns the start and end indices and interpolation factor for a time stamp. The return value is an array with the following format: 122 | 123 | ```js 124 | [startFrameIndex, endFrameIndex, interpolationFactor] 125 | ``` 126 | 127 | If we are sitting on a keyframe, then start and end indices will be equal. If we have no keyframes, both indices will be -1. 128 | 129 | The returned array is *re-used* to reduce GC load. You should not store reference to it since it will change with subsequent calls. 130 | 131 | This is useful for tools that wish to separate the easing function (i.e. remap the value of t using [eases](https://nodei.co/npm/eases/)) from a user-defined interpolator (i.e. interpolating objects with `{x, y}` properties). 132 | 133 | ## License 134 | 135 | MIT, see [LICENSE.md](http://github.com/mattdesl/keyframes/blob/master/LICENSE.md) for details. 136 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | //Generic list of keyframes with timestamps and values 2 | 3 | var lerp = require('lerp-array') 4 | var range = require('unlerp') 5 | var vec3 = require('gl-vec3/set') 6 | 7 | var temp = [0, 0, 0] 8 | 9 | function sort(a, b) { 10 | return a.time - b.time 11 | } 12 | 13 | function Keyframes(frames, sorted) { 14 | if (!(this instanceof Keyframes)) 15 | return new Keyframes(frames, sorted) 16 | this.frames = frames||[] 17 | if (!sorted) 18 | this.sort() 19 | } 20 | 21 | //Finds the index of the nearest keyframe to the given time stamp. 22 | //If radius is specified, it will return the nearest only within that radius 23 | Keyframes.prototype.nearestIndex = function(time, radius) { 24 | radius = typeof radius === 'number' ? radius : Number.MAX_VALUE 25 | var minDist = Number.MAX_VALUE 26 | var nearest = -1 27 | for (var i=0; i=0; i--) { 80 | if (time >= this.frames[i].time) { 81 | prev = i 82 | break 83 | } 84 | } 85 | 86 | //start or end keyframes 87 | if (prev === -1 || prev === this.frames.length-1) { 88 | if (prev < 0) 89 | prev = 0 90 | return vec3(temp, prev, prev, 0) 91 | } 92 | else { 93 | var startFrame = this.frames[prev] 94 | var endFrame = this.frames[prev+1] 95 | 96 | //clamp and get range 97 | time = Math.max(startFrame.time, Math.min(time, endFrame.time)) 98 | var t = range(startFrame.time, endFrame.time, time) 99 | 100 | //provide interpolation factor 101 | return vec3(temp, prev, prev+1, t) 102 | } 103 | } 104 | 105 | Keyframes.prototype.next = function(time) { 106 | if (this.frames.length < 1) 107 | return null 108 | 109 | var cur = -1 110 | //get last keyframe to time 111 | for (var i=0; i=0; i--) { 127 | if (time > this.frames[i].time) { 128 | cur = i 129 | break 130 | } 131 | } 132 | return cur===-1 ? null : this.frames[cur] 133 | } 134 | 135 | //Adds a frame at the given time stamp 136 | Keyframes.prototype.add = function(frame) { 137 | this.frames.push(frame) 138 | this.sort() 139 | } 140 | 141 | //convenience for .frames.splice 142 | //if items are inserted, a sort will be applied after insertion 143 | Keyframes.prototype.splice = function(index, howmany, itemsN) { 144 | this.frames.splice.apply(this.frames, arguments) 145 | if (arguments.length > 2) 146 | this.sort() 147 | } 148 | 149 | //sorts the keyframes. you should do this after 150 | //adding new keyframes that are not in linear time 151 | Keyframes.prototype.sort = function() { 152 | this.frames.sort(sort) 153 | } 154 | 155 | //Clears the keyframe list 156 | Keyframes.prototype.clear = function() { 157 | this.frames.length = 0 158 | } 159 | 160 | Object.defineProperty(Keyframes.prototype, "count", { 161 | get: function() { 162 | return this.frames.length 163 | } 164 | }) 165 | 166 | module.exports = Keyframes -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keyframes", 3 | "version": "2.3.0", 4 | "description": "keyframe utils for a generic timeline", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "author": { 8 | "name": "Matt DesLauriers", 9 | "email": "dave.des@gmail.com" 10 | }, 11 | "dependencies": { 12 | "gl-vec3": "^1.0.2", 13 | "lerp-array": "^1.0.0", 14 | "unlerp": "^1.0.0" 15 | }, 16 | "devDependencies": { 17 | "tape": "~2.13.2" 18 | }, 19 | "scripts": { 20 | "test": "node test.js" 21 | }, 22 | "testling": { 23 | "files": "test.js", 24 | "browsers": [ 25 | "ie/6..latest", 26 | "chrome/22..latest", 27 | "firefox/16..latest", 28 | "safari/latest", 29 | "opera/11.0..latest", 30 | "iphone/6", 31 | "ipad/6", 32 | "android-browser/latest" 33 | ] 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "git://github.com/mattdesl/keyframes.git" 38 | }, 39 | "homepage": "https://github.com/mattdesl/keyframes", 40 | "bugs": { 41 | "url": "https://github.com/mattdesl/keyframes/issues" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape').test 2 | 3 | var Keyframes = require('./') 4 | 5 | test('simple', function (t) { 6 | var keys = [ 7 | { time: 0, value: [ 0, 0 ] }, 8 | { time: 1, value: [ 10, 5 ] } 9 | ] 10 | 11 | var timeline = Keyframes(keys) 12 | var array = [ 0, 0 ] 13 | var newArray = timeline.value(0.5, undefined, array) 14 | t.equal(newArray, array, 're-uses array') 15 | t.deepEqual(newArray, [ 5, 2.5 ], 'interpolates array') 16 | 17 | timeline.value(0.5, function (start, end, time, out) { 18 | out[0] = 50 19 | out[1] = 25 20 | t.deepEqual(start, keys[0], 'gets first') 21 | t.deepEqual(end, keys[1], 'gets last') 22 | t.equal(time, 0.5, 'gets time') 23 | }, array) 24 | t.deepEqual(array, [ 50, 25 ], 'gets custom interpolation') 25 | 26 | t.deepEqual(timeline.value(-1), [ 0, 0 ], 'first keyframe') 27 | t.deepEqual(timeline.value(1000), [ 10, 5 ], 'last keyframe') 28 | t.end() 29 | }) 30 | 31 | test('timeline controls', function(t) { 32 | var keys = [ 33 | { time: 2, value: 1 }, 34 | { time: 4, value: 2 }, 35 | { time: 0, value: 3 }, 36 | ] 37 | var sorted = [ 38 | { time: 0, value: 3 }, 39 | { time: 2, value: 1 }, 40 | { time: 4, value: 2 }, 41 | ] 42 | 43 | var c1 = Keyframes(keys) 44 | 45 | t.equal(c1.count, 3, 'count is correct') 46 | t.deepEqual(c1.frames, sorted, 'the keys are sorted') 47 | 48 | t.equal(c1.nearest(3.5, 0.4), null, 'nearest with small radius returns null' ) 49 | t.deepEqual(c1.nearest(3.5), sorted[2], 'nearest finds nearest keyframe' ) 50 | t.deepEqual(c1.get(1), null, 'get strict') 51 | t.deepEqual(c1.get(2), sorted[1], 'get strict') 52 | 53 | t.deepEqual(c1.next(-1), sorted[0], 'jumps to next keyframe') 54 | t.deepEqual(c1.next(0.5), sorted[1], 'jumps to next keyframe') 55 | t.deepEqual(c1.next(2), sorted[2], 'jumps to next keyframe') 56 | t.deepEqual(c1.next(4), null, 'jumps to next keyframe') 57 | t.deepEqual(c1.next(4.5), null, 'jumps to next keyframe') 58 | 59 | t.deepEqual(c1.previous(-1), null, 'jumps to previous keyframe') 60 | t.deepEqual(c1.previous(0.5), sorted[0], 'jumps to previous keyframe') 61 | t.deepEqual(c1.previous(2), sorted[0], 'jumps to previous keyframe') 62 | t.deepEqual(c1.previous(4), sorted[1], 'jumps to previous keyframe') 63 | t.deepEqual(c1.previous(4.5), sorted[2], 'jumps to previous keyframe') 64 | 65 | t.equal(c1.value(0), 3, 'interpolation') 66 | t.equal(c1.value(1), 2, 'interpolation') 67 | t.equal(c1.value(-1), 3, 'interpolation') 68 | t.equal(c1.value(4), 2, 'interpolation') 69 | t.equal(c1.value(3), 1.5, 'interpolation') 70 | t.equal(c1.value(5), 2, 'interpolation') 71 | 72 | 73 | var idx = c1.nearestIndex(4) 74 | c1.splice(idx, 1) 75 | sorted.splice(idx, 1) 76 | t.deepEqual(c1.frames, sorted, 'splice works') 77 | 78 | var newItem = { time: 10, value: 1 } 79 | c1.splice(0, 0, newItem) 80 | sorted.splice(0, 0, newItem) 81 | t.notDeepEqual(c1.frames, sorted, 'splice insert re-sorts array') 82 | 83 | var two = Keyframes([ { time: 0, value: 50 }]) 84 | t.equal(two.previous(100), two.frames[0]) 85 | t.equal(two.next(100), null) 86 | 87 | t.end() 88 | }) --------------------------------------------------------------------------------