├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── bundle.js ├── index.html ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | test 7 | test.js 8 | demo/ 9 | .npmignore 10 | LICENSE.md -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2015 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 | # spring-input 2 | 3 | [![stable](http://badges.github.io/stability-badges/dist/stable.svg)](http://github.com/badges/stability-badges) 4 | 5 | A utility to provide a springy mouse and touch input, similar to bouncy scroll panels in iOS. This can be used in a variety of applications, such as scrolling, rotating a 3D camera, flicking a 2D card, etc. 6 | 7 | Demo: 8 | 9 | [http://mattdesl.github.io/spring-input/](http://mattdesl.github.io/spring-input/) 10 | 11 | 12 | 13 | Adapted from [touch-scroll-physics](https://github.com/Jam3/touch-scroll-physics/), which is more application-specific than this module. 14 | 15 | ## Install 16 | 17 | ```sh 18 | npm install spring-input --save 19 | ``` 20 | 21 | ## Example 22 | 23 | See [test.js](./test.js) for a full example. 24 | 25 | ```js 26 | var createSpring = require('spring-input') 27 | 28 | // e.g. a slider along the x-axis 29 | var spring = createSpring({ 30 | min: 0, // min bound 31 | max: 1, // max bound 32 | edge: 0.1, // gutter size 33 | value: 0.5, // initial value 34 | damping: 0.25, // flick friction 35 | spring: 0.15 // "bounce back" friction 36 | }) 37 | 38 | function onDragStart (x, y) { 39 | spring.start(x) 40 | } 41 | 42 | function onDragMove (x, y) { 43 | spring.move(x) 44 | } 45 | 46 | function onDragEnd (x, y) { 47 | spring.end(x) 48 | } 49 | 50 | function onRequestAnimationFrame () { 51 | spring.update() 52 | } 53 | ``` 54 | 55 | This is a low-level module, intended to be used with your own input handling and update loop. This can be easily combined with the following modules: 56 | 57 | - [touches](https://github.com/Jam3/touches) - unified mouse / touch input and drag events 58 | - [mouse-wheel](http://npmjs.com/package/mouse-wheel) - cross-browser mouse wheel events 59 | - [raf-loop](https://www.npmjs.com/package/raf-loop) - a simple requestAnimationFrame loop 60 | 61 | ## Usage 62 | 63 | [![NPM](https://nodei.co/npm/spring-input.png)](https://www.npmjs.com/package/spring-input) 64 | 65 | #### `spring = createSpring([opt])` 66 | 67 | Creates a new sprint input with the optional settings: 68 | 69 | - `value` - the initial value, default 0 70 | - `min` - the minimum bound, default 0 (can be `-Infinity`) 71 | - `max` - the maximum bound, default 1 (can be `Infinity`) 72 | - `edge` - the relative edge gutter size, default 0 (i.e. no "bounce back") 73 | - `damping` - adjusts the friction when flicking; defualt 0.3 74 | - `spring` - adjusts the friction when bouncing back; default 0.2 75 | - `maxVelocity` - the maximum velocity in a flick, default 0.05 76 | 77 | All values can be changed during runtime, eg: 78 | 79 | ```js 80 | spring.max = newScrollHeight 81 | ``` 82 | 83 | #### `spring.start(value)` 84 | 85 | Called to trigger a "start" event with the specified `value`, such as the initial X mouse position. 86 | 87 | #### `spring.move(value)` 88 | 89 | Called to trigger a "move" event with the specified `value`, such as a new mouse X position. 90 | 91 | #### `spring.end()` 92 | 93 | Stops user input, allowing the value to be integrated and slide into place. 94 | 95 | #### `spring.update()` 96 | 97 | Integrates the spring. Should be called once per animation loop. 98 | 99 | #### `spring.value` 100 | 101 | The currently integrated value. 102 | 103 | #### `spring.velocity` 104 | 105 | The current velocity. 106 | 107 | ## See Also 108 | 109 | - [touch-scroll-physics](https://github.com/Jam3/touch-scroll-physics/) very similar, but more application-specific 110 | 111 | ## License 112 | 113 | MIT, see [LICENSE.md](http://github.com/mattdesl/spring-input/blob/master/LICENSE.md) for details. 114 | -------------------------------------------------------------------------------- /bundle.js: -------------------------------------------------------------------------------- 1 | !function t(e,n,r){function i(o,u){if(!n[o]){if(!e[o]){var a="function"==typeof require&&require;if(!u&&a)return a(o,!0);if(s)return s(o,!0);var c=new Error("Cannot find module '"+o+"'");throw c.code="MODULE_NOT_FOUND",c}var h=n[o]={exports:{}};e[o][0].call(h.exports,function(t){var n=e[o][1][t];return i(n?n:t)},h,h.exports,t,e,n,r)}return n[o].exports}for(var s="function"==typeof require&&require,o=0;othis.max,n=!this.interacting,r=0;t?(this.velocity=0,this.inputDelta<0&&0!==this.edge&&(this.inputDelta*=1-Math.abs(this.value-this.min)/this.edge)):e&&(this.velocity=0,this.inputDelta>0&&0!==this.edge&&(this.inputDelta*=1-Math.abs(this.value-this.max)/this.edge)),n&&(t?r=this.value-this.min:e&&(r=this.value-this.max),r*=this.spring),this.value+=this.inputDelta,this.inputDelta=0,this.interacting||(this.value+=this.velocity),this.velocity*=1-this.damping,this.value-=r,this.value=s(this.value,this.min-this.edge,this.max+this.edge)},i.prototype.start=function(t){this.interacting=!0,this.velocity=0,this.inputDelta=0,this.lastInput=t},i.prototype.move=function(t){if(this.interacting){var e=t-this.lastInput;this.value+e>this.max+this.edge&&(t=Math.min(t,this.max+this.edge)),this.value+e0&&(this.velocity=Math.min(this.velocity+this.inputDelta,n))}},i.prototype.end=function(){this.interacting=!1}},{clamp:4,defined:5}],2:[function(t,e,n){function r(){this._events=this._events||{},this._maxListeners=this._maxListeners||void 0}function i(t){return"function"==typeof t}function s(t){return"number"==typeof t}function o(t){return"object"==typeof t&&null!==t}function u(t){return void 0===t}e.exports=r,r.EventEmitter=r,r.prototype._events=void 0,r.prototype._maxListeners=void 0,r.defaultMaxListeners=10,r.prototype.setMaxListeners=function(t){if(!s(t)||0>t||isNaN(t))throw TypeError("n must be a positive number");return this._maxListeners=t,this},r.prototype.emit=function(t){var e,n,r,s,a,c;if(this._events||(this._events={}),"error"===t&&(!this._events.error||o(this._events.error)&&!this._events.error.length)){if(e=arguments[1],e instanceof Error)throw e;throw TypeError('Uncaught, unspecified "error" event.')}if(n=this._events[t],u(n))return!1;if(i(n))switch(arguments.length){case 1:n.call(this);break;case 2:n.call(this,arguments[1]);break;case 3:n.call(this,arguments[1],arguments[2]);break;default:for(r=arguments.length,s=new Array(r-1),a=1;r>a;a++)s[a-1]=arguments[a];n.apply(this,s)}else if(o(n)){for(r=arguments.length,s=new Array(r-1),a=1;r>a;a++)s[a-1]=arguments[a];for(c=n.slice(),r=c.length,a=0;r>a;a++)c[a].apply(this,s)}return!0},r.prototype.addListener=function(t,e){var n;if(!i(e))throw TypeError("listener must be a function");if(this._events||(this._events={}),this._events.newListener&&this.emit("newListener",t,i(e.listener)?e.listener:e),this._events[t]?o(this._events[t])?this._events[t].push(e):this._events[t]=[this._events[t],e]:this._events[t]=e,o(this._events[t])&&!this._events[t].warned){var n;n=u(this._maxListeners)?r.defaultMaxListeners:this._maxListeners,n&&n>0&&this._events[t].length>n&&(this._events[t].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[t].length),"function"==typeof console.trace&&console.trace())}return this},r.prototype.on=r.prototype.addListener,r.prototype.once=function(t,e){function n(){this.removeListener(t,n),r||(r=!0,e.apply(this,arguments))}if(!i(e))throw TypeError("listener must be a function");var r=!1;return n.listener=e,this.on(t,n),this},r.prototype.removeListener=function(t,e){var n,r,s,u;if(!i(e))throw TypeError("listener must be a function");if(!this._events||!this._events[t])return this;if(n=this._events[t],s=n.length,r=-1,n===e||i(n.listener)&&n.listener===e)delete this._events[t],this._events.removeListener&&this.emit("removeListener",t,e);else if(o(n)){for(u=s;u-->0;)if(n[u]===e||n[u].listener&&n[u].listener===e){r=u;break}if(0>r)return this;1===n.length?(n.length=0,delete this._events[t]):n.splice(r,1),this._events.removeListener&&this.emit("removeListener",t,e)}return this},r.prototype.removeAllListeners=function(t){var e,n;if(!this._events)return this;if(!this._events.removeListener)return 0===arguments.length?this._events={}:this._events[t]&&delete this._events[t],this;if(0===arguments.length){for(e in this._events)"removeListener"!==e&&this.removeAllListeners(e);return this.removeAllListeners("removeListener"),this._events={},this}if(n=this._events[t],i(n))this.removeListener(t,n);else for(;n.length;)this.removeListener(t,n[n.length-1]);return delete this._events[t],this},r.prototype.listeners=function(t){var e;return e=this._events&&this._events[t]?i(this._events[t])?[this._events[t]]:this._events[t].slice():[]},r.listenerCount=function(t,e){var n;return n=t._events&&t._events[e]?i(t._events[e])?1:t._events[e].length:0}},{}],3:[function(t,e,n){function r(){h=!1,u.length?c=u.concat(c):f=-1,c.length&&i()}function i(){if(!h){var t=setTimeout(r);h=!0;for(var e=c.length;e;){for(u=c,c=[];++f1)for(var n=1;ne?e>t?e:t>n?n:t:n>t?n:t>e?e:t}e.exports=r},{}],5:[function(t,e,n){e.exports=function(){for(var t=0;t 2 | 3 | 4 | 5 | 6 | spring-input 7 | 8 | 62 | 63 | 64 |
swipe the dot
65 |
66 |
67 |
68 |
69 |
70 |
71 | 72 | 73 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var clamp = require('clamp') 2 | var defined = require('defined') 3 | 4 | module.exports = createSpringInput 5 | function createSpringInput (opt) { 6 | return new SpringInput(opt) 7 | } 8 | 9 | function SpringInput (opt) { 10 | opt = opt || {} 11 | 12 | this.velocity = 0 13 | this.lastInput = 0 14 | this.interacting = false 15 | this.inputDelta = 0 16 | 17 | this.value = opt.value || 0 18 | this.min = opt.min || 0 19 | this.max = defined(opt.max, 1) 20 | this.edge = opt.edge || 0 21 | this.damping = defined(opt.damping, 0.3) 22 | this.spring = defined(opt.spring, 0.2) 23 | this.maxVelocity = defined(opt.maxVelocity, 0.01) 24 | } 25 | 26 | SpringInput.prototype.update = function () { 27 | var isBefore = this.value < this.min 28 | var isAfter = this.value > this.max 29 | var dipping = !this.interacting 30 | var dip = 0 31 | 32 | // ease input at edges 33 | if (isBefore) { 34 | this.velocity = 0 35 | if (this.inputDelta < 0 && this.edge !== 0) { 36 | this.inputDelta *= 1 - Math.abs(this.value - this.min) / this.edge 37 | } 38 | } else if (isAfter) { 39 | this.velocity = 0 40 | if (this.inputDelta > 0 && this.edge !== 0) { 41 | this.inputDelta *= 1 - Math.abs(this.value - this.max) / this.edge 42 | } 43 | } 44 | 45 | // dip back to edge 46 | if (dipping) { 47 | if (isBefore) { 48 | dip = this.value - this.min 49 | } else if (isAfter) { 50 | dip = this.value - this.max 51 | } 52 | dip *= this.spring 53 | } 54 | 55 | // integrate 56 | this.value += this.inputDelta 57 | this.inputDelta = 0 58 | if (!this.interacting) { 59 | this.value += this.velocity 60 | } 61 | this.velocity *= 1 - this.damping 62 | this.value -= dip 63 | this.value = clamp(this.value, this.min - this.edge, this.max + this.edge) 64 | } 65 | 66 | SpringInput.prototype.start = function (value) { 67 | this.interacting = true 68 | this.velocity = 0 69 | this.inputDelta = 0 70 | this.lastInput = value 71 | } 72 | 73 | SpringInput.prototype.move = function (value) { 74 | if (this.interacting) { 75 | var delta = value - this.lastInput 76 | // avoid getting out of sync when user is at gutter 77 | if (this.value + delta > this.max + this.edge) { 78 | value = Math.min(value, this.max + this.edge) 79 | } 80 | if (this.value + delta < this.min - this.edge) { 81 | value = Math.max(value, this.min - this.edge) 82 | } 83 | this.inputDelta = delta 84 | this.lastInput = value 85 | 86 | // clamp to max velocity 87 | var maxVelocity = Math.abs(this.maxVelocity) 88 | if (this.inputDelta < 0) { 89 | this.velocity = Math.max(this.velocity + this.inputDelta, -maxVelocity) 90 | } else if (this.inputDelta > 0) { 91 | this.velocity = Math.min(this.velocity + this.inputDelta, maxVelocity) 92 | } 93 | } 94 | } 95 | 96 | SpringInput.prototype.end = function () { 97 | this.interacting = false 98 | } 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spring-input", 3 | "version": "2.0.0", 4 | "description": "integrates scroll and flick physics", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "author": { 8 | "name": "Matt DesLauriers", 9 | "email": "dave.des@gmail.com", 10 | "url": "https://github.com/mattdesl" 11 | }, 12 | "dependencies": { 13 | "clamp": "^1.0.1", 14 | "defined": "^1.0.0" 15 | }, 16 | "devDependencies": { 17 | "browserify": "^11.2.0", 18 | "budo": "^5.1.0", 19 | "dom-css": "^1.1.2", 20 | "faucet": "0.0.1", 21 | "garnish": "^3.2.1", 22 | "new-array": "^1.0.0", 23 | "raf-loop": "^1.1.3", 24 | "tape": "^4.2.0", 25 | "touches": "^1.2.0", 26 | "uglify-js": "^2.4.24" 27 | }, 28 | "scripts": { 29 | "test": "node test.js | faucet", 30 | "start": "budo test.js:bundle.js --live | garnish", 31 | "build": "browserify test.js | uglifyjs -cm > bundle.js" 32 | }, 33 | "keywords": [ 34 | "simple", 35 | "scroll", 36 | "flick", 37 | "touch", 38 | "swipe", 39 | "bounce", 40 | "bouncy", 41 | "gesture", 42 | "spring", 43 | "springy", 44 | "scrolling", 45 | "physics", 46 | "integration" 47 | ], 48 | "repository": { 49 | "type": "git", 50 | "url": "git://github.com/mattdesl/spring-input.git" 51 | }, 52 | "homepage": "https://github.com/mattdesl/spring-input", 53 | "bugs": { 54 | "url": "https://github.com/mattdesl/spring-input/issues" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var loop = require('raf-loop') 2 | var css = require('dom-css') 3 | var touches = require('touches') 4 | 5 | var input = require('./')({ 6 | min: 0, // min bound 7 | max: 1, // max bound 8 | edge: 0.15, // gutter size 9 | value: 0.5, // initial value 10 | damping: 0.25, // flick friction 11 | spring: 0.2 // "bounce back" friction 12 | }) 13 | 14 | var width = 200 15 | var padding = width * input.edge 16 | 17 | var gutter = document.querySelector('.gutter') 18 | var container = document.querySelector('.container') 19 | var cursor = document.querySelector('.cursor') 20 | var text = document.querySelector('.text') 21 | 22 | css(gutter, 'width', width + padding * 2) 23 | 24 | css(container, { 25 | width: width, 26 | left: padding 27 | }) 28 | 29 | css(cursor, 'transform', 'translate(-50%, -50%)') 30 | 31 | var dragging = false 32 | touches(window, { target: container, filtered: true }) 33 | .on('start', function (ev, pos) { 34 | dragging = true 35 | input.start(normalize(pos)) 36 | }) 37 | .on('move', function (ev, pos) { 38 | if (!dragging) return 39 | ev.preventDefault() 40 | input.move(normalize(pos)) 41 | }) 42 | .on('end', function (ev, pos) { 43 | dragging = false 44 | input.end(normalize(pos)) 45 | }) 46 | 47 | function normalize (pos) { 48 | return pos[0] / width 49 | } 50 | 51 | loop(tick).start() 52 | tick() 53 | 54 | function tick () { 55 | input.update() 56 | var x = input.value * width 57 | css(cursor, 'left', x) 58 | text.innerText = input.value.toFixed(2) 59 | } 60 | --------------------------------------------------------------------------------