├── History.md ├── .gitignore ├── .tm_properties ├── tests ├── tests.css ├── long-press.html ├── swipe.html ├── tap.html ├── pinch.html ├── rotation.html ├── pan.html └── combined.html ├── component.json ├── LICENSE ├── view.js ├── long-press.js ├── tap.js ├── swipe.js ├── pinch.js ├── rotation.js ├── pan.js ├── README.md └── gesture-recognizer.js /History.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | components 2 | build -------------------------------------------------------------------------------- /.tm_properties: -------------------------------------------------------------------------------- 1 | exclude = '{$exclude,build,components}' -------------------------------------------------------------------------------- /tests/tests.css: -------------------------------------------------------------------------------- 1 | 2 | #target { 3 | position: absolute; 4 | top: 200px; 5 | left: 200px; 6 | width: 200px; 7 | height: 200px; 8 | background-image: linear-gradient(red, blue); 9 | } 10 | -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gesture-recognizer", 3 | "repo": "graouts/gesture-recognizer", 4 | "description": "JavaScript implementation of UIGestureRecognizer", 5 | "version": "0.0.1", 6 | "keywords": [], 7 | "dependencies": { 8 | "graouts/dom-events": "*", 9 | "graouts/geometry": "*" 10 | }, 11 | "development": {}, 12 | "license": "MIT", 13 | "main": "gesture-recognizer.js", 14 | "scripts": [ 15 | "gesture-recognizer.js", 16 | "view.js", 17 | "long-press.js", 18 | "pan.js", 19 | "pinch.js", 20 | "rotation.js", 21 | "swipe.js", 22 | "tap.js" 23 | ] 24 | } -------------------------------------------------------------------------------- /tests/long-press.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | LongPressGestureRecognizer 5 | 6 | 7 | 24 | 25 | 26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/swipe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SwipeGestureRecognizer 5 | 6 | 7 | 25 | 26 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 by Takashi Okamoto, BuzaMoto 2 | Copyright (C) 2014 by Antoine Quint 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 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. -------------------------------------------------------------------------------- /tests/tap.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TapGestureRecognizer 5 | 6 | 7 | 25 | 26 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/pinch.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PinchGestureRecognizer 5 | 6 | 7 | 29 | 30 | 31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/rotation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RotationGestureRecognizer 5 | 6 | 7 | 29 | 30 | 31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/pan.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PanGestureRecognizer 5 | 6 | 7 | 32 | 33 | 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /view.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = GestureView; 3 | 4 | var GestureRecognizer = require("./gesture-recognizer"); 5 | 6 | function GestureView(elementOrId) 7 | { 8 | this.element = (typeof elementOrId === 'string') ? document.getElementById(elementOrId) : elementOrId; 9 | this.scale = 1; 10 | this.rotation = this.x = this.y = this.z = 0; 11 | this.transform = {}; 12 | }; 13 | 14 | GestureView.prototype = { 15 | constructor: GestureView, 16 | 17 | set transform(obj) { 18 | this._x = this._getDefined(obj.x, this._x, this.x); 19 | this._y = this._getDefined(obj.y, this._y, this.y); 20 | this._z = this._getDefined(obj.z, this._z, this.z); 21 | this._scale = this._getDefined(obj.scale, this._scale, this.scale); 22 | this._rotation = this._getDefined(obj.rotation, this._rotation, this.rotation); 23 | this.element.style.webkitTransform = "translate3d(" + 24 | this._x + "px, " + this._y + "px, " + this._z + ") " + 25 | "scale(" + this._scale + ") " + 26 | "rotate(" + this._rotation + "deg)"; 27 | }, 28 | 29 | addGestureRecognizer: function(recognizer) { 30 | recognizer.target = this.element; 31 | recognizer.view = this; 32 | }, 33 | 34 | _getDefined: function() { 35 | for (var i = 0; i < arguments.length; i++) { 36 | if (typeof arguments[i] != "undefined") return arguments[i]; 37 | } 38 | return arguments[arguments.length-1]; 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /long-press.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = LongPressGestureRecognizer; 3 | 4 | var GestureRecognizer = require("./gesture-recognizer"); 5 | 6 | function LongPressGestureRecognizer() 7 | { 8 | // FIXME: implement .numberOfTapsRequired 9 | this.allowableMovement = 10; 10 | this.minimumPressDuration = 500; 11 | this.numberOfTouchesRequired = 1; 12 | 13 | GestureRecognizer.call(this); 14 | } 15 | 16 | LongPressGestureRecognizer.prototype = { 17 | constructor: LongPressGestureRecognizer, 18 | __proto__: GestureRecognizer.prototype, 19 | 20 | touchesBegan: function(event) 21 | { 22 | if (event.currentTarget !== this.target) 23 | return; 24 | 25 | event.preventDefault(); 26 | 27 | GestureRecognizer.prototype.touchesBegan.call(this, event); 28 | 29 | if (this.numberOfTouches !== this.numberOfTouchesRequired) { 30 | this.enterFailedState(); 31 | return; 32 | } 33 | 34 | this._startPoint = this.locationInElement(); 35 | 36 | this._timerId = window.setTimeout(this.enterRecognizedState.bind(this), this.minimumPressDuration); 37 | }, 38 | 39 | touchesMoved: function(event) 40 | { 41 | event.preventDefault(); 42 | 43 | if (this._startPoint.distanceToPoint(this.locationInElement()) > this.allowableMovement) 44 | this.enterFailedState(); 45 | }, 46 | 47 | touchesEnded: function(event) 48 | { 49 | event.preventDefault(); 50 | 51 | if (this.numberOfTouches !== this.numberOfTouchesRequired) 52 | this.enterFailedState(); 53 | }, 54 | 55 | reset: function() 56 | { 57 | window.clearTimeout(this._timerId); 58 | delete this._timerId; 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /tap.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = TapGestureRecognizer; 3 | 4 | var GestureRecognizer = require("./gesture-recognizer"), 5 | Point = require("geometry/point.js"); 6 | 7 | function TapGestureRecognizer() 8 | { 9 | this.numberOfTapsRequired = 1; 10 | this.numberOfTouchesRequired = 1; 11 | 12 | GestureRecognizer.call(this); 13 | } 14 | 15 | TapGestureRecognizer.MoveTolerance = 40; 16 | TapGestureRecognizer.WaitingForNextTapToStartTimeout = 350; 17 | TapGestureRecognizer.WaitingForTapCompletionTimeout = 750; 18 | 19 | TapGestureRecognizer.prototype = { 20 | constructor: TapGestureRecognizer, 21 | __proto__: GestureRecognizer.prototype, 22 | 23 | touchesBegan: function(event) 24 | { 25 | if (event.currentTarget !== this.target) 26 | return; 27 | 28 | GestureRecognizer.prototype.touchesBegan.call(this, event); 29 | 30 | if (this.numberOfTouches !== this.numberOfTouchesRequired) { 31 | this.enterFailedState(); 32 | return; 33 | } 34 | 35 | this._startPoint = GestureRecognizer.prototype.locationInElement.call(this); 36 | 37 | this._rewindTimer(TapGestureRecognizer.WaitingForTapCompletionTimeout); 38 | 39 | event.preventDefault(); 40 | }, 41 | 42 | touchesMoved: function(event) 43 | { 44 | if (!GestureRecognizer.SupportsTouches) { 45 | event.preventDefault(); 46 | this.enterFailedState(); 47 | return; 48 | } 49 | 50 | if (this._startPoint.distanceToPoint(GestureRecognizer.prototype.locationInElement.call(this)) > TapGestureRecognizer.MoveTolerance) 51 | this.enterFailedState(); 52 | }, 53 | 54 | touchesEnded: function(event) 55 | { 56 | this._taps++; 57 | 58 | if (this._taps === this.numberOfTapsRequired) { 59 | this.enterRecognizedState(); 60 | this.reset(); 61 | } 62 | 63 | this._rewindTimer(TapGestureRecognizer.WaitingForNextTapToStartTimeout); 64 | }, 65 | 66 | reset: function() 67 | { 68 | this._taps = 0; 69 | this._clearTimer(); 70 | }, 71 | 72 | locationInElement: function(element) 73 | { 74 | var p = this._startPoint || new Point; 75 | 76 | if (!element) 77 | return p; 78 | 79 | var wkPoint = window.webkitConvertPointFromPageToNode(element, new WebKitPoint(p.x, p.y)); 80 | return new Point(wkPoint.x, wkPoint.y); 81 | }, 82 | 83 | // Private 84 | 85 | _clearTimer: function() 86 | { 87 | window.clearTimeout(this._timerId); 88 | delete this._timerId; 89 | }, 90 | 91 | _rewindTimer: function(timeout) 92 | { 93 | this._clearTimer(); 94 | this._timerId = window.setTimeout(this._timerFired.bind(this), timeout); 95 | }, 96 | 97 | _timerFired: function() 98 | { 99 | this.enterFailedState(); 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /swipe.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = SwipeGestureRecognizer; 3 | 4 | var GestureRecognizer = require("./gesture-recognizer"), 5 | Point = require("geometry/point.js"); 6 | 7 | function SwipeGestureRecognizer() 8 | { 9 | this.numberOfTouchesRequired = 1; 10 | this.direction = SwipeGestureRecognizer.Directions.Right; 11 | 12 | GestureRecognizer.call(this); 13 | } 14 | 15 | SwipeGestureRecognizer.MinimumDistance = 100; 16 | SwipeGestureRecognizer.Directions = { 17 | Right : 1 << 0, 18 | Left : 1 << 1, 19 | Up : 1 << 2, 20 | Down : 1 << 3 21 | }; 22 | 23 | SwipeGestureRecognizer.prototype = { 24 | constructor: SwipeGestureRecognizer, 25 | __proto__: GestureRecognizer.prototype, 26 | 27 | touchesBegan: function(event) 28 | { 29 | if (event.currentTarget !== this.target) 30 | return; 31 | 32 | if (this.numberOfTouchesRequired === this.numberOfTouches) { 33 | event.preventDefault(); 34 | GestureRecognizer.prototype.touchesBegan.call(this, event); 35 | this._translationOrigin = this.locationInElement(); 36 | } else 37 | this.enterFailedState(); 38 | }, 39 | 40 | touchesMoved: function(event) 41 | { 42 | if (this.numberOfTouchesRequired !== this.numberOfTouches) { 43 | this.enterFailedState(); 44 | return; 45 | } 46 | 47 | event.preventDefault(); 48 | 49 | var point = this.locationInElement(); 50 | var translation = new Point(point.x - this._translationOrigin.x, point.y - this._translationOrigin.y); 51 | 52 | if (this.state !== GestureRecognizer.States.Recognized && this.direction === SwipeGestureRecognizer.Directions.Right) { 53 | if (translation.x > SwipeGestureRecognizer.MinimumDistance && Math.abs(translation.x) > Math.abs(translation.y)) 54 | this.enterRecognizedState(); 55 | } 56 | 57 | if (this.state !== GestureRecognizer.States.Recognized && this.direction === SwipeGestureRecognizer.Directions.Left) { 58 | if (translation.x < -SwipeGestureRecognizer.MinimumDistance && Math.abs(translation.x) > Math.abs(translation.y)) 59 | this.enterRecognizedState(); 60 | } 61 | 62 | if (this.state !== GestureRecognizer.States.Recognized && this.direction === SwipeGestureRecognizer.Directions.Up) { 63 | if (translation.y < -SwipeGestureRecognizer.MinimumDistance && Math.abs(translation.y) > Math.abs(translation.x)) 64 | this.enterRecognizedState(); 65 | } 66 | 67 | if (this.state !== GestureRecognizer.States.Recognized && this.direction === SwipeGestureRecognizer.Directions.Down) { 68 | if (translation.y > SwipeGestureRecognizer.MinimumDistance && Math.abs(translation.y) > Math.abs(translation.x)) 69 | this.enterRecognizedState(); 70 | } 71 | }, 72 | 73 | touchesEnded: function(event) 74 | { 75 | this.enterFailedState(); 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /pinch.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = PinchGestureRecognizer; 3 | 4 | var GestureRecognizer = require("./gesture-recognizer"); 5 | 6 | function PinchGestureRecognizer() 7 | { 8 | GestureRecognizer.call(this); 9 | } 10 | 11 | PinchGestureRecognizer.MaximumTimeForRecordingGestures = 100; 12 | PinchGestureRecognizer.MaximumDecelerationTime = 500; 13 | 14 | PinchGestureRecognizer.prototype = { 15 | constructor: PinchGestureRecognizer, 16 | __proto__: GestureRecognizer.prototype, 17 | 18 | get velocity() 19 | { 20 | var lastGesture = this._gestures[this._gestures.length - 1]; 21 | if (!lastGesture) 22 | return this._velocity; 23 | 24 | var elapsedTime = Date.now() - (lastGesture.timeStamp + PinchGestureRecognizer.MaximumTimeForRecordingGestures); 25 | if (elapsedTime <= 0) 26 | return this._velocity; 27 | 28 | var f = Math.max((PinchGestureRecognizer.MaximumDecelerationTime - elapsedTime) / PinchGestureRecognizer.MaximumDecelerationTime, 0); 29 | return this._velocity * f; 30 | }, 31 | 32 | touchesBegan: function(event) 33 | { 34 | if (event.currentTarget !== this.target || this.numberOfTouches !== 2) 35 | return; 36 | 37 | event.preventDefault(); 38 | GestureRecognizer.prototype.touchesBegan.call(this, event); 39 | }, 40 | 41 | gestureBegan: function(event) 42 | { 43 | GestureRecognizer.prototype.gestureBegan.call(this, event); 44 | 45 | this._recordGesture(event); 46 | this.scale = event.scale; 47 | this.enterBeganState(); 48 | }, 49 | 50 | gestureChanged: function(event) 51 | { 52 | event.preventDefault(); 53 | 54 | this.enterChangedState(); 55 | 56 | this._recordGesture(event); 57 | 58 | var oldestGesture = this._gestures[0]; 59 | var ds = event.scale / oldestGesture.scale; 60 | var dt = event.timeStamp - oldestGesture.timeStamp; 61 | this._velocity = ds / dt * 1000; 62 | 63 | this.scale *= event.scale / this._gestures[this._gestures.length - 2].scale; 64 | }, 65 | 66 | gestureEnded: function(event) 67 | { 68 | this.enterEndedState(); 69 | }, 70 | 71 | reset: function() 72 | { 73 | this.scale = 1; 74 | this._velocity = 0; 75 | this._gestures = []; 76 | }, 77 | 78 | // Private 79 | 80 | _recordGesture: function(event) 81 | { 82 | var currentTime = event.timeStamp; 83 | var count = this._gestures.push({ 84 | scale: event.scale, 85 | timeStamp: currentTime 86 | }); 87 | 88 | // We want to keep at least two gestures at all times. 89 | if (count <= 2) 90 | return; 91 | 92 | var scaleDirection = this._gestures[count - 1].scale >= this._gestures[count - 2].scale; 93 | for (var i = count - 3; i >= 0; --i) { 94 | var gesture = this._gestures[i]; 95 | if (currentTime - gesture.timeStamp > PinchGestureRecognizer.MaximumTimeForRecordingGestures || 96 | this._gestures[i + 1].scale >= gesture.scale !== scaleDirection) 97 | break; 98 | } 99 | 100 | if (i > 0) 101 | this._gestures = this._gestures.slice(i + 1); 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /rotation.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = RotationGestureRecognizer; 3 | 4 | var GestureRecognizer = require("./gesture-recognizer"); 5 | 6 | function RotationGestureRecognizer() 7 | { 8 | GestureRecognizer.call(this); 9 | } 10 | 11 | RotationGestureRecognizer.MaximumTimeForRecordingGestures = 100; 12 | RotationGestureRecognizer.MaximumDecelerationTime = 500; 13 | 14 | RotationGestureRecognizer.prototype = { 15 | constructor: RotationGestureRecognizer, 16 | __proto__: GestureRecognizer.prototype, 17 | 18 | get velocity() 19 | { 20 | var lastGesture = this._gestures[this._gestures.length - 1]; 21 | if (!lastGesture) 22 | return this._velocity; 23 | 24 | var elapsedTime = Date.now() - (lastGesture.timeStamp + RotationGestureRecognizer.MaximumTimeForRecordingGestures); 25 | if (elapsedTime <= 0) 26 | return this._velocity; 27 | 28 | var f = Math.max((RotationGestureRecognizer.MaximumDecelerationTime - elapsedTime) / RotationGestureRecognizer.MaximumDecelerationTime, 0); 29 | return this._velocity * f; 30 | }, 31 | 32 | touchesBegan: function(event) 33 | { 34 | if (event.currentTarget !== this.target || this.numberOfTouches !== 2) 35 | return; 36 | 37 | event.preventDefault(); 38 | GestureRecognizer.prototype.touchesBegan.call(this, event); 39 | }, 40 | 41 | gestureBegan: function(event) 42 | { 43 | GestureRecognizer.prototype.gestureBegan.call(this, event); 44 | 45 | this._recordGesture(event); 46 | this.rotation = event.rotation; 47 | this.enterBeganState(); 48 | }, 49 | 50 | gestureChanged: function(event) 51 | { 52 | event.preventDefault(); 53 | 54 | this.enterChangedState(); 55 | 56 | this._recordGesture(event); 57 | 58 | var oldestGesture = this._gestures[0]; 59 | var dr = event.rotation - oldestGesture.rotation; 60 | var dt = event.timeStamp - oldestGesture.timeStamp; 61 | this._velocity = dr / dt * 1000; 62 | 63 | this.rotation += event.rotation - this._gestures[this._gestures.length - 2].rotation; 64 | }, 65 | 66 | gestureEnded: function(event) 67 | { 68 | this.enterEndedState(); 69 | }, 70 | 71 | reset: function() 72 | { 73 | this.rotation = 0; 74 | this._velocity = 0; 75 | this._gestures = []; 76 | }, 77 | 78 | // Private 79 | 80 | _recordGesture: function(event) 81 | { 82 | var currentTime = event.timeStamp; 83 | var count = this._gestures.push({ 84 | rotation: event.rotation, 85 | timeStamp: currentTime 86 | }); 87 | 88 | // We want to keep at least two gestures at all times. 89 | if (count <= 2) 90 | return; 91 | 92 | var rotationDirection = this._gestures[count - 1].rotation >= this._gestures[count - 2].rotation; 93 | for (var i = count - 3; i >= 0; --i) { 94 | var gesture = this._gestures[i]; 95 | if (currentTime - gesture.timeStamp > RotationGestureRecognizer.MaximumTimeForRecordingGestures || 96 | this._gestures[i + 1].rotation >= gesture.rotation !== rotationDirection) 97 | break; 98 | } 99 | 100 | if (i > 0) 101 | this._gestures = this._gestures.slice(i + 1); 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /tests/combined.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Combining multiple gesture recognizers 5 | 6 | 7 | 74 | 75 | 76 |
77 | 78 | 79 | -------------------------------------------------------------------------------- /pan.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = PanGestureRecognizer; 3 | 4 | var GestureRecognizer = require("./gesture-recognizer"), 5 | Point = require("geometry/point.js"); 6 | 7 | function PanGestureRecognizer() 8 | { 9 | this.minimumNumberOfTouches = 1; 10 | this.maximumNumberOfTouches = 100000; 11 | 12 | this._travelledMinimumDistance = false; 13 | 14 | GestureRecognizer.call(this); 15 | } 16 | 17 | PanGestureRecognizer.MinimumDistance = 10; 18 | PanGestureRecognizer.MaximumTimeForRecordingGestures = 100; 19 | PanGestureRecognizer.MaximumDecelerationTime = 500; 20 | 21 | PanGestureRecognizer.prototype = { 22 | constructor: PanGestureRecognizer, 23 | __proto__: GestureRecognizer.prototype, 24 | 25 | get velocity() 26 | { 27 | var lastGesture = this._gestures[this._gestures.length - 1]; 28 | if (!lastGesture) 29 | return this._velocity; 30 | 31 | var elapsedTime = Date.now() - (lastGesture.timeStamp + PanGestureRecognizer.MaximumTimeForRecordingGestures); 32 | if (elapsedTime <= 0) 33 | return this._velocity; 34 | 35 | var f = Math.max((PanGestureRecognizer.MaximumDecelerationTime - elapsedTime) / PanGestureRecognizer.MaximumDecelerationTime, 0); 36 | return new Point(this._velocity.x * f, this._velocity.y * f); 37 | }, 38 | 39 | touchesBegan: function(event) 40 | { 41 | if (event.currentTarget !== this.target) 42 | return; 43 | 44 | GestureRecognizer.prototype.touchesBegan.call(this, event); 45 | 46 | if (!this._numberOfTouchesIsAllowed()) 47 | this.enterFailedState(); 48 | }, 49 | 50 | touchesMoved: function(event) 51 | { 52 | event.preventDefault(); 53 | 54 | var location = this.locationInElement(); 55 | 56 | var currentTime = event.timeStamp; 57 | this._recordGesture(location, currentTime); 58 | 59 | if (this._gestures.length === 1) { 60 | this._translationOrigin = location; 61 | this._travelledMinimumDistance = false; 62 | return; 63 | } 64 | 65 | var sliceIndexX = 0; 66 | var sliceIndexY = 0; 67 | 68 | var currentGesture = this._gestures[this._gestures.length - 1]; 69 | var previousGesture = this._gestures[this._gestures.length - 2]; 70 | 71 | var txDirection = currentGesture.location.x >= previousGesture.location.x; 72 | var tyDirection = currentGesture.location.y >= previousGesture.location.y; 73 | for (var i = this._gestures.length - 3; i >= 0; --i) { 74 | var gesture = this._gestures[i]; 75 | if (currentTime - gesture.timeStamp > PanGestureRecognizer.MaximumTimeForRecordingGestures) { 76 | if (sliceIndexX === 0) 77 | sliceIndexX = i + 1; 78 | if (sliceIndexY === 0) 79 | sliceIndexY = i + 1; 80 | break; 81 | } 82 | 83 | var nextTranslation = this._gestures[i + 1].location; 84 | 85 | if (nextTranslation.x >= gesture.location.x !== txDirection) 86 | sliceIndexX = i + 1; 87 | if (nextTranslation.y >= gesture.location.y !== tyDirection) 88 | sliceIndexY = i + 1; 89 | 90 | if (sliceIndexX > 0 && sliceIndexY > 0) 91 | break; 92 | } 93 | 94 | var xGestures = this._gestures, 95 | yGestures = this._gestures; 96 | 97 | if (sliceIndexX === sliceIndexY && sliceIndexX > 0) 98 | this._gestures = this._gestures.slice(sliceIndexX); 99 | else { 100 | if (sliceIndexX > 0) 101 | xGestures = this._gestures.slice(sliceIndexX); 102 | if (sliceIndexY > 0) 103 | yGestures = this._gestures.slice(sliceIndexY); 104 | } 105 | 106 | var oldestGestureX = xGestures[0]; 107 | var xdt = currentTime - oldestGestureX.timeStamp; 108 | var dx = location.x - oldestGestureX.location.x; 109 | this._velocity.x = dx / xdt * 1000; 110 | 111 | var oldestGestureY = yGestures[0]; 112 | var ydt = currentTime - oldestGestureY.timeStamp; 113 | var dy = location.y - oldestGestureY.location.y; 114 | this._velocity.y = dy / ydt * 1000; 115 | 116 | if (!this._travelledMinimumDistance) { 117 | if (this.canBeginWithTravelledDistance(new Point(location.x - this._translationOrigin.x, location.y - this._translationOrigin.y))) { 118 | this._travelledMinimumDistance = true; 119 | this.enterBeganState(); 120 | } 121 | } else { 122 | this.translation.x += location.x - previousGesture.location.x; 123 | this.translation.y += location.y - previousGesture.location.y; 124 | 125 | this.enterChangedState(); 126 | } 127 | }, 128 | 129 | canBeginWithTravelledDistance: function(distance) 130 | { 131 | return Math.abs(distance.x) >= PanGestureRecognizer.MinimumDistance || Math.abs(distance.y) >= PanGestureRecognizer.MinimumDistance; 132 | }, 133 | 134 | touchesEnded: function(event) 135 | { 136 | if (this._numberOfTouchesIsAllowed()) 137 | return; 138 | 139 | if (this._travelledMinimumDistance) 140 | this.enterEndedState(); 141 | else 142 | this.enterFailedState(); 143 | }, 144 | 145 | reset: function() 146 | { 147 | this._gestures = []; 148 | this.translation = new Point; 149 | this._velocity = new Point; 150 | }, 151 | 152 | // Private 153 | 154 | _recordGesture: function(location, timeStamp) 155 | { 156 | this._gestures.push({ 157 | location: location, 158 | timeStamp: timeStamp 159 | }); 160 | }, 161 | 162 | _numberOfTouchesIsAllowed: function() 163 | { 164 | return this.numberOfTouches >= this.minimumNumberOfTouches && this.numberOfTouches <= this.maximumNumberOfTouches; 165 | } 166 | }; 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gesture-recognizer 2 | 3 | A JavaScript implementation of the [UIKit gesture recognizers](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/GestureRecognizer_basics/GestureRecognizer_basics.html#//apple_ref/doc/uid/TP40009541-CH2-SW2) aiming to bring the same system and features available to iOS developers to Web developers targeting devices with multi-touch input. 4 | 5 | ## Installation 6 | 7 | Install with [component(1)](http://component.io): 8 | 9 | $ component install graouts/gesture-recognizer 10 | 11 | ## Usage 12 | 13 | The library introduces a `GestureRecognizer` abstract class and six concrete subclasses: `LongPressGestureRecognizer`, `PanGestureRecognizer`, `PinchGestureRecognizer`, `RotationGestureRecognizer`, `SwipeGestureRecognizer`, `TapGestureRecognizer`, each tracking a specific kind of gesture. A gesture recognizer evolves through various states as it tracks touches and as the recognizer's `state` changes, `statechange` events are dispatched. 14 | 15 | Once you've created a gesture recognizer, you typically listen to `statechange` events like so: 16 | 17 | ```javascript 18 | recognizer.addEventListener("statechange", function(event) { 19 | // do something based on `event.target.state` 20 | }); 21 | ``` 22 | 23 | And then you typically attach it to a target DOM element: 24 | 25 | ```javascript 26 | recognizer.target = someElement; 27 | ``` 28 | 29 | A utility class `GestureView` is also made available to wrap an element and eases setting its transform as illustrated across the examples below. 30 | 31 | ### TapGestureRecognizer 32 | 33 | This gesture recognizer allows you to identify a quick tap, or multiple quick taps when you set `numberOfTapsRequired` to a value higher than the default value `1`. You can customize the number of fingers used for the taps with `numberOfTouchesRequired`. 34 | 35 | ```javascript 36 | var GestureRecognizer = require("gesture-recognizer"), 37 | TapGestureRecognizer = require("gesture-recognizer/tap"); 38 | 39 | var recognizer = new TapGestureRecognizer; 40 | recognizer.numberOfTapsRequired = 3; 41 | recognizer.addEventListener("statechange", function(event) { 42 | if (recognizer.state === GestureRecognizer.States.Recognized) 43 | console.log("tapped!"); 44 | }); 45 | 46 | recognizer.target = someElement; 47 | ``` 48 | 49 | ### LongPressGestureRecognizer 50 | 51 | This gesture recognizer allows you to identify a long stationary press. You can customize the expected duration of the press with `minimumPressDuration` as well as the number of fingers used for the press with `numberOfTouchesRequired`. 52 | 53 | ```javascript 54 | var GestureRecognizer = require("gesture-recognizer"), 55 | LongPressGestureRecognizer = require("gesture-recognizer/long-press"); 56 | 57 | var recognizer = new LongPressGestureRecognizer; 58 | recognizer.maximumNumberOfTouches = 1; 59 | recognizer.addEventListener("statechange", function(event) { 60 | if (recognizer.state === GestureRecognizer.States.Recognized) 61 | console.log("Long press!"); 62 | }); 63 | 64 | recognizer.target = someElement; 65 | ``` 66 | 67 | ### SwipeGestureRecognizer 68 | 69 | This gesture recognizer allows you to identify a linear swipe gesture. You can customize the allowed `direction` for the swipe as well as the number of fingers used for the swipe with `numberOfTouchesRequired`. 70 | 71 | ```javascript 72 | var GestureRecognizer = require("gesture-recognizer"), 73 | SwipeGestureRecognizer = require("gesture-recognizer/swipe"); 74 | 75 | var recognizer = new SwipeGestureRecognizer; 76 | recognizer.direction = SwipeGestureRecognizer.Directions.Right; 77 | recognizer.addEventListener("statechange", function(event) { 78 | if (recognizer.state === GestureRecognizer.States.Recognized) 79 | console.log("Swiped"); 80 | }); 81 | 82 | recognizer.target = someElement; 83 | ``` 84 | 85 | ### PanGestureRecognizer 86 | 87 | This gesture recognizer allows you to track where the user's finger travels on the screen. You can set both the `minimumNumberOfTouches` and `maximumNumberOfTouches` for the interaction. Each time the `statechange` fires, you can check the `velocity` for the user interaction. This example shows a way to make an element follow the user's finger at all times. 88 | 89 | ```javascript 90 | var GestureRecognizer = require("gesture-recognizer"), 91 | PanGestureRecognizer = require("gesture-recognizer/pan"), 92 | GestureView = require("gesture-recognizer/view"), 93 | Point = require("geometry").Point; 94 | 95 | var translation = new Point; 96 | 97 | var recognizer = new PanGestureRecognizer; 98 | recognizer.maximumNumberOfTouches = 1; 99 | recognizer.addEventListener("statechange", function(event) { 100 | if (recognizer.state === GestureRecognizer.States.Ended || recognizer.state === GestureRecognizer.States.Changed) { 101 | translation.x += recognizer.translation.x; 102 | translation.y += recognizer.translation.y; 103 | recognizer.view.transform = translation; 104 | recognizer.translation = new Point; 105 | } 106 | }); 107 | 108 | window.addEventListener("DOMContentLoaded", function() { 109 | new GestureView("target").addGestureRecognizer(recognizer); 110 | }); 111 | ``` 112 | 113 | ### PinchGestureRecognizer 114 | 115 | This gesture recognizer allows you to identify a two-finger pinch gesture, usually used to scale an element up and down and possibly bring the element full-screen during the gesture. Each time the `statechange` fires, you can check the `velocity` for the user interaction. 116 | 117 | ```javascript 118 | var GestureRecognizer = require("gesture-recognizer"), 119 | PinchGestureRecognizer = require("gesture-recognizer/pinch"), 120 | GestureView = require("gesture-recognizer/view"); 121 | 122 | var scale = 1; 123 | 124 | var recognizer = new PinchGestureRecognizer; 125 | recognizer.addEventListener("statechange", function(event) { 126 | if (recognizer.state === GestureRecognizer.States.Ended || recognizer.state === GestureRecognizer.States.Changed) { 127 | scale *= recognizer.scale; 128 | recognizer.view.transform = { scale: scale }; 129 | recognizer.scale = 1; 130 | } 131 | }); 132 | 133 | window.addEventListener("DOMContentLoaded", function() { 134 | new GestureView("target").addGestureRecognizer(recognizer); 135 | }); 136 | ``` 137 | 138 | ### RotationGestureRecognizer 139 | 140 | This gesture recognizer allows you to identify a two-finger rotation gesture, usually used to rotate an element around its center. Each time the `statechange` fires, you can check the `velocity` for the user interaction. 141 | 142 | ```javascript 143 | var GestureRecognizer = require("gesture-recognizer"), 144 | RotationGestureRecognizer = require("gesture-recognizer/rotation"), 145 | GestureView = require("gesture-recognizer/view"); 146 | 147 | var rotation = 0; 148 | 149 | var recognizer = new RotationGestureRecognizer; 150 | recognizer.addEventListener("statechange", function(event) { 151 | if (recognizer.state === GestureRecognizer.States.Ended || recognizer.state === GestureRecognizer.States.Changed) { 152 | rotation += recognizer.rotation; 153 | recognizer.view.transform = { rotation: rotation }; 154 | recognizer.rotation = 0; 155 | } 156 | }); 157 | 158 | window.addEventListener("DOMContentLoaded", function() { 159 | new GestureView("target").addGestureRecognizer(recognizer); 160 | }); 161 | ``` 162 | 163 | ## Authors 164 | 165 | This project is a fork of [JSGestureRecognizer](https://github.com/mud/JSGestureRecognizer), originally created by [Takashi Okamoto](http://mud.mitplw.com/), maintained by [Antoine Quint](https://github.com/graouts/). 166 | -------------------------------------------------------------------------------- /gesture-recognizer.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = GestureRecognizer; 3 | 4 | var DOM = require("dom-events"), 5 | Point = require("geometry/point.js"); 6 | 7 | function GestureRecognizer() 8 | { 9 | DOM.EventTarget.call(this); 10 | 11 | this._targetTouches = []; 12 | 13 | this._enabled = true; 14 | this._target = null; 15 | this.view = null; 16 | this.state = GestureRecognizer.States.Possible; 17 | this.delegate = null; 18 | } 19 | 20 | GestureRecognizer.SupportsTouches = "createTouch" in document; 21 | 22 | GestureRecognizer.States = { 23 | Possible : "possible", 24 | Began : "began", 25 | Changed : "changed", 26 | Ended : "ended", 27 | Cancelled : "cancelled", 28 | Failed : "failed", 29 | Recognized : "ended" 30 | }; 31 | 32 | GestureRecognizer.Events = { 33 | TouchStart : GestureRecognizer.SupportsTouches ? "touchstart" : "mousedown", 34 | TouchMove : GestureRecognizer.SupportsTouches ? "touchmove" : "mousemove", 35 | TouchEnd : GestureRecognizer.SupportsTouches ? "touchend" : "mouseup", 36 | TouchCancel : "touchcancel", 37 | GestureStart : "gesturestart", 38 | GestureChange : "gesturechange", 39 | GestureEnd : "gestureend", 40 | StateChange : "statechange" 41 | }; 42 | 43 | GestureRecognizer.prototype = { 44 | constructor: GestureRecognizer, 45 | __proto__: DOM.EventTarget.prototype, 46 | 47 | get target() 48 | { 49 | return this._target; 50 | }, 51 | 52 | set target(target) 53 | { 54 | if (!target || this._target === target) 55 | return; 56 | 57 | this._target = target; 58 | this._initRecognizer(); 59 | }, 60 | 61 | get numberOfTouches() 62 | { 63 | return this._targetTouches.length; 64 | }, 65 | 66 | get enabled() 67 | { 68 | return this._enabled; 69 | }, 70 | 71 | set enabled(enabled) 72 | { 73 | if (this._enabled === enabled) 74 | return; 75 | 76 | this._enabled = enabled; 77 | 78 | if (!enabled) { 79 | if (this.numberOfTouches === 0) { 80 | this._removeTrackingListeners(); 81 | this.reset(); 82 | } else 83 | this.enterCancelledState(); 84 | } 85 | 86 | this._updateBaseListeners(); 87 | }, 88 | 89 | reset: function() 90 | { 91 | // Implemented by subclasses. 92 | }, 93 | 94 | locationInElement: function(element) 95 | { 96 | var p = new Point; 97 | var touches = this._targetTouches; 98 | for (var i = 0, count = touches.length; i < count; ++i) { 99 | var touch = touches[i]; 100 | p.x += touch.pageX; 101 | p.y += touch.pageY; 102 | } 103 | p.x /= count; 104 | p.y /= count; 105 | 106 | if (!element) 107 | return p; 108 | 109 | var wkPoint = window.webkitConvertPointFromPageToNode(element, new WebKitPoint(p.x, p.y)); 110 | return new Point(wkPoint.x, wkPoint.y); 111 | }, 112 | 113 | locationOfTouchInElement: function(touchIndex, element) 114 | { 115 | var touch = this._targetTouches[touchIndex]; 116 | if (!touch) 117 | return new Point; 118 | 119 | if (!element) 120 | return new Point(touch.pageX, touch.pageY); 121 | 122 | var wkPoint = window.webkitConvertPointFromPageToNode(element, new WebKitPoint(touch.pageX, touch.pageY)); 123 | return new Point(wkPoint.x, wkPoint.y); 124 | }, 125 | 126 | // Touch and gesture event handling 127 | 128 | handleEvent: function(event) 129 | { 130 | this._updateTargetTouches(event); 131 | 132 | switch (event.type) { 133 | case GestureRecognizer.Events.TouchStart: 134 | this.touchesBegan(event); 135 | break; 136 | case GestureRecognizer.Events.TouchMove: 137 | this.touchesMoved(event); 138 | break; 139 | case GestureRecognizer.Events.TouchEnd: 140 | this.touchesEnded(event); 141 | break; 142 | case GestureRecognizer.Events.TouchCancel: 143 | this.touchesCancelled(event); 144 | break; 145 | case GestureRecognizer.Events.GestureStart: 146 | this.gestureBegan(event); 147 | break; 148 | case GestureRecognizer.Events.GestureChange: 149 | this.gestureChanged(event); 150 | break; 151 | case GestureRecognizer.Events.GestureEnd: 152 | this.gestureEnded(event); 153 | break; 154 | } 155 | }, 156 | 157 | touchesBegan: function(event) 158 | { 159 | if (event.currentTarget !== this._target) 160 | return; 161 | 162 | window.addEventListener(GestureRecognizer.Events.TouchMove, this, true); 163 | window.addEventListener(GestureRecognizer.Events.TouchEnd, this, true); 164 | window.addEventListener(GestureRecognizer.Events.TouchCancel, this, true); 165 | this.enterPossibleState(); 166 | }, 167 | 168 | touchesMoved: function(event) 169 | { 170 | // Implemented by subclasses. 171 | }, 172 | 173 | touchesEnded: function(event) 174 | { 175 | // Implemented by subclasses. 176 | }, 177 | 178 | touchesCancelled: function(event) 179 | { 180 | // Implemented by subclasses. 181 | }, 182 | 183 | gestureBegan: function(event) 184 | { 185 | if (event.currentTarget !== this._target) 186 | return; 187 | 188 | window.addEventListener(GestureRecognizer.Events.GestureChange, this, true); 189 | window.addEventListener(GestureRecognizer.Events.GestureEnd, this, true); 190 | this.enterPossibleState(); 191 | }, 192 | 193 | gestureChanged: function(event) 194 | { 195 | // Implemented by subclasses. 196 | }, 197 | 198 | gestureEnded: function(event) 199 | { 200 | // Implemented by subclasses. 201 | }, 202 | 203 | // State changes 204 | 205 | enterPossibleState: function() 206 | { 207 | this._setStateAndNotifyOfChange(GestureRecognizer.States.Possible); 208 | }, 209 | 210 | enterBeganState: function() 211 | { 212 | if (this.delegate && typeof this.delegate.gestureRecognizerShouldBegin === "function" && !this.delegate.gestureRecognizerShouldBegin(this)) { 213 | this.enterFailedState(); 214 | return; 215 | } 216 | this._setStateAndNotifyOfChange(GestureRecognizer.States.Began); 217 | }, 218 | 219 | enterEndedState: function() 220 | { 221 | this._setStateAndNotifyOfChange(GestureRecognizer.States.Ended); 222 | this._removeTrackingListeners(); 223 | this.reset(); 224 | }, 225 | 226 | enterCancelledState: function() 227 | { 228 | this._setStateAndNotifyOfChange(GestureRecognizer.States.Cancelled); 229 | this._removeTrackingListeners(); 230 | this.reset(); 231 | }, 232 | 233 | enterFailedState: function() 234 | { 235 | this._setStateAndNotifyOfChange(GestureRecognizer.States.Failed); 236 | this._removeTrackingListeners(); 237 | this.reset(); 238 | }, 239 | 240 | enterChangedState: function() 241 | { 242 | this._setStateAndNotifyOfChange(GestureRecognizer.States.Changed); 243 | }, 244 | 245 | enterRecognizedState: function() 246 | { 247 | this._setStateAndNotifyOfChange(GestureRecognizer.States.Recognized); 248 | }, 249 | 250 | // Private 251 | 252 | _initRecognizer: function() 253 | { 254 | this.reset(); 255 | this.state = GestureRecognizer.States.Possible; 256 | 257 | this._updateBaseListeners(); 258 | }, 259 | 260 | _updateBaseListeners: function() 261 | { 262 | if (!this._target) 263 | return; 264 | 265 | if (this._enabled) { 266 | this._target.addEventListener(GestureRecognizer.Events.TouchStart, this); 267 | if (GestureRecognizer.SupportsTouches) 268 | this._target.addEventListener(GestureRecognizer.Events.GestureStart, this); 269 | } else { 270 | this._target.removeEventListener(GestureRecognizer.Events.TouchStart, this); 271 | if (GestureRecognizer.SupportsTouches) 272 | this._target.removeEventListener(GestureRecognizer.Events.GestureStart, this); 273 | } 274 | }, 275 | 276 | _removeTrackingListeners: function() 277 | { 278 | window.removeEventListener(GestureRecognizer.Events.TouchMove, this, true); 279 | window.removeEventListener(GestureRecognizer.Events.TouchEnd, this, true); 280 | window.removeEventListener(GestureRecognizer.Events.GestureChange, this, true); 281 | window.removeEventListener(GestureRecognizer.Events.GestureEnd, this, true); 282 | }, 283 | 284 | _setStateAndNotifyOfChange: function(state) 285 | { 286 | this.state = state; 287 | this.dispatchEvent(new DOM.Event(GestureRecognizer.Events.StateChange)); 288 | }, 289 | 290 | _updateTargetTouches: function(event) 291 | { 292 | if (!GestureRecognizer.SupportsTouches) { 293 | if (event.type === GestureRecognizer.Events.TouchEnd) 294 | this._targetTouches = []; 295 | else 296 | this._targetTouches = [event] 297 | return; 298 | } 299 | 300 | if (!(event instanceof TouchEvent)) 301 | return; 302 | 303 | // With a touchstart event, event.targetTouches is accurate so 304 | // we simply add all of those. 305 | if (event.type === GestureRecognizer.Events.TouchStart) { 306 | this._targetTouches = []; 307 | var touches = event.targetTouches; 308 | for (var i = 0, count = touches.length; i < count; ++i) 309 | this._targetTouches.push(touches[i]); 310 | return; 311 | } 312 | 313 | // With a touchmove event, the target is window so event.targetTouches is 314 | // inaccurate so we add all touches that we knew about previously. 315 | if (event.type === GestureRecognizer.Events.TouchMove) { 316 | var targetIdentifiers = this._targetTouches.map(function(touch) { 317 | return touch.identifier; 318 | }); 319 | 320 | this._targetTouches = []; 321 | var touches = event.touches; 322 | for (var i = 0, count = touches.length; i < count; ++i) { 323 | var touch = touches[i]; 324 | if (targetIdentifiers.indexOf(touch.identifier) !== -1) 325 | this._targetTouches.push(touch); 326 | } 327 | return; 328 | } 329 | 330 | // With a touchend or touchcancel event, we only keep the existing touches 331 | // that are also found in event.touches. 332 | var allTouches = event.touches; 333 | var existingIdentifiers = []; 334 | for (var i = 0, count = allTouches.length; i < count; ++i) 335 | existingIdentifiers.push(allTouches[i].identifier); 336 | 337 | this._targetTouches = this._targetTouches.filter(function(touch) { 338 | return existingIdentifiers.indexOf(touch.identifier) !== -1; 339 | }); 340 | 341 | } 342 | }; 343 | --------------------------------------------------------------------------------