├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── demo.js ├── index.html ├── index.js └── package.json /.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/ 9 | .npmignore 10 | LICENSE.md -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2015 Jam3 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 | # touch-pinch 2 | 3 | [![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges) 4 | 5 | A low-level utility for two-finger pinch and panning gestures. 6 | 7 | ## Install 8 | 9 | ```sh 10 | npm install touch-pinch --save 11 | ``` 12 | 13 | ## Example 14 | 15 | The following example scales by the delta difference in a two-finger pinch gesture. 16 | 17 | ```js 18 | var pinch = require('touch-pinch') 19 | 20 | var scale = 1 21 | pinch(window) 22 | .on('change', function (dist, prev) { 23 | scale += (dist - prev) 24 | }) 25 | ``` 26 | 27 | ## Usage 28 | 29 | [![NPM](https://nodei.co/npm/touch-pinch.png)](https://www.npmjs.com/package/touch-pinch) 30 | 31 | #### `pinch = touchPinch([target])` 32 | 33 | Creates a new `pinch` emitter with the optional `target` element, which defaults to `window`. 34 | 35 | ### events 36 | 37 | #### `pinch.on('start', fn)` 38 | 39 | Called when the pinch event begins; i.e. when two fingers are active on screen. 40 | 41 | Called with `fn(distance)`, which is the initial Euclidean distance between these two points. 42 | 43 | #### `pinch.on('change', fn)` 44 | 45 | Called when the pinch changes; i.e. one or both of the fingers in the pinch have moved. 46 | 47 | Called with `fn(distance, prevDistance)`, where `distance` is the new Euclidean distance, and `prevDistance` is the last recorded distance. Often, you will use this delta to compute a new scale: 48 | 49 | ```js 50 | scale += (distance - prevDistance) 51 | ``` 52 | 53 | #### `pinch.on('end', fn)` 54 | 55 | Called when the pinch is finished; i.e. one or both of the active fingers have been lifted from the screen. 56 | 57 | #### `pinch.on('place', fn)` 58 | 59 | Called before the pinch has started, to indicate that a new finger has been placed on screen (with a maximum of two fingers). 60 | 61 | Called with `fn(newTouch, otherTouch)`, where `newTouch` is the new TouchEvent. `otherTouch` is the touch event that represents the other finger on screen, or `undefined` if none exists. 62 | 63 | #### `pinch.on('lift', fn)` 64 | 65 | Called before the pinch has ended, to indicate that a previoulsy pinching finger has been lifted. 66 | 67 | Called with `fn(removedTouch, otherTouch)`, where `removedTouch` is the TouchEvent that was removed from the screen. `otherTouch` is the touch event for the other finger on screen, or `undefined` if none exists. 68 | 69 | ### members 70 | 71 | #### `pinch.pinching` 72 | 73 | A read-only boolean; `true` only if the user is currently pinching (two fingers on screen). 74 | 75 | #### `pinch.fingers` 76 | 77 | An array of two elements, which are initially both `null` (representing "no finger"). The elements are the two possible fingers in a pinch event. 78 | 79 | When a finger is present on screen, the element in the array will contain: 80 | 81 | ```js 82 | { 83 | position: [x, y], // the offset relative to target 84 | touch: TouchEvent // the associated event 85 | } 86 | ``` 87 | 88 | The order is maintained; so if you place a finger, then place a second, then remove the first finger, `pinch.fingers` will look like this: 89 | 90 | ```js 91 | [ null, { position, touch } ] 92 | ``` 93 | 94 | ### methods 95 | 96 | #### `pinch.indexOfTouch(touchEvent)` 97 | 98 | Returns the index of `touchEvent` within the `pinch.fingers` array. This can be used to determine 99 | 100 | ## License 101 | 102 | MIT, see [LICENSE.md](http://github.com/Jam3/touch-pinch/blob/master/LICENSE.md) for details. 103 | -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | var css = require('dom-css') 2 | 3 | var pxSize = 50 4 | var div = document.createElement('div') 5 | document.body.appendChild(div) 6 | css(div, { 7 | width: pxSize, 8 | height: pxSize, 9 | background: 'blue', 10 | position: 'absolute', 11 | left: (document.documentElement.clientWidth - pxSize) / 2, 12 | top: (document.documentElement.clientHeight - pxSize) / 2, 13 | }) 14 | 15 | var pinch = require('./')(window) 16 | 17 | var scale = 1 18 | 19 | window.addEventListener('touchstart', function (ev) { 20 | ev.preventDefault() // no scrolling 21 | }) 22 | 23 | pinch.on('start', function () { 24 | css(div, 'background', 'green') 25 | }) 26 | 27 | pinch.on('end', function () { 28 | css(div, 'background', 'blue') 29 | }) 30 | 31 | pinch.on('change', function (current, prev) { 32 | var delta = (current - prev) * 0.01 33 | scale += delta 34 | css(div, 'transform', 'scale(' + scale.toFixed(5) + ')') 35 | }) 36 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | touch-pinch 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var getDistance = require('gl-vec2/distance') 2 | var EventEmitter = require('events').EventEmitter 3 | var dprop = require('dprop') 4 | var eventOffset = require('mouse-event-offset') 5 | 6 | module.exports = touchPinch 7 | function touchPinch (target) { 8 | target = target || window 9 | 10 | var emitter = new EventEmitter() 11 | var fingers = [ null, null ] 12 | var activeCount = 0 13 | 14 | var lastDistance = 0 15 | var ended = false 16 | var enabled = false 17 | 18 | // some read-only values 19 | Object.defineProperties(emitter, { 20 | pinching: dprop(function () { 21 | return activeCount === 2 22 | }), 23 | 24 | fingers: dprop(function () { 25 | return fingers 26 | }) 27 | }) 28 | 29 | enable() 30 | emitter.enable = enable 31 | emitter.disable = disable 32 | emitter.indexOfTouch = indexOfTouch 33 | return emitter 34 | 35 | function indexOfTouch (touch) { 36 | var id = touch.identifier 37 | for (var i = 0; i < fingers.length; i++) { 38 | if (fingers[i] && 39 | fingers[i].touch && 40 | fingers[i].touch.identifier === id) { 41 | return i 42 | } 43 | } 44 | return -1 45 | } 46 | 47 | function enable () { 48 | if (enabled) return 49 | enabled = true 50 | target.addEventListener('touchstart', onTouchStart, false) 51 | target.addEventListener('touchmove', onTouchMove, false) 52 | target.addEventListener('touchend', onTouchRemoved, false) 53 | target.addEventListener('touchcancel', onTouchRemoved, false) 54 | } 55 | 56 | function disable () { 57 | if (!enabled) return 58 | enabled = false 59 | activeCount = 0 60 | fingers[0] = null 61 | fingers[1] = null 62 | lastDistance = 0 63 | ended = false 64 | target.removeEventListener('touchstart', onTouchStart, false) 65 | target.removeEventListener('touchmove', onTouchMove, false) 66 | target.removeEventListener('touchend', onTouchRemoved, false) 67 | target.removeEventListener('touchcancel', onTouchRemoved, false) 68 | } 69 | 70 | function onTouchStart (ev) { 71 | for (var i = 0; i < ev.changedTouches.length; i++) { 72 | var newTouch = ev.changedTouches[i] 73 | var id = newTouch.identifier 74 | var idx = indexOfTouch(id) 75 | 76 | if (idx === -1 && activeCount < 2) { 77 | var first = activeCount === 0 78 | 79 | // newest and previous finger (previous may be undefined) 80 | var newIndex = fingers[0] ? 1 : 0 81 | var oldIndex = fingers[0] ? 0 : 1 82 | var newFinger = new Finger() 83 | 84 | // add to stack 85 | fingers[newIndex] = newFinger 86 | activeCount++ 87 | 88 | // update touch event & position 89 | newFinger.touch = newTouch 90 | eventOffset(newTouch, target, newFinger.position) 91 | 92 | var oldTouch = fingers[oldIndex] ? fingers[oldIndex].touch : undefined 93 | emitter.emit('place', newTouch, oldTouch) 94 | 95 | if (!first) { 96 | var initialDistance = computeDistance() 97 | ended = false 98 | emitter.emit('start', initialDistance) 99 | lastDistance = initialDistance 100 | } 101 | } 102 | } 103 | } 104 | 105 | function onTouchMove (ev) { 106 | var changed = false 107 | for (var i = 0; i < ev.changedTouches.length; i++) { 108 | var movedTouch = ev.changedTouches[i] 109 | var idx = indexOfTouch(movedTouch) 110 | if (idx !== -1) { 111 | changed = true 112 | fingers[idx].touch = movedTouch // avoid caching touches 113 | eventOffset(movedTouch, target, fingers[idx].position) 114 | } 115 | } 116 | 117 | if (activeCount === 2 && changed) { 118 | var currentDistance = computeDistance() 119 | emitter.emit('change', currentDistance, lastDistance) 120 | lastDistance = currentDistance 121 | } 122 | } 123 | 124 | function onTouchRemoved (ev) { 125 | for (var i = 0; i < ev.changedTouches.length; i++) { 126 | var removed = ev.changedTouches[i] 127 | var idx = indexOfTouch(removed) 128 | 129 | if (idx !== -1) { 130 | fingers[idx] = null 131 | activeCount-- 132 | var otherIdx = idx === 0 ? 1 : 0 133 | var otherTouch = fingers[otherIdx] ? fingers[otherIdx].touch : undefined 134 | emitter.emit('lift', removed, otherTouch) 135 | } 136 | } 137 | 138 | if (!ended && activeCount !== 2) { 139 | ended = true 140 | emitter.emit('end') 141 | } 142 | } 143 | 144 | function computeDistance () { 145 | if (activeCount < 2) return 0 146 | return getDistance(fingers[0].position, fingers[1].position) 147 | } 148 | } 149 | 150 | function Finger () { 151 | this.position = [0, 0] 152 | this.touch = null 153 | } 154 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "touch-pinch", 3 | "version": "1.0.1", 4 | "description": "minimal two-finger pinch gesture detection", 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 | "dprop": "^1.0.0", 14 | "events": "^1.0.2", 15 | "gl-vec2": "^1.0.0", 16 | "mouse-event-offset": "^3.0.2" 17 | }, 18 | "devDependencies": { 19 | "budo": "^5.1.0", 20 | "dom-css": "^1.1.2", 21 | "garnish": "^3.2.1" 22 | }, 23 | "scripts": { 24 | "test": "node test.js", 25 | "start": "budo demo.js:bundle.js --live | garnish" 26 | }, 27 | "keywords": [ 28 | "touch", 29 | "pinch", 30 | "detection", 31 | "zoom", 32 | "pan" 33 | ], 34 | "repository": { 35 | "type": "git", 36 | "url": "git://github.com/Jam3/touch-pinch.git" 37 | }, 38 | "homepage": "https://github.com/Jam3/touch-pinch", 39 | "bugs": { 40 | "url": "https://github.com/Jam3/touch-pinch/issues" 41 | } 42 | } 43 | --------------------------------------------------------------------------------