├── .babelrc
├── .eslintrc
├── .gitignore
├── CHANGELOG.md
├── README.md
├── package.json
├── sample
├── bundle.js
├── index.html
├── index.js
└── ninja@3x.png
├── src
├── gestures
│ ├── pan.js
│ ├── pinch.js
│ └── tap.js
└── index.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"]
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "standard",
3 | "parser": "babel-eslint",
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .DS_Store
3 | npm-debug.log
4 | node_modules
5 | lib
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ### 0.3.0
2 |
3 | - Add Inertia Mode
4 |
5 | ### 0.2.0
6 |
7 | - Add 'simpletap'.
8 | - Modify 'panstart' emit timing. Now 'panstart' is emitted after the mouse / touch actually moved.
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pixi-simple-gesture
2 | Add Pinch, Pan, Tap gesture support to pixi.js sprites.
3 |
4 | ```
5 | npm install --save pixi-simple-gesture
6 | ```
7 |
8 | ## Usage
9 |
10 | ### Pinch
11 |
12 | ```js
13 | var gesture = require('pixi-simple-gesture')
14 |
15 | var sprite = new PIXI.Sprite(texture)
16 | var inertiaMode = true
17 |
18 | gesture.pinchable(sprite, inertiaMode)
19 |
20 | sprite.on('pinchstart', function() {
21 | console.log('pinch start')
22 | })
23 |
24 | sprite.on('pinchmove', function(event) {
25 | console.log('pinch move', event)
26 | })
27 |
28 | sprite.on('pinchend', function() {
29 | console.log('pinch end')
30 | })
31 | ```
32 |
33 | The 'pinchmove' handler receives an event object containing the following properties.
34 |
35 | | Name | Value |
36 | |:--------|:--------------------------------------|
37 | | scale | Scaling that has been done |
38 | | velocity| Velocity in px/ms |
39 | | center | Center position for multi-touch |
40 | | data | Original InteractionData from pixi.js |
41 |
42 | ### Pan
43 |
44 | ```js
45 | var gesture = require('pixi-simple-gesture')
46 | var inertiaMode = true
47 |
48 | var sprite = new PIXI.Sprite(texture)
49 | gesture.panable(sprite, inertiaMode)
50 |
51 | sprite.on('panstart', function() {
52 | console.log('pan start')
53 | })
54 |
55 | sprite.on('panmove', function(event) {
56 | console.log('pan move', event)
57 | })
58 |
59 | sprite.on('panend', function() {
60 | console.log('pan end')
61 | })
62 | ```
63 |
64 | The 'panmove' handler receives an event object containing the following properties.
65 |
66 | | Name | Value |
67 | |:--------|:--------------------------------------|
68 | | deltaX | Movement of the X axis |
69 | | deltaY | Movement of the Y axis |
70 | | velocity| Velocity in px/ms |
71 | | data | Original InteractionData from pixi.js |
72 |
73 | ### Tap
74 | ```js
75 | var gesture = require('pixi-simple-gesture')
76 |
77 | var sprite = new PIXI.Sprite(texture)
78 | gesture.tappable(sprite)
79 |
80 | sprite.on('simpletap', function() {
81 | console.log('simply tapped')
82 | })
83 | ```
84 |
85 | NOT 'tap', **simpletap**. Because 'tap' is already used by pixi.js. This 'simpletap' works a bit better with 'pinch' and 'pan'. The Handler receives an event object containing the following properties.
86 |
87 | | Name | Value |
88 | |:--------|:--------------------------------------|
89 | | data | Original InteractionData from pixi.js |
90 |
91 |
92 | ## Note
93 |
94 | Any requests, issues, PRs are welcome!
95 |
96 |
97 | ### TODO
98 |
99 | - Add Inertia Mode
100 | - Add Complex? Tap, emits 'tapstart', 'tapcancel', 'tapend'. Could be useful to implement UI components which has active state style.
101 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pixi-simple-gesture",
3 | "version": "0.3.0",
4 | "description": "Add Pinch, Pan, Tap gesture support to pixi.js sprites",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib",
8 | "src"
9 | ],
10 | "scripts": {
11 | "build": "babel src --out-dir lib --source-maps inline",
12 | "watch": "babel src --out-dir lib --watch --source-maps inline",
13 | "dev": "webpack-dev-server --devtool cheap-source-map --debug --output-pathinfo --inline --hot --port 8888",
14 | "test": "echo \"Error: no test specified\" && exit 1"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/dekimasoon/pixi-simple-gesture.git"
19 | },
20 | "keywords": [
21 | "pixi.js",
22 | "pixijs",
23 | "pixi"
24 | ],
25 | "author": {
26 | "name": "dekimasoon"
27 | },
28 | "license": "MIT",
29 | "bugs": {
30 | "url": "https://github.com/dekimasoon/pixi-simple-gesture/issues"
31 | },
32 | "homepage": "https://github.com/dekimasoon/pixi-simple-gesture#readme",
33 | "devDependencies": {
34 | "babel-cli": "^6.3.17",
35 | "babel-core": "^6.3.15",
36 | "babel-eslint": "^4.1.6",
37 | "babel-loader": "^6.2.0",
38 | "babel-preset-es2015": "^6.3.13",
39 | "eslint": "^1.10.3",
40 | "eslint-config-standard": "^4.4.0",
41 | "eslint-plugin-standard": "^1.3.1",
42 | "file-loader": "^0.8.5",
43 | "html-webpack-plugin": "^1.7.0",
44 | "json-loader": "^0.5.4",
45 | "webpack": "^1.12.9",
46 | "webpack-dev-server": "^1.14.0",
47 | "pixi.js": "^3.0.9"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/sample/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | gesture sample
7 |
8 |
9 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/sample/index.js:
--------------------------------------------------------------------------------
1 | import gesture from '../src'
2 | import PIXI from 'pixi.js'
3 |
4 | let rendererOpts = {
5 | backgroundColor: 0x6482C0
6 | }
7 |
8 | let width = window.innerWidth
9 | let height = window.innerHeight
10 | let renderer = PIXI.autoDetectRenderer(width, height, rendererOpts)
11 | document.querySelector('body').appendChild(renderer.view)
12 |
13 | let sprite = PIXI.Sprite.fromImage('./ninja@3x.png')
14 | gesture.panable(sprite, true)
15 | gesture.pinchable(sprite, true)
16 | gesture.tappable(sprite, true)
17 |
18 | sprite.on('panmove', e => {
19 | sprite.x += e.deltaX
20 | sprite.y += e.deltaY
21 | })
22 |
23 | sprite.on('panstart', () => {
24 | console.log('panstart')
25 | })
26 |
27 | sprite.on('panend', () => {
28 | console.log('panend')
29 | })
30 |
31 | sprite.on('pinchmove', e => {
32 | sprite.scale.x = Math.max(0.5, sprite.scale.x * e.scale)
33 | sprite.scale.y = Math.max(0.5, sprite.scale.y * e.scale)
34 | })
35 |
36 | sprite.on('pinchstart', () => {
37 | console.log('pinchstart')
38 | })
39 |
40 | sprite.on('pinchend', () => {
41 | console.log('pinchend')
42 | })
43 |
44 | sprite.on('simpletap', () => {
45 | console.log('simpletap')
46 | })
47 |
48 | let interval = 1000 / 60 // 60fps
49 | let stage = new PIXI.Container()
50 | stage.addChild(sprite)
51 | function startAnimate () {
52 | let then = Date.now()
53 | function animate () {
54 | requestAnimationFrame(animate)
55 | let now = Date.now()
56 | let elapsed = now - then
57 | if (elapsed > interval) {
58 | then = now
59 | renderer.render(stage)
60 | }
61 | }
62 | animate()
63 | }
64 | startAnimate()
65 |
--------------------------------------------------------------------------------
/sample/ninja@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dekimasoon/pixi-simple-gesture/899910b1415a595c6cb9c408299a557c337a4d22/sample/ninja@3x.png
--------------------------------------------------------------------------------
/src/gestures/pan.js:
--------------------------------------------------------------------------------
1 | export default function panable (sprite, inertia) {
2 |
3 | function mouseDown (e) {
4 | start(e.data.originalEvent)
5 | }
6 |
7 | function touchStart (e) {
8 | start(e.data.originalEvent.targetTouches[0])
9 | }
10 |
11 | function start (t) {
12 | if (sprite._pan) {
13 | if (!sprite._pan.intervalId) {
14 | return
15 | }
16 | clearInterval(sprite._pan.intervalId)
17 | sprite.emit('panend')
18 | }
19 | sprite._pan = {
20 | p: {
21 | x: t.clientX,
22 | y: t.clientY,
23 | date: new Date()
24 | }
25 | }
26 | sprite
27 | .on('mousemove', mouseMove)
28 | .on('touchmove', touchMove)
29 | }
30 |
31 | function mouseMove (e) {
32 | move(e, e.data.originalEvent)
33 | }
34 |
35 | function touchMove (e) {
36 | let t = e.data.originalEvent.targetTouches
37 | if (!t || t.length > 1) {
38 | end(e, t[0])
39 | return
40 | }
41 | move(e, t[0])
42 | }
43 |
44 | function move (e, t) {
45 | let now = new Date()
46 | let interval = now - sprite._pan.p.date
47 | if (interval < 12) {
48 | return
49 | }
50 | let dx = t.clientX - sprite._pan.p.x
51 | let dy = t.clientY - sprite._pan.p.y
52 | let distance = Math.sqrt(dx * dx + dy * dy)
53 | if (!sprite._pan.pp) {
54 | let threshold = (t instanceof window.MouseEvent) ? 2 : 7
55 | if (distance > threshold) {
56 | sprite.emit('panstart')
57 | } else {
58 | return
59 | }
60 | } else {
61 | let event = {
62 | deltaX: dx,
63 | deltaY: dy,
64 | velocity: distance / interval,
65 | data: e.data
66 | }
67 | sprite.emit('panmove', event)
68 | }
69 | sprite._pan.pp = {
70 | x: sprite._pan.p.x,
71 | y: sprite._pan.p.y,
72 | date: sprite._pan.p.date
73 | }
74 | sprite._pan.p = {
75 | x: t.clientX,
76 | y: t.clientY,
77 | date: now
78 | }
79 | }
80 |
81 | function mouseUp (e) {
82 | end(e, e.data.originalEvent)
83 | }
84 |
85 | function touchEnd (e) {
86 | end(e, e.data.originalEvent.changedTouches[0])
87 | }
88 |
89 | function end (e, t) {
90 | sprite
91 | .removeListener('mousemove', mouseMove)
92 | .removeListener('touchmove', touchMove)
93 | if (!sprite._pan || !sprite._pan.pp) {
94 | sprite._pan = null
95 | return
96 | }
97 | if (inertia) {
98 | if (sprite._pan.intervalId) {
99 | return
100 | }
101 | let interval = new Date() - sprite._pan.pp.date
102 | let vx = (t.clientX - sprite._pan.pp.x) / interval
103 | let vy = (t.clientY - sprite._pan.pp.y) / interval
104 | sprite._pan.intervalId = setInterval(() => {
105 | if (Math.abs(vx) < 0.04 && Math.abs(vy) < 0.04) {
106 | clearInterval(sprite._pan.intervalId)
107 | sprite.emit('panend')
108 | sprite._pan = null
109 | return
110 | }
111 | let touch = {
112 | clientX: sprite._pan.p.x + vx * 12,
113 | clientY: sprite._pan.p.y + vy * 12
114 | }
115 | move(e, touch)
116 | vx *= 0.9
117 | vy *= 0.9
118 | }, 12)
119 | } else {
120 | sprite.emit('panend')
121 | sprite._pan = null
122 | }
123 | }
124 |
125 | sprite.interactive = true
126 | sprite
127 | .on('mousedown', mouseDown)
128 | .on('touchstart', touchStart)
129 | .on('mouseup', mouseUp)
130 | .on('mouseupoutside', mouseUp)
131 | .on('touchend', touchEnd)
132 | .on('touchendoutside', touchEnd)
133 | }
134 |
--------------------------------------------------------------------------------
/src/gestures/pinch.js:
--------------------------------------------------------------------------------
1 | export default function pinchable (sprite, inertia) {
2 |
3 | function start (e) {
4 | sprite.on('touchmove', move)
5 | }
6 |
7 | function move (e) {
8 | let t = e.data.originalEvent.targetTouches
9 | if (!t || t.length < 2) {
10 | return
11 | }
12 | let dx = t[0].clientX - t[1].clientX
13 | let dy = t[0].clientY - t[1].clientY
14 | let distance = Math.sqrt(dx * dx + dy * dy)
15 | if (!sprite._pinch) {
16 | sprite._pinch = {
17 | p: {
18 | distance: distance,
19 | date: new Date()
20 | }
21 | }
22 | sprite.emit('pinchstart')
23 | return
24 | }
25 | let now = new Date()
26 | let interval = now - sprite._pinch.p.date
27 | if (interval < 12) {
28 | return
29 | }
30 | let center = {
31 | x: (t[0].clientX + t[1].clientX) / 2,
32 | y: (t[0].clientY + t[1].clientY) / 2
33 | }
34 | let event = {
35 | scale: distance / sprite._pinch.p.distance,
36 | velocity: distance / interval,
37 | center: center,
38 | data: e.data
39 | }
40 | sprite.emit('pinchmove', event)
41 | sprite._pinch.pp = {
42 | distance: sprite._pinch.p.distance,
43 | date: sprite._pinch.p.date
44 | }
45 | sprite._pinch.p = {
46 | distance: distance,
47 | date: now
48 | }
49 | }
50 |
51 | function end (e) {
52 | sprite.removeListener('touchmove', move)
53 | if (!sprite._pinch) {
54 | return
55 | }
56 | if (inertia && sprite._pinch.pp) {
57 | if (sprite._pinch.intervalId) {
58 | return
59 | }
60 | let interval = new Date() - sprite._pinch.p.date
61 | let velocity = (sprite._pinch.p.distance - sprite._pinch.pp.distance) / interval
62 | let center = sprite._pinch.p.center
63 | let distance = sprite._pinch.p.distance
64 | sprite._pinch.intervalId = setInterval(() => {
65 | if (Math.abs(velocity) < 0.04) {
66 | clearInterval(sprite._pinch.intervalId)
67 | sprite.emit('pinchend')
68 | sprite._pinch = null
69 | return
70 | }
71 | let updatedDistance = distance + velocity * 12
72 | let event = {
73 | scale: updatedDistance / distance,
74 | velocity: velocity,
75 | center: center,
76 | data: e.data
77 | }
78 | sprite.emit('pinchmove', event)
79 | distance = updatedDistance
80 | velocity *= 0.8
81 | }, 12)
82 | } else {
83 | sprite.emit('pinchend')
84 | sprite._pinch = null
85 | }
86 | }
87 |
88 | sprite.interactive = true
89 | sprite
90 | .on('touchstart', start)
91 | .on('touchend', end)
92 | .on('touchendoutside', end)
93 | }
94 |
--------------------------------------------------------------------------------
/src/gestures/tap.js:
--------------------------------------------------------------------------------
1 | export default function tappable (sprite) {
2 | function mouseDown (e) {
3 | start(e, e.data.originalEvent)
4 | }
5 |
6 | function touchStart (e) {
7 | start(e, e.data.originalEvent.targetTouches[0])
8 | }
9 |
10 | // possibly be called twice or more
11 | function start (e, t) {
12 | if (sprite._tap) {
13 | return
14 | }
15 | sprite._tap = {
16 | p: {
17 | x: t.clientX,
18 | y: t.clientY
19 | }
20 | }
21 | sprite
22 | .on('mousemove', mouseMove)
23 | .on('touchmove', touchMove)
24 | }
25 |
26 | function mouseMove (e) {
27 | move(e, e.data.originalEvent)
28 | }
29 |
30 | function touchMove (e) {
31 | let t = e.data.originalEvent.targetTouches
32 | if (!t || t.length > 1) {
33 | sprite._tap.canceled = true
34 | end(e)
35 | return
36 | }
37 | move(e, t[0])
38 | }
39 |
40 | function move (e, t) {
41 | let dx = t.clientX - sprite._tap.p.x
42 | let dy = t.clientY - sprite._tap.p.y
43 | let distance = Math.sqrt(dx * dx + dy * dy)
44 | let threshold = (t instanceof window.MouseEvent) ? 2 : 7
45 | if (distance > threshold) {
46 | sprite._tap.canceled = true
47 | }
48 | }
49 |
50 | // possibly be called twice or more
51 | function end (e) {
52 | if (sprite._tap && !sprite._tap.canceled) {
53 | let event = {
54 | data: e.data
55 | }
56 | sprite.emit('simpletap', event)
57 | }
58 | sprite._tap = null
59 | sprite
60 | .removeListener('mousemove', mouseMove)
61 | .removeListener('touchmove', touchMove)
62 | }
63 |
64 | sprite.interactive = true
65 | sprite
66 | .on('mousedown', mouseDown)
67 | .on('touchstart', touchStart)
68 | .on('mouseup', end)
69 | .on('mouseupoutside', end)
70 | .on('touchend', end)
71 | .on('touchendoutside', end)
72 | }
73 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import pinchable from './gestures/pinch'
2 | import panable from './gestures/pan'
3 | import tappable from './gestures/tap'
4 |
5 | export default {
6 | pinchable,
7 | panable,
8 | tappable
9 | }
10 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 |
3 | module.exports = {
4 | entry: './sample/index',
5 | output: {
6 | path: __dirname + './sample',
7 | filename: 'bundle.js'
8 | },
9 | module: {
10 | preLoaders: [
11 | ],
12 | loaders: [
13 | { test: /\.json$/, include: path.join(__dirname, 'node_modules', 'pixi.js'), loader: 'json' },
14 | { test: /\.js$/, include: /sample|src/, loader: 'babel?presets[]=es2015' },
15 | { test: /\.png$|\.jpg$/, include: /sample/, loader: 'file?name=[path][name].[ext]' }
16 | ]
17 | },
18 | node: {
19 | fs: 'empty'
20 | },
21 | devServer: {
22 | contentBase: __dirname + '/sample'
23 | }
24 | }
25 |
--------------------------------------------------------------------------------