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