├── 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 |
--------------------------------------------------------------------------------