├── .babelrc ├── .gitignore ├── .npmignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── example_app.js ├── phaser-list-view.js └── phaser.min.js ├── index.html ├── package.json ├── src ├── basic_swiper.js ├── config.js ├── directional_scroller.js ├── example │ ├── game.js │ ├── index.js │ ├── states │ │ ├── boot.js │ │ ├── game_state.js │ │ ├── list_view_state.js │ │ └── swipe_carousel_state.js │ └── utils │ │ ├── array_utils.js │ │ ├── bitmap_data_utils.js │ │ ├── layout.js │ │ └── math_utils.js ├── index.js ├── list_view.js ├── list_view_core.js ├── scroller.js ├── scroller_event_dispatcher.js ├── swipe_carousel.js ├── util.js ├── utils │ └── math_utils.js └── wheel_scroller.js ├── webpack.config.js └── webpack.demos.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .tern-project 3 | lib/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .tern-project 2 | src/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "none", 6 | "noSemi": false 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## Version 1.5.2 - 15th Mar 2018 4 | 5 | * Bug fix 6 | * Update docs 7 | 8 | ## Version 1.5.1 - 15th Mar 2018 9 | 10 | * Bring back `setPosition` as we didn't bump a major version. So deprecated it instead. 11 | 12 | ## Version 1.5.0 - 15th Mar 2018 13 | 14 | * Add `moveToPosition` and `moveToItem` to ListViewCore 15 | * Remove `setPosition` from ListViewCore 16 | 17 | ## Version 1.4.1 - 14th Mar 2018 18 | 19 | * Add `tweenToPosition` and `tweenToItem` to ListViewCore 20 | * Improved API docs 21 | 22 | ## Version 1.4.0 - 15th Dec 2017 23 | 24 | * Removed lodash and gsap dependencies reducing the build from 1.3mb to 363kb (not minified) Thanks @lukz 25 | 26 | ## Version 1.3.1 - 29th July 2017 27 | 28 | ### Bug Fixes 29 | 30 | * Fixed npm imports 31 | * allow onInputDown events to fire for ListView children 32 | 33 | ## Version 1.3.0 - 12th Jan 2017 34 | 35 | ### Bug Fixes 36 | 37 | * Fixed SwipeCarousel, it was completely broken. 38 | ### New Features 39 | * Allow for install via script tag 40 | * Add a demo 41 | 42 | ## Version 1.2.0 - 26th July 2016 43 | 44 | ### Bug Fixes 45 | 46 | ### New Features 47 | 48 | * Changed to a more es6y style of coding, mainly using classes. 49 | * ListView now includes a 'searchForClicks' flag, which will search for onInputDown and onInputUp events on ListView items and their children when you click. 50 | 51 | ## Version 1.1.0 - 25th July 2016 52 | 53 | ### Bug Fixes 54 | 55 | ### New Features 56 | 57 | * Add a registerClickables method on Scroller. This allows for click events on scroller objects to be respected. 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Matt Colman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # List View classes for Phaser 2 | 3 | ## Install via npm 4 | 5 | `npm install phaser-list-view --save` 6 | https://www.npmjs.com/package/phaser-list-view 7 | 8 | ## Install via script tag 9 | 10 | Copy dist/phaser-list-view.js into your project and include via script tag 11 | 12 | ## Build 13 | 14 | `npm run build` 15 | 16 | ## Run demo 17 | 18 | `npm i` 19 | 20 | `npm start` 21 | 22 | ## API 23 | 24 | * **Scroller** : A pure logic scroller. Includes iOS-like behaviour such as momentum, bounce-back and snapping. Most likely you would use DirectionalScroller or WheelScroller over this base Scroller. But if you have custom needs you can use it. 25 | * **DirectionalScroller** : A pure logic scroller built for scrolling on the x and y axis. Extends the base Scroller class. 26 | * **WheelScroller** : A pure logic scroller built for scrolling around a circle. Extends the base Scroller class. 27 | * **ListView** : An iOS-like ListView class. Uses DirectionalScroller for the input and outputs a ListView. Performance is good because we cull off-screen items. 28 | Perfect for high scoreboards. 29 | * **SwipeCarousel** : An iOS-like SwipeCarousel. Uses DirectionalScroller for the input and outputs a SwipeCarousel. Perfect for instructions screens, or a photo gallery. 30 | 31 | ## ListView 32 | 33 | ### Usage 34 | 35 | ``` 36 | import {ListView} from 'phaser-list-view' 37 | 38 | const parent = this.world 39 | const bounds = new Phaser.Rectangle(0, 0, 300, 400) 40 | const options = { 41 | direction: 'y', 42 | overflow: 100, 43 | padding: 10 44 | } 45 | 46 | const listView = new ListView(this.game, parent, bounds, options) 47 | const items = this.createSomeDisplayObjectsAndReturnAnArray() // [Graphics, Image, Sprite, Group] 48 | listView.addMultiple(...items) 49 | const newItem = this.createGroup(); 50 | newItem.nominalHeight = 120; // listView calculates items width and height. You can set your own width or height to save calculating it using nominalWidth or nominalHeight (note this is mainly useful for Phaser.Groups) 51 | listView.add(newItem) 52 | ``` 53 | 54 | ![](http://i.imgur.com/XgdgqYX.gif) 55 | 56 | ### Options 57 | 58 | * `direction` direction of scroll ['x' | 'y'] // default 'y' 59 | * `autocull` auto hidden elements outside of the viewport for performance [boolean] // default true 60 | * `momentum` [boolean] // default true 61 | * `bouncing` when you extend beyond the bounds and release, it bounces back [boolean] // default true 62 | * `snapping` snaps to snapStep [boolean] // default false 63 | * `snapStep` [number] // default undefined 64 | * `overflow`: Amount in pixels you can pull past the bounds. Bouncing occurs when you release inside the overflow [number] // default 100 65 | * `padding`: Padding between the children [number] // default 10 66 | * `searchForClicks`: onInputDown and onInputUp events on ListView children will become active when set to true [boolean] // default false 67 | 68 | ### API 69 | 70 | | Members | Type | Description | 71 | | :--------- | :------------------- | :-------------------------------------------------------------------- | 72 | | `items` | Array | A list of all the listView items | 73 | | `grp` | Phaser.Group | The parent of all list view items | 74 | | `position` | number (READONLY) | position in pixels (x or y axis depends on the direction you specify) | 75 | | `scroller` | Scroller | access the Scroller for advanced tuning (Scroller API below) | 76 | 77 | | Methods | Params | Description | 78 | | :---------------- | :--------------------------------------------- | :------------------------------------------------------------------------------------------------ | 79 | | `add` | (child: DisplayObject): void | add a child to the list view | 80 | | `addMultiple` | (...children: DisplayObjects): void | add multiple children to the list view. Pass through multiple arguments, not an array of children | 81 | | `remove` | (child: DisplayObject): void | remove a child | 82 | | `removeAll` | (): void | remove all children from the list view | 83 | | `moveToPosition` | (position: number): void | set position of the list view in pixels | 84 | | `moveToItem` | (index: number): void | move to the item index in the list view. | 85 | | `tweenToPosition` | (position: number, duration = 1: number): void | tween to position in pixels. Duration in seconds. | 86 | | `tweenToItem` | (index: number, duration = 1: number): void | tween to the item index in the list view. Duration in seconds. | 87 | | `reset` | (): void | resets the position and scroller | 88 | | `destroy` | (): void | destroy the list view and clean up all event listeners | 89 | 90 | ## SwipeCarousel (extends ListView) 91 | 92 | ### Usage 93 | 94 | ``` 95 | import {SwipeCarousel} from 'phaser-list-view' 96 | 97 | const parent = this.world 98 | const bounds = new Phaser.Rectangle(0, 0, 300, 400) 99 | const options = { 100 | direction: 'x', 101 | overflow: 100, 102 | padding: 10 103 | } 104 | 105 | const swipeCarousel = new SwipeCarousel(this.game, parent, bounds, options) 106 | const photos = this.getAnArrayOfImages() // [Image, Image, Image, Image] 107 | swipeCarousel.addMultiple(...photos) 108 | ``` 109 | 110 | ![](http://i.imgur.com/Sp5aE0H.gif) 111 | 112 | ### Options 113 | 114 | * `direction` direction of scroll ['x' | 'y'] // default 'x' 115 | * `autocull` auto hidden elements outside of the viewport for performance [boolean] // default true 116 | * `momentum` [boolean] // default false 117 | * `bouncing` when you extend beyond the bounds and release, it bounces back [boolean] // default true 118 | * `snapping` snaps to bounds.width + padding [boolean] // default true 119 | * `overflow`: Amount in pixels you can pull past the bounds. Bouncing occurs when you release inside the overflow [number] // default 100 120 | * `padding`: Padding between the children [number] // default 10 121 | * `searchForClicks`: onInputDown and onInputUp events on ListView children will become active when set to true [boolean] // default false 122 | 123 | ### API 124 | 125 | The same as ListView above. 126 | 127 | ## Scroller API (access via listView.scroller) 128 | 129 | | Members | Type | Description | 130 | | :--------- | :---------------- | :-------------------------------------------------------------------- | 131 | | `grp` | Phaser.Group | The parent of all list view items | 132 | | `position` | number (READONLY) | position in pixels (x or y axis depends on the direction you specify) | 133 | | `scroller` | Scroller | access the Scroller for advanced tuning | 134 | 135 | | Methods | Params: Return | Description | 136 | | :----------- | :------------- | :------------------------ | 137 | | `enable` | (): void | Enables the scroller | 138 | | `disable` | (): void | Disables the scroller | 139 | | `isTweening` | (): boolean | Is the scroller tweening? | 140 | 141 | | Events (Phaser.Signals) | 142 | | :---------------------- | 143 | | `onUpdate` | 144 | | `onInputUp` | 145 | | `onInputDown` | 146 | | `onInputMove` | 147 | | `onComplete` | 148 | | `onSwipe` | 149 | 150 | ## DirectionalScroller Usage 151 | 152 | // TODO 153 | 154 | ## WheelScroller Usage 155 | 156 | // TODO 157 | 158 | ## Build 159 | 160 | `npm run compile` 161 | 162 | ## Example 163 | 164 | http://mattcolman.com/labs/phaser-list-view/index.html 165 | 166 | ## TODO 167 | 168 | * Mouse wheel support 169 | 170 | ## Maintainers 171 | 172 | [Matt Colman](https://twitter.com/matt_colman) 173 | -------------------------------------------------------------------------------- /dist/example_app.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) 10 | /******/ return installedModules[moduleId].exports; 11 | 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ exports: {}, 15 | /******/ id: moduleId, 16 | /******/ loaded: false 17 | /******/ }; 18 | 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | 22 | /******/ // Flag the module as loaded 23 | /******/ module.loaded = true; 24 | 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | 29 | 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | 36 | /******/ // __webpack_public_path__ 37 | /******/ __webpack_require__.p = "./dist/"; 38 | 39 | /******/ // Load entry module and return exports 40 | /******/ return __webpack_require__(0); 41 | /******/ }) 42 | /************************************************************************/ 43 | /******/ ([ 44 | /* 0 */ 45 | /***/ function(module, exports, __webpack_require__) { 46 | 47 | module.exports = __webpack_require__(1); 48 | 49 | 50 | /***/ }, 51 | /* 1 */ 52 | /***/ function(module, exports, __webpack_require__) { 53 | 54 | 'use strict'; 55 | 56 | var _game = __webpack_require__(2); 57 | 58 | var _game2 = _interopRequireDefault(_game); 59 | 60 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 61 | 62 | var init = function init() { 63 | var config = { 64 | width: 1366, 65 | height: 768, 66 | renderer: Phaser.AUTO, 67 | parent: 'content', 68 | resolution: 1, //window.devicePixelRatio, 69 | state: _game2.default.prototype.states[0][1] 70 | }; 71 | var game = new _game2.default(config); 72 | }; 73 | 74 | init(); 75 | 76 | /***/ }, 77 | /* 2 */ 78 | /***/ function(module, exports, __webpack_require__) { 79 | 80 | 'use strict'; 81 | 82 | Object.defineProperty(exports, "__esModule", { 83 | value: true 84 | }); 85 | 86 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 87 | 88 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 89 | 90 | var _game_state = __webpack_require__(3); 91 | 92 | var _game_state2 = _interopRequireDefault(_game_state); 93 | 94 | var _boot = __webpack_require__(4); 95 | 96 | var _boot2 = _interopRequireDefault(_boot); 97 | 98 | var _list_view_state = __webpack_require__(5); 99 | 100 | var _list_view_state2 = _interopRequireDefault(_list_view_state); 101 | 102 | var _swipe_carousel_state = __webpack_require__(14); 103 | 104 | var _swipe_carousel_state2 = _interopRequireDefault(_swipe_carousel_state); 105 | 106 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 107 | 108 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 109 | 110 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 111 | 112 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 113 | 114 | var Game = function (_Phaser$Game) { 115 | _inherits(Game, _Phaser$Game); 116 | 117 | function Game() { 118 | _classCallCheck(this, Game); 119 | 120 | return _possibleConstructorReturn(this, Object.getPrototypeOf(Game).apply(this, arguments)); 121 | } 122 | 123 | _createClass(Game, [{ 124 | key: 'setupStage', 125 | value: function setupStage() { 126 | this.input.maxPointers = 1; 127 | this.scale.scaleMode = Phaser.ScaleManager.USER_SCALE; 128 | this.scale.setMinMax(this.width / 2, this.height / 2, this.width, this.height); 129 | // this.scale.forceOrientation(true) // landscape 130 | this.scale.pageAlignHorizontally = true; 131 | 132 | // if (this.device.desktop) { 133 | // this.scale.setResizeCallback(this.fitToWindow, this) 134 | // } else { 135 | // // Mobile 136 | // this.scale.setResizeCallback(this.fitToWindowMobile, this) 137 | // } 138 | } 139 | }, { 140 | key: 'fitToWindowMobile', 141 | value: function fitToWindowMobile() { 142 | var gameHeight = this.height; 143 | var windowAspectRatio = window.innerWidth / window.innerHeight; 144 | var gameWidth = Math.ceil(this.height * windowAspectRatio); 145 | this.scale.setGameSize(gameWidth, gameHeight); 146 | this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL; 147 | } 148 | }, { 149 | key: 'fitToWindow', 150 | value: function fitToWindow() { 151 | var w = window.innerWidth / this.width; 152 | var h = window.innerHeight / this.height; 153 | var scale = Math.min(w, h); 154 | this.scale.setUserScale(scale, scale); 155 | } 156 | }, { 157 | key: 'addStates', 158 | value: function addStates() { 159 | var _this2 = this; 160 | 161 | this.states.forEach(function (_ref) { 162 | var _ref2 = _slicedToArray(_ref, 2); 163 | 164 | var name = _ref2[0]; 165 | var stateClass = _ref2[1]; 166 | 167 | _this2.state.add(name, stateClass); 168 | }); 169 | } 170 | }, { 171 | key: 'startGame', 172 | value: function startGame() { 173 | console.log('start game'); 174 | this.stateIndex = 0; 175 | this.nextState(); 176 | } 177 | }, { 178 | key: 'nextState', 179 | value: function nextState() { 180 | this.gotoStateByIndex(this.stateIndex + 1); 181 | } 182 | }, { 183 | key: 'prevState', 184 | value: function prevState() { 185 | this.gotoStateByIndex(this.stateIndex - 1); 186 | } 187 | }, { 188 | key: 'gotoStateByIndex', 189 | value: function gotoStateByIndex(index) { 190 | index = Math.min(index, this.states.length - 1); 191 | index = Math.max(index, 1); 192 | this.stateIndex = index; 193 | this.state.start(this.states[index][0]); 194 | } 195 | }, { 196 | key: 'addDropDownMenu', 197 | value: function addDropDownMenu() { 198 | var _this3 = this; 199 | 200 | this.experiments.forEach(function (a) { 201 | var _a = _slicedToArray(a, 2); 202 | 203 | var name = _a[0]; 204 | var klass = _a[1]; 205 | 206 | 207 | var option = document.createElement('option'); 208 | option.text = name; 209 | document.getElementById('selector').add(option); 210 | }); 211 | 212 | document.getElementById('selector').addEventListener('change', function (e) { 213 | _this3.state.start(e.target.value); 214 | }); 215 | } 216 | }]); 217 | 218 | return Game; 219 | }(Phaser.Game); 220 | 221 | Game.prototype.experiments = [['ListView Example', _list_view_state2.default], ['SwipeCarousel Example', _swipe_carousel_state2.default]]; 222 | 223 | Game.prototype.states = [['boot', _boot2.default]].concat(Game.prototype.experiments); 224 | 225 | exports.default = Game; 226 | 227 | /***/ }, 228 | /* 3 */ 229 | /***/ function(module, exports) { 230 | 231 | 'use strict'; 232 | 233 | Object.defineProperty(exports, "__esModule", { 234 | value: true 235 | }); 236 | 237 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 238 | 239 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 240 | 241 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 242 | 243 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 244 | 245 | var GameState = function (_Phaser$State) { 246 | _inherits(GameState, _Phaser$State); 247 | 248 | function GameState() { 249 | _classCallCheck(this, GameState); 250 | 251 | return _possibleConstructorReturn(this, Object.getPrototypeOf(GameState).apply(this, arguments)); 252 | } 253 | 254 | _createClass(GameState, [{ 255 | key: 'create', 256 | value: function create() { 257 | this.game.time.advancedTiming = true; 258 | this.stage.backgroundColor = '#4488AA'; 259 | this.fpsTxt = this.game.add.text(50, 20, this.game.time.fps || '--', { 260 | font: '24px Arial', 261 | fill: '#00ff00' 262 | }); 263 | var txt = this.add.text(this.world.centerX, 50, this.name || this.game.state.current, { font: '30px Arial', fill: '#fff' }); 264 | txt.anchor.set(0.5); 265 | } 266 | }, { 267 | key: 'update', 268 | value: function update() { 269 | this.fpsTxt.text = this.game.time.fps; // debug text doesn't work with the canvas renderer?? 270 | this.fpsTxt.bringToTop(); 271 | } 272 | }]); 273 | 274 | return GameState; 275 | }(Phaser.State); 276 | 277 | exports.default = GameState; 278 | 279 | /***/ }, 280 | /* 4 */ 281 | /***/ function(module, exports) { 282 | 283 | 'use strict'; 284 | 285 | Object.defineProperty(exports, "__esModule", { 286 | value: true 287 | }); 288 | 289 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 290 | 291 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 292 | 293 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 294 | 295 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 296 | 297 | var Boot = function (_Phaser$State) { 298 | _inherits(Boot, _Phaser$State); 299 | 300 | function Boot() { 301 | _classCallCheck(this, Boot); 302 | 303 | return _possibleConstructorReturn(this, Object.getPrototypeOf(Boot).apply(this, arguments)); 304 | } 305 | 306 | _createClass(Boot, [{ 307 | key: 'preload', 308 | value: function preload() { 309 | this.game.load.crossOrigin = 'anonymous'; 310 | } 311 | }, { 312 | key: 'create', 313 | value: function create() { 314 | console.log('boot me up'); 315 | this.game.addDropDownMenu(); 316 | this.game.setupStage(); 317 | this.game.addStates(); 318 | this.game.startGame(); 319 | } 320 | }]); 321 | 322 | return Boot; 323 | }(Phaser.State); 324 | 325 | exports.default = Boot; 326 | 327 | /***/ }, 328 | /* 5 */ 329 | /***/ function(module, exports, __webpack_require__) { 330 | 331 | 'use strict'; 332 | 333 | Object.defineProperty(exports, "__esModule", { 334 | value: true 335 | }); 336 | 337 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 338 | 339 | var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; 340 | 341 | var _game_state = __webpack_require__(3); 342 | 343 | var _game_state2 = _interopRequireDefault(_game_state); 344 | 345 | var _math_utils = __webpack_require__(6); 346 | 347 | var _list_view = __webpack_require__(7); 348 | 349 | var _list_view2 = _interopRequireDefault(_list_view); 350 | 351 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 352 | 353 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 354 | 355 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 356 | 357 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 358 | 359 | var ListViewState = function (_GameState) { 360 | _inherits(ListViewState, _GameState); 361 | 362 | function ListViewState() { 363 | _classCallCheck(this, ListViewState); 364 | 365 | return _possibleConstructorReturn(this, Object.getPrototypeOf(ListViewState).apply(this, arguments)); 366 | } 367 | 368 | _createClass(ListViewState, [{ 369 | key: 'preload', 370 | value: function preload() { 371 | this.game.load.crossOrigin = 'anonymous'; 372 | } 373 | }, { 374 | key: 'shutdown', 375 | value: function shutdown() { 376 | this.listView.destroy(); 377 | } 378 | }, { 379 | key: 'create', 380 | value: function create() { 381 | var maskW = 600; 382 | var maskH = 200; 383 | var boxW = maskW; 384 | var boxH = 50; 385 | 386 | this.listView = new _list_view2.default(this.game, this.world, new Phaser.Rectangle(this.world.centerX - maskW / 2, 120, maskW, 400), { 387 | direction: 'y' 388 | }); 389 | 390 | for (var i = 0; i < 500; i++) { 391 | var color = Phaser.Color.getRandomColor(); 392 | var group = this.game.make.group(null); 393 | var g = this.game.add.graphics(0, 0, group); 394 | var h = boxH + Math.floor(Math.random() * 100); 395 | g.beginFill(color).drawRect(0, 0, boxW, h); 396 | 397 | var txt = this.game.add.text(boxW / 2, h / 2, i, { font: '40px Arial', fill: '#000' }, group); 398 | txt.anchor.set(0.5); 399 | var img = this.game.add.image(0, 0, group.generateTexture()); 400 | this.listView.add(img); 401 | } 402 | 403 | this.listView.moveToItem(3); 404 | _get(Object.getPrototypeOf(ListViewState.prototype), 'create', this).call(this); 405 | } 406 | }]); 407 | 408 | return ListViewState; 409 | }(_game_state2.default); 410 | 411 | exports.default = ListViewState; 412 | 413 | /***/ }, 414 | /* 6 */ 415 | /***/ function(module, exports) { 416 | 417 | "use strict"; 418 | 419 | Object.defineProperty(exports, "__esModule", { 420 | value: true 421 | }); 422 | var MathUtils = { 423 | nearestMultiple: function nearestMultiple(n, multiple) { 424 | return Math.round(n / multiple) * multiple; 425 | }, 426 | 427 | scaleBetween: function scaleBetween(lo, hi, scale) { 428 | return lo + (hi - lo) * scale; 429 | }, 430 | 431 | // returns a percentage between hi and lo from a given input 432 | // e.g percentageBetween2(7, 4, 10) -> .5 433 | percentageBetween2: function percentageBetween2(input, lo, hi) { 434 | return (input - lo) / (hi - lo); 435 | } 436 | }; 437 | 438 | window.MathUtils = MathUtils; 439 | 440 | exports.default = MathUtils; 441 | 442 | /***/ }, 443 | /* 7 */ 444 | /***/ function(module, exports, __webpack_require__) { 445 | 446 | 'use strict'; 447 | 448 | Object.defineProperty(exports, "__esModule", { 449 | value: true 450 | }); 451 | 452 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 453 | 454 | var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; 455 | 456 | var _list_view_core = __webpack_require__(8); 457 | 458 | var _list_view_core2 = _interopRequireDefault(_list_view_core); 459 | 460 | var _directional_scroller = __webpack_require__(11); 461 | 462 | var _directional_scroller2 = _interopRequireDefault(_directional_scroller); 463 | 464 | var _util = __webpack_require__(9); 465 | 466 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 467 | 468 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 469 | 470 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 471 | 472 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 473 | 474 | var defaultOptions = { 475 | direction: 'y', 476 | autocull: true, 477 | momentum: true, 478 | bouncing: true, 479 | snapping: false, 480 | overflow: 100, 481 | padding: 10, 482 | searchForClicks: false // if you just click on the list view it will search the list view items for onInputDown and onInputUp events. 483 | }; 484 | 485 | var ListView = function (_ListViewCore) { 486 | _inherits(ListView, _ListViewCore); 487 | 488 | function ListView(game, parent, bounds) { 489 | var options = arguments.length <= 3 || arguments[3] === undefined ? {} : arguments[3]; 490 | 491 | _classCallCheck(this, ListView); 492 | 493 | // we have to use a new mask instance for the click object or webgl ignores the mask 494 | var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(ListView).call(this, game, parent, (0, _util.parseBounds)(bounds), Object.assign({}, defaultOptions, options))); 495 | 496 | _this.scroller = new _directional_scroller2.default(_this.game, _this._addMask(bounds), Object.assign({ 497 | from: 0, 498 | to: 0 499 | }, _this.options)); 500 | _this.scroller.events.onUpdate.add(function (o) { 501 | _this._setPosition(o.total); 502 | }); 503 | _this.events.onAdded.add(function (limit) { 504 | var _to = Math.min(-limit, 0); 505 | _this.scroller.setFromTo(0, _to); 506 | if (_this.options.searchForClicks) { 507 | _this.scroller.registerClickables(_this.items); 508 | } 509 | }); 510 | return _this; 511 | } 512 | 513 | _createClass(ListView, [{ 514 | key: 'destroy', 515 | value: function destroy() { 516 | this.scroller.destroy(); 517 | this.scroller = null; 518 | _get(Object.getPrototypeOf(ListView.prototype), 'destroy', this).call(this); 519 | } 520 | }, { 521 | key: 'reset', 522 | value: function reset() { 523 | this._setPosition(0); 524 | this.scroller.reset(); 525 | } 526 | }]); 527 | 528 | return ListView; 529 | }(_list_view_core2.default); 530 | 531 | exports.default = ListView; 532 | 533 | /***/ }, 534 | /* 8 */ 535 | /***/ function(module, exports, __webpack_require__) { 536 | 537 | 'use strict'; 538 | 539 | Object.defineProperty(exports, "__esModule", { 540 | value: true 541 | }); 542 | 543 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 544 | 545 | var _util = __webpack_require__(9); 546 | 547 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 548 | 549 | var defaultOptions = { 550 | direction: 'y', 551 | autocull: true, 552 | padding: 10 553 | }; 554 | 555 | var ListViewCore = function () { 556 | function ListViewCore(game, parent, bounds) { 557 | var options = arguments.length <= 3 || arguments[3] === undefined ? {} : arguments[3]; 558 | 559 | _classCallCheck(this, ListViewCore); 560 | 561 | this.game = game; 562 | this.parent = parent; 563 | this.bounds = bounds; 564 | 565 | this.o = this.options = Object.assign({}, defaultOptions, options); 566 | 567 | this.items = []; 568 | 569 | if (this.o.direction == 'y') { 570 | this.p = { xy: 'y', wh: 'height' }; 571 | } else { 572 | this.p = { xy: 'x', wh: 'width' }; 573 | } 574 | 575 | this.grp = this.game.add.group(parent); 576 | this.grp.position.set(bounds.x, bounds.y); 577 | 578 | this.events = { 579 | onAdded: new Phaser.Signal() 580 | }; 581 | 582 | this.position = 0; 583 | 584 | // [MC] - is masking the fastest option here? Cropping the texture may be faster? 585 | this.grp.mask = this._addMask(bounds); 586 | } 587 | 588 | /** 589 | * [add a child to the list 590 | * stacks them on top of each other by measuring their 591 | * height and adding custom padding. Optionally you can 592 | * specify nominalHeight or nominalWidth on the display object, 593 | * this will take preference over height and width] 594 | * @param {DisplayObject} child 595 | */ 596 | 597 | 598 | _createClass(ListViewCore, [{ 599 | key: 'add', 600 | value: function add(child) { 601 | this.items.push(child); 602 | var xy = 0; 603 | if (this.grp.children.length > 0) { 604 | var lastChild = this.grp.getChildAt(this.grp.children.length - 1); 605 | xy = lastChild[this.p.xy] + (0, _util.getWidthOrHeight)(lastChild, this.p.wh) + this.o.padding; 606 | } 607 | child[this.p.xy] = xy; 608 | this.grp.addChild(child); 609 | this.length = xy + child[this.p.wh]; 610 | 611 | // this._setPosition(this.position) 612 | this.events.onAdded.dispatch(this.length - this.bounds[this.p.wh]); 613 | return child; 614 | } 615 | 616 | /** 617 | * [addMultiple children to the list] 618 | * @param {...[DisplayObjects]} children 619 | */ 620 | 621 | }, { 622 | key: 'addMultiple', 623 | value: function addMultiple() { 624 | for (var _len = arguments.length, children = Array(_len), _key = 0; _key < _len; _key++) { 625 | children[_key] = arguments[_key]; 626 | } 627 | 628 | children.forEach(this.add, this); 629 | } 630 | }, { 631 | key: 'remove', 632 | value: function remove(child) { 633 | this.grp.removeChild(child); 634 | var index = this.items.indexOf(child); 635 | if (index == -1) return; 636 | this.items.splice(index, 1); 637 | return child; 638 | } 639 | }, { 640 | key: 'destroy', 641 | value: function destroy() { 642 | this.events.onAdded.dispose(); 643 | this.events = null; 644 | this.grp.destroy(); 645 | this.grp = null; 646 | this.game = null; 647 | this.parent = null; 648 | this.items = null; 649 | } 650 | 651 | /** 652 | * [removeAll - removes all children from the group] 653 | * @note This does not reset the position of the ListView. 654 | */ 655 | 656 | }, { 657 | key: 'removeAll', 658 | value: function removeAll() { 659 | this.grp.removeAll(); 660 | this.items = []; 661 | } 662 | 663 | /** 664 | * [cull - culls the off-screen list elements] 665 | * mainly called internally with the autoCull property 666 | */ 667 | 668 | }, { 669 | key: 'cull', 670 | value: function cull() { 671 | for (var i = 0; i < this.items.length; i++) { 672 | var child = this.items[i]; 673 | child.visible = true; 674 | if (child[this.p.xy] + (0, _util.getWidthOrHeight)(child, this.p.wh) + this.grp[this.p.xy] < this.bounds[this.p.xy]) { 675 | child.visible = false; 676 | } else if (child[this.p.xy] + this.grp[this.p.xy] > this.bounds[this.p.xy] + this.bounds[this.p.wh]) { 677 | child.visible = false; 678 | } 679 | } 680 | } 681 | }, { 682 | key: 'getPositionByItemIndex', 683 | value: function getPositionByItemIndex(index) { 684 | return -this.items[index][this.p.xy]; 685 | } 686 | 687 | // @deprecated 688 | 689 | }, { 690 | key: 'setPosition', 691 | value: function setPosition(position) { 692 | this.moveToPosition(position); 693 | } 694 | }, { 695 | key: 'moveToPosition', 696 | value: function moveToPosition(position) { 697 | this.scroller.setTo(position); 698 | } 699 | }, { 700 | key: 'moveToItem', 701 | value: function moveToItem(index) { 702 | this.scroller.setTo(this.getPositionByItemIndex(index)); 703 | } 704 | }, { 705 | key: 'tweenToPosition', 706 | value: function tweenToPosition(position) { 707 | var duration = arguments.length <= 1 || arguments[1] === undefined ? 1 : arguments[1]; 708 | 709 | this.scroller.tweenTo(duration, position); 710 | } 711 | }, { 712 | key: 'tweenToItem', 713 | value: function tweenToItem(index) { 714 | var duration = arguments.length <= 1 || arguments[1] === undefined ? 1 : arguments[1]; 715 | 716 | this.scroller.tweenTo(duration, this.getPositionByItemIndex(index)); 717 | } 718 | 719 | /** 720 | * @private 721 | */ 722 | 723 | }, { 724 | key: '_setPosition', 725 | value: function _setPosition(position) { 726 | this.position = position; 727 | this.grp[this.p.xy] = this.bounds[this.p.xy] + position; 728 | if (this.o.autocull) this.cull(); 729 | } 730 | 731 | /** 732 | * @private 733 | */ 734 | 735 | }, { 736 | key: '_addMask', 737 | value: function _addMask(bounds) { 738 | var mask = this.game.add.graphics(0, 0, this.parent); 739 | mask.beginFill(0xff0000).drawRect(bounds.x, bounds.y, bounds.width, bounds.height); 740 | mask.alpha = 0; 741 | return mask; 742 | } 743 | }]); 744 | 745 | return ListViewCore; 746 | }(); 747 | 748 | exports.default = ListViewCore; 749 | 750 | /***/ }, 751 | /* 9 */ 752 | /***/ function(module, exports, __webpack_require__) { 753 | 754 | 'use strict'; 755 | 756 | Object.defineProperty(exports, "__esModule", { 757 | value: true 758 | }); 759 | exports.parseBounds = parseBounds; 760 | exports.getWidthOrHeight = getWidthOrHeight; 761 | exports.capitalizeFirstLetter = capitalizeFirstLetter; 762 | exports.findChild = findChild; 763 | exports.detectDrag = detectDrag; 764 | exports.dispatchClicks = dispatchClicks; 765 | 766 | var _config = __webpack_require__(10); 767 | 768 | var _config2 = _interopRequireDefault(_config); 769 | 770 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 771 | 772 | function parseBounds(bounds) { 773 | bounds.x = bounds.x ? bounds.x : 0; 774 | bounds.y = bounds.y ? bounds.y : 0; 775 | if (bounds.width <= 0) { 776 | console.warn('PhaserListView: bounds.width <= 0'); 777 | } else if (bounds.height <= 0) { 778 | console.warn('PhaserListView: bounds.height <= 0'); 779 | } 780 | return bounds; 781 | } 782 | 783 | // prefer nominalWidth and nominalHeight 784 | function getWidthOrHeight(displayObject, widthOrHeight) { 785 | return displayObject['nominal' + capitalizeFirstLetter(widthOrHeight)] || displayObject[widthOrHeight]; 786 | } 787 | 788 | function capitalizeFirstLetter(string) { 789 | return string.charAt(0).toUpperCase() + string.slice(1); 790 | } 791 | 792 | function findChild(children, predicate) { 793 | var scope = arguments.length <= 2 || arguments[2] === undefined ? null : arguments[2]; 794 | 795 | if (!children) return false; 796 | for (var i = 0; i < children.length; i++) { 797 | var child = children[i]; 798 | if (!child) continue; 799 | if (predicate.call(scope, child)) { 800 | return child; 801 | } 802 | var found = findChild(child.children, predicate, scope); 803 | if (found) { 804 | return found; 805 | } 806 | } 807 | return false; 808 | } 809 | 810 | function detectDrag(pointer) { 811 | var distanceX = Math.abs(pointer.positionDown.x - pointer.positionUp.x); 812 | var distanceY = Math.abs(pointer.positionDown.y - pointer.positionUp.y); 813 | var time = pointer.timeUp - pointer.timeDown; 814 | return distanceX > _config2.default.AUTO_DETECT_THRESHOLD || distanceY > _config2.default.AUTO_DETECT_THRESHOLD; 815 | } 816 | 817 | function dispatchClicks(pointer, clickables, type) { 818 | if (type == 'onInputUp' && detectDrag(pointer)) return; 819 | // SEARCH OBJECT UNDER POINT AS THERE IS NO CLICK PROPAGATION SUPPORT IN PHASER 820 | var found = findChild(clickables, function (clickable) { 821 | var pt = clickable.worldPosition; 822 | var anchor = clickable.anchor; 823 | var pivot = clickable.pivot; 824 | var width = clickable.width; 825 | var height = clickable.height; 826 | var scale = clickable.scale; 827 | 828 | var x = pt.x - (anchor ? anchor.x * width : 0) - pivot.x * scale.x; 829 | var y = pt.y - (anchor ? anchor.y * height : 0) - pivot.y * scale.y; 830 | // console.log('does ', x, y, clickable.width, clickable.height, ' intersect ', pointer.x, pointer.y) 831 | return clickable.inputEnabled && new Phaser.Rectangle(x, y, clickable.width, clickable.height).contains(pointer.x, pointer.y); 832 | }); 833 | if (found && found.events && found.events[type] && found.events[type].dispatch) { 834 | found.events[type].dispatch(found, pointer, true); 835 | } 836 | return found; 837 | } 838 | 839 | /***/ }, 840 | /* 10 */ 841 | /***/ function(module, exports) { 842 | 843 | "use strict"; 844 | 845 | Object.defineProperty(exports, "__esModule", { 846 | value: true 847 | }); 848 | var Config = { 849 | AUTO_DETECT_THRESHOLD: 8 850 | }; 851 | 852 | exports.default = Config; 853 | 854 | /***/ }, 855 | /* 11 */ 856 | /***/ function(module, exports, __webpack_require__) { 857 | 858 | 'use strict'; 859 | 860 | Object.defineProperty(exports, "__esModule", { 861 | value: true 862 | }); 863 | 864 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 865 | 866 | var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; 867 | 868 | var _math_utils = __webpack_require__(12); 869 | 870 | var _math_utils2 = _interopRequireDefault(_math_utils); 871 | 872 | var _scroller = __webpack_require__(13); 873 | 874 | var _scroller2 = _interopRequireDefault(_scroller); 875 | 876 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 877 | 878 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 879 | 880 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 881 | 882 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 883 | 884 | var DirectionalScroller = function (_Scroller) { 885 | _inherits(DirectionalScroller, _Scroller); 886 | 887 | function DirectionalScroller(game, clickObject) { 888 | var options = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; 889 | 890 | _classCallCheck(this, DirectionalScroller); 891 | 892 | return _possibleConstructorReturn(this, Object.getPrototypeOf(DirectionalScroller).call(this, game, clickObject, { x: clickObject.width, y: clickObject.height }, options)); 893 | } 894 | 895 | _createClass(DirectionalScroller, [{ 896 | key: 'handleDown', 897 | value: function handleDown(target, pointer) { 898 | this.old = this.down = pointer[this.o.direction]; 899 | _get(Object.getPrototypeOf(DirectionalScroller.prototype), 'handleDown', this).call(this, target, pointer); 900 | } 901 | }, { 902 | key: 'handleUp', 903 | value: function handleUp(target, pointer) { 904 | this.current = pointer[this.o.direction]; 905 | _get(Object.getPrototypeOf(DirectionalScroller.prototype), 'handleUp', this).call(this, target, pointer); 906 | } 907 | }]); 908 | 909 | return DirectionalScroller; 910 | }(_scroller2.default); 911 | 912 | exports.default = DirectionalScroller; 913 | 914 | /***/ }, 915 | /* 12 */ 916 | /***/ function(module, exports) { 917 | 918 | "use strict"; 919 | 920 | Object.defineProperty(exports, "__esModule", { 921 | value: true 922 | }); 923 | var MathUtils = { 924 | nearestMultiple: function nearestMultiple(n, multiple) { 925 | return Math.round(n / multiple) * multiple; 926 | }, 927 | 928 | scaleBetween: function scaleBetween(lo, hi, scale) { 929 | return lo + (hi - lo) * scale; 930 | }, 931 | 932 | // returns a percentage between hi and lo from a given input 933 | // e.g percentageBetween2(7, 4, 10) -> .5 934 | percentageBetween2: function percentageBetween2(input, lo, hi) { 935 | return (input - lo) / (hi - lo); 936 | } 937 | }; 938 | 939 | exports.default = MathUtils; 940 | 941 | /***/ }, 942 | /* 13 */ 943 | /***/ function(module, exports, __webpack_require__) { 944 | 945 | 'use strict'; 946 | 947 | Object.defineProperty(exports, "__esModule", { 948 | value: true 949 | }); 950 | 951 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 952 | 953 | var _math_utils = __webpack_require__(12); 954 | 955 | var _math_utils2 = _interopRequireDefault(_math_utils); 956 | 957 | var _util = __webpack_require__(9); 958 | 959 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 960 | 961 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 962 | 963 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 964 | 965 | var _ptHelper = new Phaser.Point(); 966 | 967 | var defaultOptions = { 968 | from: 0, 969 | to: 200, 970 | direction: 'y', 971 | momentum: false, 972 | snapping: false, 973 | bouncing: false, 974 | deceleration: 0.5, // value between 0 and 1 975 | overflow: 20, 976 | snapStep: 10, 977 | emitMoving: false, 978 | duration: 2, // (s) duration of the inertial scrolling simulation. 979 | speedLimit: 3, // set maximum speed. Higher values will allow faster scroll (which comes down to a bigger offset for the duration of the momentum scroll) note: touch motion determines actual speed, this is just a limit. 980 | flickTimeThreshold: 100, // (ms) determines if a flick occurred: time between last updated movement @ touchmove and time @ touchend, if smaller than this value, trigger inertial scrolling 981 | offsetThreshold: 30, // (pixels) determines if calculated offset is above this threshold 982 | acceleration: 0.5, // increase the multiplier by this value, each time the user swipes again when still scrolling. The multiplier is used to multiply the offset. Set to 0 to disable. 983 | accelerationT: 250, // (ms) time between successive swipes that determines if the multiplier is increased (if lower than this value) 984 | maxAcceleration: 4, 985 | time: {}, // contains timestamps of the most recent down, up, and move events 986 | multiplier: 1, //acceleration multiplier, don't edit here 987 | swipeEnabled: false, 988 | swipeThreshold: 5, // (pixels) must move this many pixels for a swipe action 989 | swipeTimeThreshold: 250, // (ms) determines if a swipe occurred: time between last updated movement @ touchmove and time @ touchend, if smaller than this value, trigger swipe 990 | minDuration: 0.5, 991 | addListeners: true 992 | }; 993 | 994 | // Pure logic scroller 995 | // Originally adapted from http://yusyuslabs.com/tutorial-momentum-scrolling-inside-scrollable-area-with-phaser-js/ 996 | // 997 | 998 | var Scroller = function () { 999 | function Scroller(game, clickObject) { 1000 | var maskLimits = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; 1001 | var options = arguments.length <= 3 || arguments[3] === undefined ? {} : arguments[3]; 1002 | 1003 | _classCallCheck(this, Scroller); 1004 | 1005 | this.game = game; 1006 | this.clickObject = clickObject; 1007 | 1008 | this.maskLimits = maskLimits; 1009 | 1010 | this.o = this.options = Object.assign({}, defaultOptions, options); 1011 | 1012 | this._updateMinMax(); 1013 | 1014 | this.dispatchValues = { step: 0, total: 0, percent: 0 }; 1015 | 1016 | this.addListeners(); 1017 | 1018 | this.clickables = []; 1019 | 1020 | this.isDown = false; // isDown is true when the down event has fired but the up event has not 1021 | this.isScrolling = false; // isScrolling is true when the down event has fired but the complete event has not 1022 | 1023 | this.scrollObject = {}; 1024 | 1025 | this.init(); 1026 | 1027 | this.tweenScroll = this.game.add.tween(this.scrollObject).to({}, 0, Phaser.Easing.Quartic.Out); 1028 | this.tweenScroll.onUpdateCallback(this.handleUpdate, this); 1029 | this.tweenScroll.onComplete.add(this.handleComplete, this); 1030 | } 1031 | 1032 | _createClass(Scroller, [{ 1033 | key: 'destroy', 1034 | value: function destroy() { 1035 | this.tweenScroll.stop(); 1036 | this.removeListeners(); 1037 | this.clickObject.destroy(); 1038 | this.clickables = null; 1039 | this.options = this.o = null; 1040 | this.maskLimits = null; 1041 | this.enabled = false; 1042 | this.game = null; 1043 | this.dispatchValues = null; 1044 | this.isDown = null; 1045 | this.target = null; 1046 | this.destroyed = true; 1047 | } 1048 | }, { 1049 | key: 'addListeners', 1050 | value: function addListeners() { 1051 | this.events = { 1052 | onUpdate: new Phaser.Signal(), 1053 | onInputUp: new Phaser.Signal(), 1054 | onInputDown: new Phaser.Signal(), 1055 | onInputMove: new Phaser.Signal(), 1056 | onComplete: new Phaser.Signal(), 1057 | onSwipe: new Phaser.Signal() 1058 | }; 1059 | 1060 | if (this.o.addListeners) { 1061 | this.clickObject.inputEnabled = true; 1062 | this.clickObject.events.onInputDown.add(this.handleDown, this); 1063 | this.clickObject.events.onInputUp.add(this.handleUp, this); 1064 | } 1065 | } 1066 | }, { 1067 | key: 'removeListeners', 1068 | value: function removeListeners() { 1069 | if (this.o.addListeners) { 1070 | this.clickObject.events.onInputDown.remove(this.handleDown, this); 1071 | this.clickObject.events.onInputUp.remove(this.handleUp, this); 1072 | } 1073 | 1074 | for (var property in this.events) { 1075 | if (this.events.hasOwnProperty(property)) { 1076 | this.events[property].dispose(); 1077 | } 1078 | } 1079 | } 1080 | }, { 1081 | key: 'enable', 1082 | value: function enable() { 1083 | this.enabled = true; 1084 | } 1085 | }, { 1086 | key: 'disable', 1087 | value: function disable() { 1088 | this.enabled = false; 1089 | } 1090 | }, { 1091 | key: 'init', 1092 | value: function init() { 1093 | this.scrollObject[this.o.direction] = this.o.from; 1094 | this.maxOffset = this.maskLimits[this.o.direction] * this.o.speedLimit; 1095 | this.enable(); 1096 | } 1097 | }, { 1098 | key: 'reset', 1099 | value: function reset() { 1100 | this.tweenScroll.pause(); 1101 | this.o.multiplier = 1; 1102 | this.init(); 1103 | } 1104 | }, { 1105 | key: 'setFromTo', 1106 | value: function setFromTo(_from, _to) { 1107 | this.o.from = _from; 1108 | this.o.to = _to; 1109 | this._updateMinMax(); 1110 | } 1111 | }, { 1112 | key: 'isTweening', 1113 | value: function isTweening() { 1114 | return this.tweenScroll.isRunning; 1115 | } 1116 | }, { 1117 | key: 'registerClickables', 1118 | value: function registerClickables(clickables) { 1119 | this.clickables = clickables; 1120 | } 1121 | }, { 1122 | key: 'handleDown', 1123 | value: function handleDown(target, pointer) { 1124 | if (!this.enabled) return; 1125 | this.isDown = true; 1126 | // console.log('input down', pointer.y) 1127 | this.target = this.requested = this.scrollObject[this.o.direction]; 1128 | this.o.time.down = pointer.timeDown; 1129 | 1130 | if (this.o.addListeners) this.game.input.addMoveCallback(this.handleMove, this); 1131 | 1132 | //check if block is currently scrolling and set multiplier 1133 | if (this.isTweening() && this.o.time.down - this.o.time.up < this.o.accelerationT) { 1134 | //swipe while animation was happening, increase multiplier 1135 | this.o.multiplier += this.o.acceleration; 1136 | // console.log('swipe while animation is happening', this.o.multiplier) 1137 | } else { 1138 | //reset 1139 | this.o.multiplier = 1; 1140 | } 1141 | 1142 | //stop tween for touch-to-stop 1143 | this.tweenScroll.stop(); 1144 | this.tweenScroll.pendingDelete = false; 1145 | 1146 | (0, _util.dispatchClicks)(pointer, this.clickables, 'onInputDown'); 1147 | this.events.onInputDown.dispatch(target, pointer); 1148 | } 1149 | }, { 1150 | key: 'handleMove', 1151 | value: function handleMove(pointer, x, y) { 1152 | if (!this.enabled) return; 1153 | this.isScrolling = true; 1154 | _ptHelper.set(x, y); 1155 | this.diff = this.old - _ptHelper[this.o.direction]; 1156 | 1157 | this.diff = this._requestDiff(this.diff, this.target, this.min, this.max, this.o.overflow); 1158 | 1159 | this.target -= this.diff; 1160 | 1161 | this.old = _ptHelper[this.o.direction]; 1162 | 1163 | //store timestamp for event 1164 | this.o.time.move = this.game.time.time; 1165 | 1166 | this.acc = Math.min(Math.abs(this.diff / 30), this.o.maxAcceleration); 1167 | 1168 | //go ahead and move the block 1169 | this.scrollObject[this.o.direction] = this.target; 1170 | this.handleUpdate(); 1171 | 1172 | if (this.o.emitMoving) this.events.onInputMove.dispatch(pointer, x, y); 1173 | } 1174 | }, { 1175 | key: 'handleUp', 1176 | value: function handleUp(target, pointer) { 1177 | this.isDown = false; 1178 | // console.log('end') 1179 | if (this.o.addListeners) this.game.input.deleteMoveCallback(this.handleMove, this); 1180 | 1181 | //store timestamp for event 1182 | this.o.time.up = pointer.timeUp; 1183 | 1184 | if (this.o.time.up - this.o.time.down > this.o.accelerationT) { 1185 | this.o.multiplier = 1; // reset 1186 | } 1187 | 1188 | var o = { 1189 | duration: 1, 1190 | target: this.target 1191 | }; 1192 | 1193 | // *** BOUNCING 1194 | if (!this.o.bouncing) o.duration = 0.01; 1195 | 1196 | if (!this.o.infinite && this.scrollObject[this.o.direction] > this.max) { 1197 | this.target = this.max; 1198 | this.tweenTo(o.duration, this.target); 1199 | } else if (!this.o.infinite && this.scrollObject[this.o.direction] < this.min) { 1200 | this.target = this.min; 1201 | this.tweenTo(o.duration, this.target); 1202 | } else { 1203 | // *** MOMENTUM 1204 | this._addMomentum(o); 1205 | 1206 | // *** SWIPING 1207 | this._addSwiping(o, pointer); 1208 | 1209 | // *** SNAPPING 1210 | this._addSnapping(o); 1211 | 1212 | // *** LIMITS 1213 | this._addLimits(o); 1214 | 1215 | // *** DURATION 1216 | this._calculateDuration(o); 1217 | 1218 | this.tweenTo(o.duration, o.target); 1219 | } 1220 | 1221 | (0, _util.dispatchClicks)(pointer, this.clickables, 'onInputUp'); 1222 | this.events.onInputUp.dispatch(target, pointer, _util.dispatchClicks); 1223 | } 1224 | }, { 1225 | key: '_addMomentum', 1226 | value: function _addMomentum(o) { 1227 | if (!this.o.momentum) return o.target; 1228 | 1229 | //distance to move after release 1230 | var offset = Math.pow(this.acc, 2) * this.maskLimits[this.o.direction]; 1231 | offset = Math.min(this.maxOffset, offset); 1232 | offset = this.diff > 0 ? -this.o.multiplier * offset : this.o.multiplier * offset; 1233 | 1234 | if (this.o.time.up - this.o.time.move < this.o.flickTimeThreshold && offset !== 0 && Math.abs(offset) > this.o.offsetThreshold) { 1235 | o.target += offset; 1236 | } 1237 | return o; 1238 | } 1239 | }, { 1240 | key: '_addSwiping', 1241 | value: function _addSwiping(o, pointer) { 1242 | var swipeDistance = Math.abs(this.down - this.current); 1243 | if (this.o.swipeEnabled && this.o.time.up - this.o.time.down < this.o.swipeTimeThreshold && swipeDistance > this.o.swipeThreshold) { 1244 | var direction = pointer[this.o.direction] < this.down ? 'forward' : 'backward'; 1245 | 1246 | if (direction == 'forward') { 1247 | o.target -= this.o.snapStep / 2; 1248 | } else { 1249 | o.target += this.o.snapStep / 2; 1250 | } 1251 | 1252 | this.events.onSwipe.dispatch(direction); 1253 | } 1254 | return o; 1255 | } 1256 | }, { 1257 | key: '_addSnapping', 1258 | value: function _addSnapping(o) { 1259 | if (!this.o.snapping) { 1260 | return o; 1261 | } 1262 | o.target = _math_utils2.default.nearestMultiple(o.target, this.o.snapStep); 1263 | return o; 1264 | } 1265 | }, { 1266 | key: '_addLimits', 1267 | value: function _addLimits(o) { 1268 | if (this.o.infinite) return o; 1269 | o.target = Math.max(o.target, this.min); 1270 | o.target = Math.min(o.target, this.max); 1271 | return o; 1272 | } 1273 | }, { 1274 | key: '_calculateDuration', 1275 | value: function _calculateDuration(o) { 1276 | var distance = Math.abs(o.target - this.scrollObject[this.o.direction]); 1277 | o.duration = this.o.duration * distance / this.maxOffset; 1278 | o.duration = Math.max(this.o.minDuration, o.duration); 1279 | return o; 1280 | } 1281 | }, { 1282 | key: '_requestDiff', 1283 | value: function _requestDiff(diff, target, min, max, overflow) { 1284 | if (this.o.infinite) return diff; 1285 | 1286 | var scale = 0; 1287 | if (target > max) { 1288 | scale = (max + overflow - target) / overflow; 1289 | diff *= scale; 1290 | } else if (target < min) { 1291 | scale = -(min - overflow - target) / overflow; 1292 | diff *= scale; 1293 | } 1294 | return diff; 1295 | } 1296 | }, { 1297 | key: 'tweenToSnap', 1298 | value: function tweenToSnap(duration, snapIndex) { 1299 | var target = this.o.from - this.o.snapStep * snapIndex; 1300 | this.tweenTo(duration, target); 1301 | } 1302 | 1303 | /** 1304 | * [tweenTo tween to scroller to the target] 1305 | * @param {Number} duration duration in seconds 1306 | * @param {Number} target target relative to the scroller space (usually pixels, but can be angle) 1307 | */ 1308 | 1309 | }, { 1310 | key: 'tweenTo', 1311 | value: function tweenTo(duration, target) { 1312 | if (duration == 0) return this.setTo(target); 1313 | 1314 | //stop a tween if it is currently happening 1315 | var o = _defineProperty({}, this.o.direction, target); 1316 | 1317 | this.tweenScroll.onUpdateCallback(this.handleUpdate, this); 1318 | this.tweenScroll.onComplete.add(this.handleComplete, this); 1319 | 1320 | this.tweenScroll.updateTweenData('vEnd', o, -1); 1321 | this.tweenScroll.updateTweenData('duration', duration * 1000, -1); 1322 | this.tweenScroll.updateTweenData('percent ', 0, -1); 1323 | 1324 | this.tweenScroll.start(); 1325 | } 1326 | 1327 | // TODO - not really sure what this cancel method should do. 1328 | // Obviously it's meant to cancel a currently active scroll...but I'm 1329 | // not sure what expect from that. 1330 | 1331 | }, { 1332 | key: 'cancel', 1333 | value: function cancel() { 1334 | this.isDown = false; 1335 | } 1336 | 1337 | /** 1338 | * [setTo sets the scroller to the target] 1339 | * @param {Number} target target relative to the scroller space (usually pixels, but can be angle) 1340 | */ 1341 | 1342 | }, { 1343 | key: 'setTo', 1344 | value: function setTo(target) { 1345 | //stop a tween if it is currently happening 1346 | this.scrollObject[this.o.direction] = target; 1347 | this.tweenScroll.stop(); 1348 | 1349 | this.handleUpdate(); 1350 | this.handleComplete(); 1351 | } 1352 | }, { 1353 | key: 'handleUpdate', 1354 | value: function handleUpdate() { 1355 | if (!this.enabled) return; 1356 | if (this.o.infinite) { 1357 | this.dispatchValues.total = Phaser.Math.wrap(this.scrollObject[this.o.direction], this.min, this.max); 1358 | } else { 1359 | this.dispatchValues.total = this.scrollObject[this.o.direction]; 1360 | } 1361 | 1362 | var step = this.dispatchValues.total - this.previousTotal; 1363 | if (step < -this.length / 2) { 1364 | step = step + this.length; 1365 | } else if (step > this.length / 2) { 1366 | step = step - this.length; 1367 | } 1368 | 1369 | this.dispatchValues.step = step; 1370 | this.dispatchValues.percent = _math_utils2.default.percentageBetween2(this.dispatchValues.total, this.o.from, this.o.to); 1371 | this.events.onUpdate.dispatch(this.dispatchValues); 1372 | 1373 | this.previousTotal = this.dispatchValues.total; 1374 | } 1375 | }, { 1376 | key: 'handleComplete', 1377 | value: function handleComplete() { 1378 | if (!this.enabled) return; 1379 | this.isScrolling = false; 1380 | // reset multiplier when finished 1381 | this.o.multiplier = 1; 1382 | this.events.onComplete.dispatch(); 1383 | } 1384 | }, { 1385 | key: '_updateMinMax', 1386 | value: function _updateMinMax() { 1387 | this.min = Math.min(this.o.from, this.o.to); 1388 | this.max = Math.max(this.o.from, this.o.to); 1389 | this.length = Math.abs(this.max - this.min); 1390 | this.previousTotal = this.o.from; 1391 | } 1392 | }]); 1393 | 1394 | return Scroller; 1395 | }(); 1396 | 1397 | exports.default = Scroller; 1398 | 1399 | /***/ }, 1400 | /* 14 */ 1401 | /***/ function(module, exports, __webpack_require__) { 1402 | 1403 | 'use strict'; 1404 | 1405 | Object.defineProperty(exports, "__esModule", { 1406 | value: true 1407 | }); 1408 | 1409 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 1410 | 1411 | var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; 1412 | 1413 | var _game_state = __webpack_require__(3); 1414 | 1415 | var _game_state2 = _interopRequireDefault(_game_state); 1416 | 1417 | var _math_utils = __webpack_require__(6); 1418 | 1419 | var _swipe_carousel = __webpack_require__(15); 1420 | 1421 | var _swipe_carousel2 = _interopRequireDefault(_swipe_carousel); 1422 | 1423 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 1424 | 1425 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 1426 | 1427 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 1428 | 1429 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 1430 | 1431 | var SwipeCarouselState = function (_GameState) { 1432 | _inherits(SwipeCarouselState, _GameState); 1433 | 1434 | function SwipeCarouselState() { 1435 | _classCallCheck(this, SwipeCarouselState); 1436 | 1437 | return _possibleConstructorReturn(this, Object.getPrototypeOf(SwipeCarouselState).apply(this, arguments)); 1438 | } 1439 | 1440 | _createClass(SwipeCarouselState, [{ 1441 | key: 'preload', 1442 | value: function preload() { 1443 | this.game.load.crossOrigin = 'anonymous'; 1444 | } 1445 | }, { 1446 | key: 'shutdown', 1447 | value: function shutdown() { 1448 | this.carousel.destroy(); 1449 | } 1450 | }, { 1451 | key: 'create', 1452 | value: function create() { 1453 | var maskW = 600; 1454 | var maskH = 200; 1455 | var boxW = maskW; 1456 | var boxH = 200; 1457 | 1458 | this.carousel = new _swipe_carousel2.default(this.game, this.world, new Phaser.Rectangle(this.world.centerX - maskW / 2, 120, maskW, 400)); 1459 | 1460 | for (var i = 0; i < 10; i++) { 1461 | var color = Phaser.Color.getRandomColor(); 1462 | var group = this.game.make.group(null); 1463 | var g = this.game.add.graphics(0, 0, group); 1464 | g.beginFill(color).drawRect(0, 0, boxW, boxH); 1465 | 1466 | var txt = this.game.add.text(boxW / 2, boxH / 2, i, { font: '40px Arial', fill: '#000' }, group); 1467 | txt.anchor.set(0.5); 1468 | var img = this.game.add.image(0, 0, group.generateTexture()); 1469 | this.carousel.add(img); 1470 | } 1471 | 1472 | _get(Object.getPrototypeOf(SwipeCarouselState.prototype), 'create', this).call(this); 1473 | } 1474 | }]); 1475 | 1476 | return SwipeCarouselState; 1477 | }(_game_state2.default); 1478 | 1479 | exports.default = SwipeCarouselState; 1480 | 1481 | /***/ }, 1482 | /* 15 */ 1483 | /***/ function(module, exports, __webpack_require__) { 1484 | 1485 | 'use strict'; 1486 | 1487 | Object.defineProperty(exports, "__esModule", { 1488 | value: true 1489 | }); 1490 | 1491 | var _list_view = __webpack_require__(7); 1492 | 1493 | var _list_view2 = _interopRequireDefault(_list_view); 1494 | 1495 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 1496 | 1497 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 1498 | 1499 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 1500 | 1501 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 1502 | 1503 | var defaultOptions = { 1504 | direction: 'x', 1505 | autocull: true, 1506 | momentum: false, 1507 | bouncing: true, 1508 | snapping: true, 1509 | overflow: 100, 1510 | padding: 10, 1511 | swipeEnabled: true, 1512 | offset: { 1513 | x: 100 1514 | } 1515 | }; 1516 | 1517 | var SwipeCarousel = function (_ListView) { 1518 | _inherits(SwipeCarousel, _ListView); 1519 | 1520 | function SwipeCarousel(game, parent, bounds) { 1521 | var options = arguments.length <= 3 || arguments[3] === undefined ? {} : arguments[3]; 1522 | 1523 | _classCallCheck(this, SwipeCarousel); 1524 | 1525 | var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(SwipeCarousel).call(this, game, parent, bounds, Object.assign({}, defaultOptions, options))); 1526 | 1527 | _this.scroller.options.snapStep = bounds.width + _this.o.padding; 1528 | return _this; 1529 | } 1530 | 1531 | return SwipeCarousel; 1532 | }(_list_view2.default); 1533 | 1534 | exports.default = SwipeCarousel; 1535 | 1536 | /***/ } 1537 | /******/ ]); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 13 |
14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phaser-list-view", 3 | "version": "1.5.2", 4 | "description": "ListView class for Phaser. Great for making high scoreboards!", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "start": "npm run run-demos", 8 | "compile": "babel --presets es2015 -d lib/ src/", 9 | "prepublish": "npm run compile", 10 | "build": "APP_ENV=production && NODE_ENV=production && webpack --progress --colors", 11 | "run-demos": "npm run build && export APP_ENV=demo && webpack --config webpack.demos.config.js", 12 | "build-demos": "export APP_ENV=demo && NODE_ENV=production && webpack --config webpack.demos.config.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/mattcolman/phaser-list-view.git" 17 | }, 18 | "keywords": [ 19 | "phaser", 20 | "list", 21 | "view", 22 | "swipe", 23 | "carousel", 24 | "scroller", 25 | "performance", 26 | "60fps" 27 | ], 28 | "author": "Matt Colman (http://www.mattcolman.com)", 29 | "license": "ISC", 30 | "bugs": { 31 | "url": "https://github.com/mattcolman/phaser-list-view/issues" 32 | }, 33 | "homepage": "https://github.com/mattcolman/phaser-list-view#readme", 34 | "devDependencies": { 35 | "babel-cli": "^6.7.7", 36 | "babel-core": "^6.21.0", 37 | "babel-loader": "^6.2.10", 38 | "babel-plugin-webpack-loaders": "^0.8.0", 39 | "babel-polyfill": "^6.20.0", 40 | "babel-preset-es2015": "^6.6.0", 41 | "babelify": "^7.3.0", 42 | "browser-sync": "^2.18.6", 43 | "browser-sync-webpack-plugin": "^1.1.3", 44 | "es6-promise": "^4.0.5", 45 | "exports-loader": "^0.6.3", 46 | "imports-loader": "^0.7.0", 47 | "path": "^0.12.7", 48 | "phaser": "^2.4.6", 49 | "webpack": "^1.14.0", 50 | "webpack-dev-server": "^1.16.2" 51 | }, 52 | "dependencies": {} 53 | } 54 | -------------------------------------------------------------------------------- /src/basic_swiper.js: -------------------------------------------------------------------------------- 1 | import MathUtils from './utils/math_utils'; 2 | 3 | const _ptHelper = new Phaser.Point(); 4 | const defaultOptions = { 5 | from: 0, 6 | to: 200, 7 | direction: 'y', 8 | snapStep: 10, 9 | duration: 1, // (s) duration of the inertial scrolling simulation. 10 | time: {}, // contains timestamps of the most recent down, up, and move events 11 | swipeThreshold: 5, // (pixels) must move this many pixels for a swipe action 12 | swipeTimeThreshold: 250, // (ms) determines if a swipe occurred: time between last updated movement @ touchmove and time @ touchend, if smaller than this value, trigger swipe 13 | addListeners: true 14 | }; 15 | 16 | // ** WORK IN PROGRESS ** 17 | // 18 | // Similar to the Scroller class but there is no focus on a start and end of the scroll surface. 19 | // For example with Scroller if you swiped left 3 times you would continue to go further left and 20 | // closer to the end of the limit. 21 | // With BasicSwiper if you swiped left 3 times, each time you receive values between -1 and 1, depending 22 | // on the direction you swipe. 23 | // 24 | // TODO - consolidate BasicSwiper and Scroller. At least they could share same functions 25 | // 26 | export default class BasicSwiper { 27 | constructor(game, clickObject, options = {}) { 28 | this.game = game; 29 | this.clickObject = clickObject; 30 | 31 | this.o = this.options = Object.assign({}, defaultOptions, options); 32 | 33 | this._updateMinMax(); 34 | 35 | this.addListeners(); 36 | 37 | this.scrollObject = {}; 38 | this.scrollObject[this.o.direction] = this.o.from; 39 | 40 | // set tween that will be re-used for moving scrolling sprite 41 | this.tweenScroll = this.game.add 42 | .tween(this.scrollObject) 43 | .to({}, 0, Phaser.Easing.Quartic.Out); 44 | this.tweenScroll.onUpdateCallback(this.handleUpdate, this); 45 | this.tweenScroll.onComplete.add(this.handleComplete, this); 46 | } 47 | 48 | addListeners() { 49 | this.events = { 50 | onUpdate: new Phaser.Signal(), 51 | onInputUp: new Phaser.Signal(), 52 | onInputDown: new Phaser.Signal(), 53 | onInputMove: new Phaser.Signal(), 54 | onComplete: new Phaser.Signal(), 55 | onSwipe: new Phaser.Signal() 56 | }; 57 | 58 | this.enable(); 59 | 60 | if (this.o.addListeners) { 61 | this.clickObject.inputEnabled = true; 62 | this.clickObject.events.onInputDown.add(this.handleDown, this); 63 | this.clickObject.events.onInputUp.add(this.handleUp, this); 64 | } 65 | } 66 | 67 | removeListeners() { 68 | if (this.o.addListeners) { 69 | this.clickObject.events.onInputDown.remove(this.handleDown, this); 70 | this.clickObject.events.onInputUp.remove(this.handleUp, this); 71 | } 72 | 73 | for (var property in this.events) { 74 | if (this.events.hasOwnProperty(property)) { 75 | this.events[property].removeAll(); 76 | } 77 | } 78 | } 79 | 80 | destroy() { 81 | this.removeListeners(); 82 | } 83 | 84 | enable() { 85 | this.enabled = true; 86 | } 87 | 88 | disable() { 89 | this.enabled = false; 90 | } 91 | 92 | isTweening() { 93 | return this.tweenScroll.isRunning; 94 | } 95 | 96 | handleDown(target, pointer) { 97 | if (!this.enabled) { 98 | this.clickBlocked = true; 99 | return; 100 | } 101 | this.clickBlocked = false; 102 | // console.log('handle down', pointer[this.o.direction]) 103 | this.isDown = true; 104 | // console.log('input down', pointer.y) 105 | this.old = this.down = pointer[this.o.direction]; 106 | this.target = 0; 107 | this.o.time.down = pointer.timeDown; 108 | 109 | if (this.o.addListeners) 110 | this.game.input.addMoveCallback(this.handleMove, this); 111 | 112 | //stop tween for touch-to-stop 113 | this.tweenScroll.stop(); 114 | this.tweenScroll.pendingDelete = false; 115 | 116 | this.events.onInputDown.dispatch(target, pointer); 117 | } 118 | 119 | handleMove(pointer, x, y) { 120 | if (!this.enabled) return; 121 | _ptHelper.set(x, y); 122 | this.diff = this.old - _ptHelper[this.o.direction]; 123 | // console.log('diff', this.diff) 124 | this.target -= this.diff; 125 | 126 | this.old = _ptHelper[this.o.direction]; 127 | 128 | //store timestamp for event 129 | this.o.time.move = this.game.time.time; 130 | 131 | //go ahead and move the block 132 | this.scrollObject[this.o.direction] = this.target; 133 | this.handleUpdate(); 134 | 135 | if (this.o.emitMoving) this.events.onInputMove.dispatch(pointer, x, y); 136 | } 137 | 138 | handleUp(target, pointer) { 139 | if (!this.enabled || this.clickBlocked) return; 140 | this.isDown = false; 141 | // console.log('end') 142 | if (this.o.addListeners) 143 | this.game.input.deleteMoveCallback(this.handleMove, this); 144 | 145 | //store timestamp for event 146 | this.o.time.up = pointer.timeUp; 147 | 148 | var o = { 149 | duration: this.o.duration, 150 | target: this.target 151 | }; 152 | 153 | // *** SWIPING 154 | this._addSwiping(o, pointer); 155 | 156 | // *** SNAPPING 157 | this._addSnapping(o); 158 | 159 | this.tweenTo(o.duration, o.target); 160 | 161 | this.events.onInputUp.dispatch(target, pointer); 162 | } 163 | 164 | _addSwiping(o, pointer) { 165 | let swipeDistance = Math.abs(this.down - this.old); 166 | if ( 167 | this.o.time.up - this.o.time.down < this.o.swipeTimeThreshold && 168 | swipeDistance > this.o.swipeThreshold 169 | ) { 170 | let direction = 171 | pointer[this.o.direction] < this.down ? 'forward' : 'backward'; 172 | 173 | if (direction == 'forward') { 174 | o.target -= this.o.snapStep / 2; 175 | } else { 176 | o.target += this.o.snapStep / 2; 177 | } 178 | 179 | this.events.onSwipe.dispatch(direction); 180 | } 181 | return o; 182 | } 183 | 184 | _addSnapping(o) { 185 | o.target = MathUtils.nearestMultiple(o.target, this.o.snapStep); 186 | return o; 187 | } 188 | 189 | tweenTo(duration, target) { 190 | // console.log('tweenTo', duration, target) 191 | //stop a tween if it is currently happening 192 | let o = {}; 193 | o[this.o.direction] = target; 194 | 195 | this.tweenScroll.onUpdateCallback(this.handleUpdate, this); 196 | this.tweenScroll.onComplete.add(this.handleComplete, this); 197 | 198 | this.tweenScroll.updateTweenData('vEnd', o, -1); 199 | this.tweenScroll.updateTweenData('duration', duration * 1000, -1); 200 | this.tweenScroll.updateTweenData('percent ', 0, -1); 201 | 202 | this.tweenScroll.start(); 203 | } 204 | 205 | // dispatches a value between -1 and 1 depending on the direction of the swipe action. 206 | handleUpdate() { 207 | this.events.onUpdate.dispatch( 208 | MathUtils.scaleBetween( 209 | -1, 210 | 1, 211 | MathUtils.percentageBetween2( 212 | this.scrollObject[this.o.direction], 213 | -this.length, 214 | this.length 215 | ) 216 | ) 217 | ); 218 | } 219 | 220 | handleComplete() { 221 | // reset multiplier when finished 222 | this.o.multiplier = 1; 223 | this.events.onComplete.dispatch(); 224 | } 225 | 226 | _updateMinMax() { 227 | this.min = Math.min(this.o.from, this.o.to); 228 | this.max = Math.max(this.o.from, this.o.to); 229 | this.length = Math.abs(this.max - this.min); 230 | this.o.snapStep = this.length; 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const Config = { 2 | AUTO_DETECT_THRESHOLD: 8 3 | }; 4 | 5 | export default Config; 6 | -------------------------------------------------------------------------------- /src/directional_scroller.js: -------------------------------------------------------------------------------- 1 | import MathUtils from './utils/math_utils'; 2 | import Scroller from './scroller'; 3 | 4 | export default class DirectionalScroller extends Scroller { 5 | constructor(game, clickObject, options = {}) { 6 | super( 7 | game, 8 | clickObject, 9 | { x: clickObject.width, y: clickObject.height }, 10 | options 11 | ); 12 | } 13 | 14 | handleDown(target, pointer) { 15 | this.old = this.down = pointer[this.o.direction]; 16 | super.handleDown(target, pointer); 17 | } 18 | 19 | handleUp(target, pointer) { 20 | this.current = pointer[this.o.direction]; 21 | super.handleUp(target, pointer); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/example/game.js: -------------------------------------------------------------------------------- 1 | import GameState from './states/game_state'; 2 | import Boot from './states/boot'; 3 | import ListViewExample from './states/list_view_state'; 4 | import SwipeCarouselExample from './states/swipe_carousel_state'; 5 | 6 | class Game extends Phaser.Game { 7 | setupStage() { 8 | this.input.maxPointers = 1; 9 | this.scale.scaleMode = Phaser.ScaleManager.USER_SCALE; 10 | this.scale.setMinMax( 11 | this.width / 2, 12 | this.height / 2, 13 | this.width, 14 | this.height 15 | ); 16 | // this.scale.forceOrientation(true) // landscape 17 | this.scale.pageAlignHorizontally = true; 18 | 19 | // if (this.device.desktop) { 20 | // this.scale.setResizeCallback(this.fitToWindow, this) 21 | // } else { 22 | // // Mobile 23 | // this.scale.setResizeCallback(this.fitToWindowMobile, this) 24 | // } 25 | } 26 | 27 | fitToWindowMobile() { 28 | let gameHeight = this.height; 29 | let windowAspectRatio = window.innerWidth / window.innerHeight; 30 | let gameWidth = Math.ceil(this.height * windowAspectRatio); 31 | this.scale.setGameSize(gameWidth, gameHeight); 32 | this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL; 33 | } 34 | 35 | fitToWindow() { 36 | let w = window.innerWidth / this.width; 37 | let h = window.innerHeight / this.height; 38 | let scale = Math.min(w, h); 39 | this.scale.setUserScale(scale, scale); 40 | } 41 | 42 | addStates() { 43 | this.states.forEach(([name, stateClass]) => { 44 | this.state.add(name, stateClass); 45 | }); 46 | } 47 | 48 | startGame() { 49 | console.log('start game'); 50 | this.stateIndex = 0; 51 | this.nextState(); 52 | } 53 | 54 | nextState() { 55 | this.gotoStateByIndex(this.stateIndex + 1); 56 | } 57 | 58 | prevState() { 59 | this.gotoStateByIndex(this.stateIndex - 1); 60 | } 61 | 62 | gotoStateByIndex(index) { 63 | index = Math.min(index, this.states.length - 1); 64 | index = Math.max(index, 1); 65 | this.stateIndex = index; 66 | this.state.start(this.states[index][0]); 67 | } 68 | 69 | addDropDownMenu() { 70 | this.experiments.forEach(a => { 71 | let [name, klass] = a; 72 | 73 | let option = document.createElement('option'); 74 | option.text = name; 75 | document.getElementById('selector').add(option); 76 | }); 77 | 78 | document.getElementById('selector').addEventListener('change', e => { 79 | this.state.start(e.target.value); 80 | }); 81 | } 82 | } 83 | 84 | Game.prototype.experiments = [ 85 | ['ListView Example', ListViewExample], 86 | ['SwipeCarousel Example', SwipeCarouselExample] 87 | ]; 88 | 89 | Game.prototype.states = [['boot', Boot]].concat(Game.prototype.experiments); 90 | 91 | export default Game; 92 | -------------------------------------------------------------------------------- /src/example/index.js: -------------------------------------------------------------------------------- 1 | import Game from './game'; 2 | 3 | var init = function() { 4 | var config = { 5 | width: 1366, 6 | height: 768, 7 | renderer: Phaser.AUTO, 8 | parent: 'content', 9 | resolution: 1, //window.devicePixelRatio, 10 | state: Game.prototype.states[0][1] 11 | }; 12 | let game = new Game(config); 13 | }; 14 | 15 | init(); 16 | -------------------------------------------------------------------------------- /src/example/states/boot.js: -------------------------------------------------------------------------------- 1 | class Boot extends Phaser.State { 2 | preload() { 3 | this.game.load.crossOrigin = 'anonymous'; 4 | } 5 | 6 | create() { 7 | console.log('boot me up'); 8 | this.game.addDropDownMenu(); 9 | this.game.setupStage(); 10 | this.game.addStates(); 11 | this.game.startGame(); 12 | } 13 | } 14 | 15 | export default Boot; 16 | -------------------------------------------------------------------------------- /src/example/states/game_state.js: -------------------------------------------------------------------------------- 1 | class GameState extends Phaser.State { 2 | create() { 3 | this.game.time.advancedTiming = true; 4 | this.stage.backgroundColor = '#4488AA'; 5 | this.fpsTxt = this.game.add.text(50, 20, this.game.time.fps || '--', { 6 | font: '24px Arial', 7 | fill: '#00ff00' 8 | }); 9 | let txt = this.add.text( 10 | this.world.centerX, 11 | 50, 12 | this.name || this.game.state.current, 13 | { font: `30px Arial`, fill: '#fff' } 14 | ); 15 | txt.anchor.set(0.5); 16 | } 17 | 18 | update() { 19 | this.fpsTxt.text = this.game.time.fps; // debug text doesn't work with the canvas renderer?? 20 | this.fpsTxt.bringToTop(); 21 | } 22 | } 23 | 24 | export default GameState; 25 | -------------------------------------------------------------------------------- /src/example/states/list_view_state.js: -------------------------------------------------------------------------------- 1 | import GameState from './game_state'; 2 | import { scaleBetween } from '../utils/math_utils'; 3 | import ListView from '../../list_view'; 4 | 5 | class ListViewState extends GameState { 6 | preload() { 7 | this.game.load.crossOrigin = 'anonymous'; 8 | } 9 | 10 | shutdown() { 11 | this.listView.destroy(); 12 | } 13 | 14 | create() { 15 | var maskW = 600; 16 | var maskH = 200; 17 | var boxW = maskW; 18 | var boxH = 50; 19 | 20 | this.listView = new ListView( 21 | this.game, 22 | this.world, 23 | new Phaser.Rectangle(this.world.centerX - maskW / 2, 120, maskW, 400), 24 | { 25 | direction: 'y' 26 | } 27 | ); 28 | 29 | for (var i = 0; i < 500; i++) { 30 | let color = Phaser.Color.getRandomColor(); 31 | let group = this.game.make.group(null); 32 | let g = this.game.add.graphics(0, 0, group); 33 | let h = boxH + Math.floor(Math.random() * 100); 34 | g.beginFill(color).drawRect(0, 0, boxW, h); 35 | 36 | let txt = this.game.add.text( 37 | boxW / 2, 38 | h / 2, 39 | i, 40 | { font: '40px Arial', fill: '#000' }, 41 | group 42 | ); 43 | txt.anchor.set(0.5); 44 | let img = this.game.add.image(0, 0, group.generateTexture()); 45 | this.listView.add(img); 46 | } 47 | 48 | this.listView.moveToItem(3); 49 | super.create(); 50 | } 51 | } 52 | 53 | export default ListViewState; 54 | -------------------------------------------------------------------------------- /src/example/states/swipe_carousel_state.js: -------------------------------------------------------------------------------- 1 | import GameState from './game_state'; 2 | import { scaleBetween } from '../utils/math_utils'; 3 | import SwipeCarousel from '../../swipe_carousel'; 4 | 5 | class SwipeCarouselState extends GameState { 6 | preload() { 7 | this.game.load.crossOrigin = 'anonymous'; 8 | } 9 | 10 | shutdown() { 11 | this.carousel.destroy(); 12 | } 13 | 14 | create() { 15 | var maskW = 600; 16 | var maskH = 200; 17 | var boxW = maskW; 18 | var boxH = 200; 19 | 20 | this.carousel = new SwipeCarousel( 21 | this.game, 22 | this.world, 23 | new Phaser.Rectangle(this.world.centerX - maskW / 2, 120, maskW, 400) 24 | ); 25 | 26 | for (var i = 0; i < 10; i++) { 27 | let color = Phaser.Color.getRandomColor(); 28 | let group = this.game.make.group(null); 29 | let g = this.game.add.graphics(0, 0, group); 30 | g.beginFill(color).drawRect(0, 0, boxW, boxH); 31 | 32 | let txt = this.game.add.text( 33 | boxW / 2, 34 | boxH / 2, 35 | i, 36 | { font: '40px Arial', fill: '#000' }, 37 | group 38 | ); 39 | txt.anchor.set(0.5); 40 | let img = this.game.add.image(0, 0, group.generateTexture()); 41 | this.carousel.add(img); 42 | } 43 | 44 | super.create(); 45 | } 46 | } 47 | 48 | export default SwipeCarouselState; 49 | -------------------------------------------------------------------------------- /src/example/utils/array_utils.js: -------------------------------------------------------------------------------- 1 | function rotateLeft(arr) { 2 | let firstEl = arr.shift(); 3 | arr.push(firstEl); 4 | return arr; 5 | } 6 | 7 | function rotateRight(arr) { 8 | let lastEl = arr.pop(); 9 | arr.unshift(lastEl); 10 | return arr; 11 | } 12 | 13 | export { rotateLeft, rotateRight }; 14 | -------------------------------------------------------------------------------- /src/example/utils/bitmap_data_utils.js: -------------------------------------------------------------------------------- 1 | var BitmapDataUtils = { 2 | roundRect(ctx, x, y, w, h, r) { 3 | if (w < 2 * r) r = w / 2; 4 | if (h < 2 * r) r = h / 2; 5 | ctx.beginPath(); 6 | ctx.moveTo(x + r, y); 7 | ctx.arcTo(x + w, y, x + w, y + h, r); 8 | ctx.arcTo(x + w, y + h, x, y + h, r); 9 | ctx.arcTo(x, y + h, x, y, r); 10 | ctx.arcTo(x, y, x + w, y, r); 11 | ctx.closePath(); 12 | return ctx; 13 | } 14 | }; 15 | 16 | export default BitmapDataUtils; 17 | -------------------------------------------------------------------------------- /src/example/utils/layout.js: -------------------------------------------------------------------------------- 1 | var Layout = { 2 | stack(items, padding) { 3 | var y = 0; 4 | items.forEach(_item => { 5 | _item.y = y; 6 | y += _item.props.height + padding; 7 | }, this); 8 | }, 9 | 10 | line(items, padding) { 11 | var x = 0; 12 | items.forEach(_item => { 13 | _item.x = x; 14 | x += _item.width + padding; 15 | }, this); 16 | } 17 | }; 18 | 19 | export default Layout; 20 | -------------------------------------------------------------------------------- /src/example/utils/math_utils.js: -------------------------------------------------------------------------------- 1 | var MathUtils = { 2 | nearestMultiple: function(n, multiple) { 3 | return Math.round(n / multiple) * multiple; 4 | }, 5 | 6 | scaleBetween: function(lo, hi, scale) { 7 | return lo + (hi - lo) * scale; 8 | }, 9 | 10 | // returns a percentage between hi and lo from a given input 11 | // e.g percentageBetween2(7, 4, 10) -> .5 12 | percentageBetween2: function(input, lo, hi) { 13 | return (input - lo) / (hi - lo); 14 | } 15 | }; 16 | 17 | window.MathUtils = MathUtils; 18 | 19 | export default MathUtils; 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Scroller from './scroller'; 2 | import ListView from './list_view'; 3 | import ListViewCore from './list_view_core'; 4 | import SwipeCarousel from './swipe_carousel'; 5 | import WheelScroller from './wheel_scroller'; 6 | import DirectionalScroller from './directional_scroller'; 7 | import BasicSwiper from './basic_swiper'; 8 | import ScrollerEventDispatcher from './scroller_event_dispatcher'; 9 | 10 | const PhaserListView = { 11 | Scroller, 12 | ListView, 13 | ListViewCore, 14 | SwipeCarousel, 15 | WheelScroller, 16 | DirectionalScroller, 17 | BasicSwiper, 18 | ScrollerEventDispatcher 19 | }; 20 | 21 | export { 22 | Scroller, 23 | ListView, 24 | ListViewCore, 25 | SwipeCarousel, 26 | WheelScroller, 27 | DirectionalScroller, 28 | BasicSwiper, 29 | ScrollerEventDispatcher 30 | }; 31 | 32 | // NOTE: we should only attach to the window in a production build 33 | window.PhaserListView = PhaserListView; 34 | 35 | export default PhaserListView; 36 | -------------------------------------------------------------------------------- /src/list_view.js: -------------------------------------------------------------------------------- 1 | import ListViewCore from './list_view_core'; 2 | import DirectionalScroller from './directional_scroller'; 3 | import { parseBounds } from './util'; 4 | 5 | const defaultOptions = { 6 | direction: 'y', 7 | autocull: true, 8 | momentum: true, 9 | bouncing: true, 10 | snapping: false, 11 | overflow: 100, 12 | padding: 10, 13 | searchForClicks: false // if you just click on the list view it will search the list view items for onInputDown and onInputUp events. 14 | }; 15 | 16 | export default class ListView extends ListViewCore { 17 | constructor(game, parent, bounds, options = {}) { 18 | super( 19 | game, 20 | parent, 21 | parseBounds(bounds), 22 | Object.assign({}, defaultOptions, options) 23 | ); 24 | 25 | // we have to use a new mask instance for the click object or webgl ignores the mask 26 | this.scroller = new DirectionalScroller( 27 | this.game, 28 | this._addMask(bounds), 29 | Object.assign( 30 | { 31 | from: 0, 32 | to: 0 33 | }, 34 | this.options 35 | ) 36 | ); 37 | this.scroller.events.onUpdate.add(o => { 38 | this._setPosition(o.total); 39 | }); 40 | this.events.onAdded.add(limit => { 41 | const _to = Math.min(-limit, 0); 42 | this.scroller.setFromTo(0, _to); 43 | if (this.options.searchForClicks) { 44 | this.scroller.registerClickables(this.items); 45 | } 46 | }); 47 | } 48 | 49 | destroy() { 50 | this.scroller.destroy(); 51 | this.scroller = null; 52 | super.destroy(); 53 | } 54 | 55 | reset() { 56 | this._setPosition(0); 57 | this.scroller.reset(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/list_view_core.js: -------------------------------------------------------------------------------- 1 | import { getWidthOrHeight } from './util'; 2 | 3 | const defaultOptions = { 4 | direction: 'y', 5 | autocull: true, 6 | padding: 10 7 | }; 8 | 9 | export default class ListViewCore { 10 | constructor(game, parent, bounds, options = {}) { 11 | this.game = game; 12 | this.parent = parent; 13 | this.bounds = bounds; 14 | 15 | this.o = this.options = Object.assign({}, defaultOptions, options); 16 | 17 | this.items = []; 18 | 19 | if (this.o.direction == 'y') { 20 | this.p = { xy: 'y', wh: 'height' }; 21 | } else { 22 | this.p = { xy: 'x', wh: 'width' }; 23 | } 24 | 25 | this.grp = this.game.add.group(parent); 26 | this.grp.position.set(bounds.x, bounds.y); 27 | 28 | this.events = { 29 | onAdded: new Phaser.Signal() 30 | }; 31 | 32 | this.position = 0; 33 | 34 | // [MC] - is masking the fastest option here? Cropping the texture may be faster? 35 | this.grp.mask = this._addMask(bounds); 36 | } 37 | 38 | /** 39 | * [add a child to the list 40 | * stacks them on top of each other by measuring their 41 | * height and adding custom padding. Optionally you can 42 | * specify nominalHeight or nominalWidth on the display object, 43 | * this will take preference over height and width] 44 | * @param {DisplayObject} child 45 | */ 46 | add(child) { 47 | this.items.push(child); 48 | let xy = 0; 49 | if (this.grp.children.length > 0) { 50 | let lastChild = this.grp.getChildAt(this.grp.children.length - 1); 51 | xy = 52 | lastChild[this.p.xy] + 53 | getWidthOrHeight(lastChild, this.p.wh) + 54 | this.o.padding; 55 | } 56 | child[this.p.xy] = xy; 57 | this.grp.addChild(child); 58 | this.length = xy + child[this.p.wh]; 59 | 60 | // this._setPosition(this.position) 61 | this.events.onAdded.dispatch(this.length - this.bounds[this.p.wh]); 62 | return child; 63 | } 64 | 65 | /** 66 | * [addMultiple children to the list] 67 | * @param {...[DisplayObjects]} children 68 | */ 69 | addMultiple(...children) { 70 | children.forEach(this.add, this); 71 | } 72 | 73 | remove(child) { 74 | this.grp.removeChild(child); 75 | const index = this.items.indexOf(child); 76 | if (index == -1) return; 77 | this.items.splice(index, 1); 78 | return child; 79 | } 80 | 81 | destroy() { 82 | this.events.onAdded.dispose(); 83 | this.events = null; 84 | this.grp.destroy(); 85 | this.grp = null; 86 | this.game = null; 87 | this.parent = null; 88 | this.items = null; 89 | } 90 | 91 | /** 92 | * [removeAll - removes all children from the group] 93 | * @note This does not reset the position of the ListView. 94 | */ 95 | removeAll() { 96 | this.grp.removeAll(); 97 | this.items = []; 98 | } 99 | 100 | /** 101 | * [cull - culls the off-screen list elements] 102 | * mainly called internally with the autoCull property 103 | */ 104 | cull() { 105 | for (var i = 0; i < this.items.length; i++) { 106 | let child = this.items[i]; 107 | child.visible = true; 108 | if ( 109 | child[this.p.xy] + 110 | getWidthOrHeight(child, this.p.wh) + 111 | this.grp[this.p.xy] < 112 | this.bounds[this.p.xy] 113 | ) { 114 | child.visible = false; 115 | } else if ( 116 | child[this.p.xy] + this.grp[this.p.xy] > 117 | this.bounds[this.p.xy] + this.bounds[this.p.wh] 118 | ) { 119 | child.visible = false; 120 | } 121 | } 122 | } 123 | 124 | getPositionByItemIndex(index) { 125 | return -this.items[index][this.p.xy]; 126 | } 127 | 128 | // @deprecated 129 | setPosition(position) { 130 | this.moveToPosition(position); 131 | } 132 | 133 | moveToPosition(position) { 134 | this.scroller.setTo(position); 135 | } 136 | 137 | moveToItem(index) { 138 | this.scroller.setTo(this.getPositionByItemIndex(index)); 139 | } 140 | 141 | tweenToPosition(position, duration = 1) { 142 | this.scroller.tweenTo(duration, position); 143 | } 144 | 145 | tweenToItem(index, duration = 1) { 146 | this.scroller.tweenTo(duration, this.getPositionByItemIndex(index)); 147 | } 148 | 149 | /** 150 | * @private 151 | */ 152 | 153 | _setPosition(position) { 154 | this.position = position; 155 | this.grp[this.p.xy] = this.bounds[this.p.xy] + position; 156 | if (this.o.autocull) this.cull(); 157 | } 158 | 159 | /** 160 | * @private 161 | */ 162 | _addMask(bounds) { 163 | let mask = this.game.add.graphics(0, 0, this.parent); 164 | mask 165 | .beginFill(0xff0000) 166 | .drawRect(bounds.x, bounds.y, bounds.width, bounds.height); 167 | mask.alpha = 0; 168 | return mask; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/scroller.js: -------------------------------------------------------------------------------- 1 | import MathUtils from './utils/math_utils'; 2 | import { findChild, detectDrag, dispatchClicks } from './util'; 3 | 4 | const _ptHelper = new Phaser.Point(); 5 | 6 | const defaultOptions = { 7 | from: 0, 8 | to: 200, 9 | direction: 'y', 10 | momentum: false, 11 | snapping: false, 12 | bouncing: false, 13 | deceleration: 0.5, // value between 0 and 1 14 | overflow: 20, 15 | snapStep: 10, 16 | emitMoving: false, 17 | duration: 2, // (s) duration of the inertial scrolling simulation. 18 | speedLimit: 3, // set maximum speed. Higher values will allow faster scroll (which comes down to a bigger offset for the duration of the momentum scroll) note: touch motion determines actual speed, this is just a limit. 19 | flickTimeThreshold: 100, // (ms) determines if a flick occurred: time between last updated movement @ touchmove and time @ touchend, if smaller than this value, trigger inertial scrolling 20 | offsetThreshold: 30, // (pixels) determines if calculated offset is above this threshold 21 | acceleration: 0.5, // increase the multiplier by this value, each time the user swipes again when still scrolling. The multiplier is used to multiply the offset. Set to 0 to disable. 22 | accelerationT: 250, // (ms) time between successive swipes that determines if the multiplier is increased (if lower than this value) 23 | maxAcceleration: 4, 24 | time: {}, // contains timestamps of the most recent down, up, and move events 25 | multiplier: 1, //acceleration multiplier, don't edit here 26 | swipeEnabled: false, 27 | swipeThreshold: 5, // (pixels) must move this many pixels for a swipe action 28 | swipeTimeThreshold: 250, // (ms) determines if a swipe occurred: time between last updated movement @ touchmove and time @ touchend, if smaller than this value, trigger swipe 29 | minDuration: 0.5, 30 | addListeners: true 31 | }; 32 | 33 | // Pure logic scroller 34 | // Originally adapted from http://yusyuslabs.com/tutorial-momentum-scrolling-inside-scrollable-area-with-phaser-js/ 35 | // 36 | export default class Scroller { 37 | constructor(game, clickObject, maskLimits = {}, options = {}) { 38 | this.game = game; 39 | this.clickObject = clickObject; 40 | 41 | this.maskLimits = maskLimits; 42 | 43 | this.o = this.options = Object.assign({}, defaultOptions, options); 44 | 45 | this._updateMinMax(); 46 | 47 | this.dispatchValues = { step: 0, total: 0, percent: 0 }; 48 | 49 | this.addListeners(); 50 | 51 | this.clickables = []; 52 | 53 | this.isDown = false; // isDown is true when the down event has fired but the up event has not 54 | this.isScrolling = false; // isScrolling is true when the down event has fired but the complete event has not 55 | 56 | this.scrollObject = {}; 57 | 58 | this.init(); 59 | 60 | this.tweenScroll = this.game.add 61 | .tween(this.scrollObject) 62 | .to({}, 0, Phaser.Easing.Quartic.Out); 63 | this.tweenScroll.onUpdateCallback(this.handleUpdate, this); 64 | this.tweenScroll.onComplete.add(this.handleComplete, this); 65 | } 66 | 67 | destroy() { 68 | this.tweenScroll.stop(); 69 | this.removeListeners(); 70 | this.clickObject.destroy(); 71 | this.clickables = null; 72 | this.options = this.o = null; 73 | this.maskLimits = null; 74 | this.enabled = false; 75 | this.game = null; 76 | this.dispatchValues = null; 77 | this.isDown = null; 78 | this.target = null; 79 | this.destroyed = true; 80 | } 81 | 82 | addListeners() { 83 | this.events = { 84 | onUpdate: new Phaser.Signal(), 85 | onInputUp: new Phaser.Signal(), 86 | onInputDown: new Phaser.Signal(), 87 | onInputMove: new Phaser.Signal(), 88 | onComplete: new Phaser.Signal(), 89 | onSwipe: new Phaser.Signal() 90 | }; 91 | 92 | if (this.o.addListeners) { 93 | this.clickObject.inputEnabled = true; 94 | this.clickObject.events.onInputDown.add(this.handleDown, this); 95 | this.clickObject.events.onInputUp.add(this.handleUp, this); 96 | } 97 | } 98 | 99 | removeListeners() { 100 | if (this.o.addListeners) { 101 | this.clickObject.events.onInputDown.remove(this.handleDown, this); 102 | this.clickObject.events.onInputUp.remove(this.handleUp, this); 103 | } 104 | 105 | for (var property in this.events) { 106 | if (this.events.hasOwnProperty(property)) { 107 | this.events[property].dispose(); 108 | } 109 | } 110 | } 111 | 112 | enable() { 113 | this.enabled = true; 114 | } 115 | 116 | disable() { 117 | this.enabled = false; 118 | } 119 | 120 | init() { 121 | this.scrollObject[this.o.direction] = this.o.from; 122 | this.maxOffset = this.maskLimits[this.o.direction] * this.o.speedLimit; 123 | this.enable(); 124 | } 125 | 126 | reset() { 127 | this.tweenScroll.pause(); 128 | this.o.multiplier = 1; 129 | this.init(); 130 | } 131 | 132 | setFromTo(_from, _to) { 133 | this.o.from = _from; 134 | this.o.to = _to; 135 | this._updateMinMax(); 136 | } 137 | 138 | isTweening() { 139 | return this.tweenScroll.isRunning; 140 | } 141 | 142 | registerClickables(clickables) { 143 | this.clickables = clickables; 144 | } 145 | 146 | handleDown(target, pointer) { 147 | if (!this.enabled) return; 148 | this.isDown = true; 149 | // console.log('input down', pointer.y) 150 | this.target = this.requested = this.scrollObject[this.o.direction]; 151 | this.o.time.down = pointer.timeDown; 152 | 153 | if (this.o.addListeners) 154 | this.game.input.addMoveCallback(this.handleMove, this); 155 | 156 | //check if block is currently scrolling and set multiplier 157 | if ( 158 | this.isTweening() && 159 | this.o.time.down - this.o.time.up < this.o.accelerationT 160 | ) { 161 | //swipe while animation was happening, increase multiplier 162 | this.o.multiplier += this.o.acceleration; 163 | // console.log('swipe while animation is happening', this.o.multiplier) 164 | } else { 165 | //reset 166 | this.o.multiplier = 1; 167 | } 168 | 169 | //stop tween for touch-to-stop 170 | this.tweenScroll.stop(); 171 | this.tweenScroll.pendingDelete = false; 172 | 173 | dispatchClicks(pointer, this.clickables, 'onInputDown'); 174 | this.events.onInputDown.dispatch(target, pointer); 175 | } 176 | 177 | handleMove(pointer, x, y) { 178 | if (!this.enabled) return; 179 | this.isScrolling = true; 180 | _ptHelper.set(x, y); 181 | this.diff = this.old - _ptHelper[this.o.direction]; 182 | 183 | this.diff = this._requestDiff( 184 | this.diff, 185 | this.target, 186 | this.min, 187 | this.max, 188 | this.o.overflow 189 | ); 190 | 191 | this.target -= this.diff; 192 | 193 | this.old = _ptHelper[this.o.direction]; 194 | 195 | //store timestamp for event 196 | this.o.time.move = this.game.time.time; 197 | 198 | this.acc = Math.min(Math.abs(this.diff / 30), this.o.maxAcceleration); 199 | 200 | //go ahead and move the block 201 | this.scrollObject[this.o.direction] = this.target; 202 | this.handleUpdate(); 203 | 204 | if (this.o.emitMoving) this.events.onInputMove.dispatch(pointer, x, y); 205 | } 206 | 207 | handleUp(target, pointer) { 208 | this.isDown = false; 209 | // console.log('end') 210 | if (this.o.addListeners) 211 | this.game.input.deleteMoveCallback(this.handleMove, this); 212 | 213 | //store timestamp for event 214 | this.o.time.up = pointer.timeUp; 215 | 216 | if (this.o.time.up - this.o.time.down > this.o.accelerationT) { 217 | this.o.multiplier = 1; // reset 218 | } 219 | 220 | var o = { 221 | duration: 1, 222 | target: this.target 223 | }; 224 | 225 | // *** BOUNCING 226 | if (!this.o.bouncing) o.duration = 0.01; 227 | 228 | if (!this.o.infinite && this.scrollObject[this.o.direction] > this.max) { 229 | this.target = this.max; 230 | this.tweenTo(o.duration, this.target); 231 | } else if ( 232 | !this.o.infinite && 233 | this.scrollObject[this.o.direction] < this.min 234 | ) { 235 | this.target = this.min; 236 | this.tweenTo(o.duration, this.target); 237 | } else { 238 | // *** MOMENTUM 239 | this._addMomentum(o); 240 | 241 | // *** SWIPING 242 | this._addSwiping(o, pointer); 243 | 244 | // *** SNAPPING 245 | this._addSnapping(o); 246 | 247 | // *** LIMITS 248 | this._addLimits(o); 249 | 250 | // *** DURATION 251 | this._calculateDuration(o); 252 | 253 | this.tweenTo(o.duration, o.target); 254 | } 255 | 256 | dispatchClicks(pointer, this.clickables, 'onInputUp'); 257 | this.events.onInputUp.dispatch(target, pointer, dispatchClicks); 258 | } 259 | 260 | _addMomentum(o) { 261 | if (!this.o.momentum) return o.target; 262 | 263 | //distance to move after release 264 | let offset = Math.pow(this.acc, 2) * this.maskLimits[this.o.direction]; 265 | offset = Math.min(this.maxOffset, offset); 266 | offset = 267 | this.diff > 0 ? -this.o.multiplier * offset : this.o.multiplier * offset; 268 | 269 | if ( 270 | this.o.time.up - this.o.time.move < this.o.flickTimeThreshold && 271 | offset !== 0 && 272 | Math.abs(offset) > this.o.offsetThreshold 273 | ) { 274 | o.target += offset; 275 | } 276 | return o; 277 | } 278 | 279 | _addSwiping(o, pointer) { 280 | let swipeDistance = Math.abs(this.down - this.current); 281 | if ( 282 | this.o.swipeEnabled && 283 | this.o.time.up - this.o.time.down < this.o.swipeTimeThreshold && 284 | swipeDistance > this.o.swipeThreshold 285 | ) { 286 | let direction = 287 | pointer[this.o.direction] < this.down ? 'forward' : 'backward'; 288 | 289 | if (direction == 'forward') { 290 | o.target -= this.o.snapStep / 2; 291 | } else { 292 | o.target += this.o.snapStep / 2; 293 | } 294 | 295 | this.events.onSwipe.dispatch(direction); 296 | } 297 | return o; 298 | } 299 | 300 | _addSnapping(o) { 301 | if (!this.o.snapping) { 302 | return o; 303 | } 304 | o.target = MathUtils.nearestMultiple(o.target, this.o.snapStep); 305 | return o; 306 | } 307 | 308 | _addLimits(o) { 309 | if (this.o.infinite) return o; 310 | o.target = Math.max(o.target, this.min); 311 | o.target = Math.min(o.target, this.max); 312 | return o; 313 | } 314 | 315 | _calculateDuration(o) { 316 | let distance = Math.abs(o.target - this.scrollObject[this.o.direction]); 317 | o.duration = this.o.duration * distance / this.maxOffset; 318 | o.duration = Math.max(this.o.minDuration, o.duration); 319 | return o; 320 | } 321 | 322 | _requestDiff(diff, target, min, max, overflow) { 323 | if (this.o.infinite) return diff; 324 | 325 | let scale = 0; 326 | if (target > max) { 327 | scale = (max + overflow - target) / overflow; 328 | diff *= scale; 329 | } else if (target < min) { 330 | scale = -(min - overflow - target) / overflow; 331 | diff *= scale; 332 | } 333 | return diff; 334 | } 335 | 336 | tweenToSnap(duration, snapIndex) { 337 | let target = this.o.from - this.o.snapStep * snapIndex; 338 | this.tweenTo(duration, target); 339 | } 340 | 341 | /** 342 | * [tweenTo tween to scroller to the target] 343 | * @param {Number} duration duration in seconds 344 | * @param {Number} target target relative to the scroller space (usually pixels, but can be angle) 345 | */ 346 | tweenTo(duration, target) { 347 | if (duration == 0) return this.setTo(target); 348 | 349 | //stop a tween if it is currently happening 350 | const o = { 351 | [this.o.direction]: target 352 | }; 353 | 354 | this.tweenScroll.onUpdateCallback(this.handleUpdate, this); 355 | this.tweenScroll.onComplete.add(this.handleComplete, this); 356 | 357 | this.tweenScroll.updateTweenData('vEnd', o, -1); 358 | this.tweenScroll.updateTweenData('duration', duration * 1000, -1); 359 | this.tweenScroll.updateTweenData('percent ', 0, -1); 360 | 361 | this.tweenScroll.start(); 362 | } 363 | 364 | // TODO - not really sure what this cancel method should do. 365 | // Obviously it's meant to cancel a currently active scroll...but I'm 366 | // not sure what expect from that. 367 | cancel() { 368 | this.isDown = false; 369 | } 370 | 371 | /** 372 | * [setTo sets the scroller to the target] 373 | * @param {Number} target target relative to the scroller space (usually pixels, but can be angle) 374 | */ 375 | setTo(target) { 376 | //stop a tween if it is currently happening 377 | this.scrollObject[this.o.direction] = target; 378 | this.tweenScroll.stop(); 379 | 380 | this.handleUpdate(); 381 | this.handleComplete(); 382 | } 383 | 384 | handleUpdate() { 385 | if (!this.enabled) return; 386 | if (this.o.infinite) { 387 | this.dispatchValues.total = Phaser.Math.wrap( 388 | this.scrollObject[this.o.direction], 389 | this.min, 390 | this.max 391 | ); 392 | } else { 393 | this.dispatchValues.total = this.scrollObject[this.o.direction]; 394 | } 395 | 396 | let step = this.dispatchValues.total - this.previousTotal; 397 | if (step < -this.length / 2) { 398 | step = step + this.length; 399 | } else if (step > this.length / 2) { 400 | step = step - this.length; 401 | } 402 | 403 | this.dispatchValues.step = step; 404 | this.dispatchValues.percent = MathUtils.percentageBetween2( 405 | this.dispatchValues.total, 406 | this.o.from, 407 | this.o.to 408 | ); 409 | this.events.onUpdate.dispatch(this.dispatchValues); 410 | 411 | this.previousTotal = this.dispatchValues.total; 412 | } 413 | 414 | handleComplete() { 415 | if (!this.enabled) return; 416 | this.isScrolling = false; 417 | // reset multiplier when finished 418 | this.o.multiplier = 1; 419 | this.events.onComplete.dispatch(); 420 | } 421 | 422 | _updateMinMax() { 423 | this.min = Math.min(this.o.from, this.o.to); 424 | this.max = Math.max(this.o.from, this.o.to); 425 | this.length = Math.abs(this.max - this.min); 426 | this.previousTotal = this.o.from; 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /src/scroller_event_dispatcher.js: -------------------------------------------------------------------------------- 1 | import { findChild, detectDrag, dispatchClicks } from './util'; 2 | import Config from './config'; 3 | 4 | const defaultOptions = { 5 | direction: 'auto', 6 | autoDetectThreshold: Config.AUTO_DETECT_THRESHOLD 7 | }; 8 | 9 | // Scroller Event Dispatcher is a centralized place to listener for events useful for scrollers 10 | // The main feature of this class is the 'auto detect' for x and y directions. 11 | // If you set 'direction' to 'auto', events won't dispatch until a direction is detected. 12 | // 13 | export default class ScrollerEventDispatcher { 14 | constructor(game, clickObject, options = {}) { 15 | this.game = game; 16 | this.clickObject = clickObject; 17 | this.clickables = []; 18 | 19 | this.o = this.options = Object.assign({}, defaultOptions, options); 20 | 21 | this.addListeners(); 22 | } 23 | 24 | addListeners() { 25 | this.events = { 26 | onInputUp: new Phaser.Signal(), 27 | onInputDown: new Phaser.Signal(), 28 | onInputMove: new Phaser.Signal(), 29 | onDirectionSet: new Phaser.Signal() 30 | }; 31 | 32 | this.clickObject.inputEnabled = true; 33 | this.enable(); 34 | this.clickObject.events.onInputDown.add(this.handleDown, this); 35 | this.clickObject.events.onInputUp.add(this.handleUp, this); 36 | } 37 | 38 | removeListeners() { 39 | this.clickObject.events.onInputDown.remove(this.handleDown, this); 40 | this.clickObject.events.onInputUp.remove(this.handleUp, this); 41 | 42 | for (var property in this.events) { 43 | if (this.events.hasOwnProperty(property)) { 44 | this.events[property].removeAll(); 45 | } 46 | } 47 | } 48 | 49 | destroy() { 50 | this.removeListeners(); 51 | } 52 | 53 | enable() { 54 | this.enabled = true; 55 | } 56 | 57 | disable() { 58 | this.enabled = false; 59 | } 60 | 61 | setDirection(direction) { 62 | this.direction = direction; 63 | this.events.onDirectionSet.dispatch(direction); 64 | } 65 | 66 | /** 67 | * [registerClickables] 68 | * If a standard click occurs on the dispatcher surface we want to handle the click. 69 | * @param {Array[DisplayObjects]} clickables - Check these clickables AND their children for standard phaser input events. 70 | * e.g. displayObject.events.onInputUp 71 | */ 72 | registerClickables(clickables) { 73 | this.clickables = clickables; 74 | } 75 | 76 | dispatchClicks(pointer, clickables, type) { 77 | const found = dispatchClicks(pointer, clickables, type); 78 | if (type == 'onInputDown') { 79 | this.currentDown = found; 80 | } 81 | return found; 82 | } 83 | 84 | handleDown(target, pointer) { 85 | if (!this.enabled) { 86 | this.clickBlocked = true; 87 | return; 88 | } 89 | this.clickBlocked = false; 90 | 91 | if (this.o.direction == 'auto') { 92 | this.direction = null; 93 | this.old = null; 94 | } else { 95 | this.setDirection(this.o.direction); 96 | this.old = this.down = pointer[this.direction]; 97 | } 98 | 99 | this.game.input.addMoveCallback(this.handleMove, this); 100 | 101 | this.dispatchClicks(pointer, this.clickables, 'onInputDown'); 102 | this.events.onInputDown.dispatch(target, pointer, (clickables, type) => { 103 | return this.dispatchClicks(pointer, clickables, 'onInputDown'); 104 | }); 105 | } 106 | 107 | handleMove(pointer, x, y) { 108 | if (!this.enabled) return; 109 | 110 | if (!this.direction && this.o.direction == 'auto') { 111 | const xDist = Math.abs(pointer.positionDown.x - x); 112 | const yDist = Math.abs(pointer.positionDown.y - y); 113 | if ( 114 | xDist > this.o.autoDetectThreshold || 115 | yDist > this.o.autoDetectThreshold 116 | ) { 117 | this._cancelCurrentDown(pointer); 118 | const direction = xDist > yDist ? 'x' : 'y'; 119 | this.setDirection(direction); 120 | } else { 121 | return; 122 | } 123 | } 124 | 125 | this.events.onInputMove.dispatch(pointer, x, y); 126 | } 127 | 128 | handleUp(target, pointer) { 129 | if (!this.enabled || this.clickBlocked) return; 130 | this.game.input.deleteMoveCallback(this.handleMove, this); 131 | this.dispatchClicks(pointer, this.clickables, 'onInputUp'); 132 | this.events.onInputUp.dispatch(target, pointer, (clickables, type) => { 133 | return this.dispatchClicks(pointer, clickables, 'onInputUp'); 134 | }); 135 | this.currentDown = null; 136 | } 137 | 138 | _cancelCurrentDown(pointer) { 139 | if ( 140 | this.currentDown && 141 | this.currentDown.events && 142 | this.currentDown.events.onInputUp 143 | ) { 144 | this.currentDown.events.onInputUp.dispatch( 145 | this.currentDown, 146 | pointer, 147 | false 148 | ); 149 | } 150 | this.currentDown = null; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/swipe_carousel.js: -------------------------------------------------------------------------------- 1 | import ListView from './list_view'; 2 | 3 | const defaultOptions = { 4 | direction: 'x', 5 | autocull: true, 6 | momentum: false, 7 | bouncing: true, 8 | snapping: true, 9 | overflow: 100, 10 | padding: 10, 11 | swipeEnabled: true, 12 | offset: { 13 | x: 100 14 | } 15 | }; 16 | 17 | export default class SwipeCarousel extends ListView { 18 | constructor(game, parent, bounds, options = {}) { 19 | super(game, parent, bounds, Object.assign({}, defaultOptions, options)); 20 | 21 | this.scroller.options.snapStep = bounds.width + this.o.padding; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | import Config from './config'; 2 | 3 | export function parseBounds(bounds) { 4 | bounds.x = bounds.x ? bounds.x : 0; 5 | bounds.y = bounds.y ? bounds.y : 0; 6 | if (bounds.width <= 0) { 7 | console.warn('PhaserListView: bounds.width <= 0'); 8 | } else if (bounds.height <= 0) { 9 | console.warn('PhaserListView: bounds.height <= 0'); 10 | } 11 | return bounds; 12 | } 13 | 14 | // prefer nominalWidth and nominalHeight 15 | export function getWidthOrHeight(displayObject, widthOrHeight) { 16 | return ( 17 | displayObject[`nominal${capitalizeFirstLetter(widthOrHeight)}`] || 18 | displayObject[widthOrHeight] 19 | ); 20 | } 21 | 22 | export function capitalizeFirstLetter(string) { 23 | return string.charAt(0).toUpperCase() + string.slice(1); 24 | } 25 | 26 | export function findChild(children, predicate, scope = null) { 27 | if (!children) return false; 28 | for (let i = 0; i < children.length; i++) { 29 | const child = children[i]; 30 | if (!child) continue; 31 | if (predicate.call(scope, child)) { 32 | return child; 33 | } 34 | const found = findChild(child.children, predicate, scope); 35 | if (found) { 36 | return found; 37 | } 38 | } 39 | return false; 40 | } 41 | 42 | export function detectDrag(pointer) { 43 | const distanceX = Math.abs(pointer.positionDown.x - pointer.positionUp.x); 44 | const distanceY = Math.abs(pointer.positionDown.y - pointer.positionUp.y); 45 | const time = pointer.timeUp - pointer.timeDown; 46 | return ( 47 | distanceX > Config.AUTO_DETECT_THRESHOLD || 48 | distanceY > Config.AUTO_DETECT_THRESHOLD 49 | ); 50 | } 51 | 52 | export function dispatchClicks(pointer, clickables, type) { 53 | if (type == 'onInputUp' && detectDrag(pointer)) return; 54 | // SEARCH OBJECT UNDER POINT AS THERE IS NO CLICK PROPAGATION SUPPORT IN PHASER 55 | const found = findChild(clickables, clickable => { 56 | const pt = clickable.worldPosition; 57 | const { anchor, pivot, width, height, scale } = clickable; 58 | const x = pt.x - (anchor ? anchor.x * width : 0) - pivot.x * scale.x; 59 | const y = pt.y - (anchor ? anchor.y * height : 0) - pivot.y * scale.y; 60 | // console.log('does ', x, y, clickable.width, clickable.height, ' intersect ', pointer.x, pointer.y) 61 | return ( 62 | clickable.inputEnabled && 63 | new Phaser.Rectangle(x, y, clickable.width, clickable.height).contains( 64 | pointer.x, 65 | pointer.y 66 | ) 67 | ); 68 | }); 69 | if ( 70 | found && 71 | found.events && 72 | found.events[type] && 73 | found.events[type].dispatch 74 | ) { 75 | found.events[type].dispatch(found, pointer, true); 76 | } 77 | return found; 78 | } 79 | -------------------------------------------------------------------------------- /src/utils/math_utils.js: -------------------------------------------------------------------------------- 1 | var MathUtils = { 2 | nearestMultiple: function(n, multiple) { 3 | return Math.round(n / multiple) * multiple; 4 | }, 5 | 6 | scaleBetween: function(lo, hi, scale) { 7 | return lo + (hi - lo) * scale; 8 | }, 9 | 10 | // returns a percentage between hi and lo from a given input 11 | // e.g percentageBetween2(7, 4, 10) -> .5 12 | percentageBetween2: function(input, lo, hi) { 13 | return (input - lo) / (hi - lo); 14 | } 15 | }; 16 | 17 | export default MathUtils; 18 | -------------------------------------------------------------------------------- /src/wheel_scroller.js: -------------------------------------------------------------------------------- 1 | import MathUtils from './utils/math_utils'; 2 | import Scroller from './scroller'; 3 | 4 | const { radToDeg, degToRad } = Phaser.Math; 5 | const _ptHelper = new Phaser.Point(); 6 | 7 | const defaultOptions = { 8 | direction: 'angle', 9 | infinite: false, 10 | speedLimit: 1.5 11 | }; 12 | 13 | export default class WheelScroller extends Scroller { 14 | constructor(game, clickObject, options = {}) { 15 | super( 16 | game, 17 | clickObject, 18 | { angle: clickObject.width / 2 }, 19 | Object.assign({}, defaultOptions, options) 20 | ); 21 | } 22 | 23 | // extends Scroller.handleDown 24 | handleDown(target, pointer) { 25 | if (!this.enabled) return; 26 | this.centerPoint = this.clickObject.toGlobal(new Phaser.Point(0, 0)); 27 | _ptHelper.set(pointer.x, pointer.y); 28 | this.old = this.down = Phaser.Math.normalizeAngle( 29 | Phaser.Math.angleBetweenPoints(_ptHelper, this.centerPoint) 30 | ); 31 | this.fullDiff = 0; 32 | 33 | super.handleDown(target, pointer); 34 | } 35 | 36 | // overrides Scroller.handleMove 37 | handleMove(pointer, x, y) { 38 | if (!this.enabled) return; 39 | this.isScrolling = true; 40 | _ptHelper.set(x, y); 41 | let currentRotation = Phaser.Math.normalizeAngle( 42 | Phaser.Math.angleBetweenPoints(_ptHelper, this.centerPoint) 43 | ); 44 | let rotations = 0; 45 | 46 | let diffRotation = this.old - currentRotation; 47 | this.diff = radToDeg(diffRotation); 48 | 49 | if (this.diff > 180) { 50 | rotations = 1; 51 | } else if (this.diff < -180) { 52 | rotations = -1; 53 | } 54 | 55 | if (rotations != 0) { 56 | let fullCircle = rotations * degToRad(360); 57 | diffRotation -= fullCircle; 58 | this.diff = radToDeg(diffRotation); 59 | } 60 | 61 | this.diff = this._requestDiff( 62 | this.diff, 63 | this.target, 64 | this.min, 65 | this.max, 66 | this.o.overflow 67 | ); 68 | 69 | this.fullDiff -= this.diff; 70 | 71 | this.target -= this.diff; 72 | 73 | if (this.o.infinite) { 74 | this.target = this._wrapTarget(this.target, this.min, this.max); 75 | } 76 | 77 | this.old = currentRotation; 78 | 79 | //store timestamp for event 80 | this.o.time.move = this.game.time.time; 81 | 82 | let diameter = this.clickObject.width; 83 | let circumference = Math.PI * diameter; 84 | let sectorLength = circumference * (this.diff / 360); 85 | this.acc = Math.min(Math.abs(sectorLength / 30), this.o.maxAcceleration); 86 | 87 | //go ahead and move the block 88 | this.scrollObject[this.o.direction] = this.target; 89 | this.handleUpdate(); 90 | 91 | if (this.o.emitMoving) this.events.onInputMove.dispatch({ pointer, x, y }); 92 | } 93 | 94 | // extends Scroller.handleDown 95 | handleUp(target, pointer) { 96 | _ptHelper.set(pointer.x, pointer.y); 97 | this.current = Phaser.Math.normalizeAngle( 98 | Phaser.Math.angleBetweenPoints(_ptHelper, this.centerPoint) 99 | ); 100 | 101 | super.handleUp(target, pointer); 102 | } 103 | 104 | _wrapTarget(target, min, max) { 105 | let diff = 0; 106 | if (target > max) { 107 | diff = target - max; 108 | target = min + diff; 109 | } else if (target < min) { 110 | diff = min - target; 111 | target = max - diff; 112 | } 113 | return target; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: 0 */ 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | 5 | const isProd = process.env.NODE_ENV === 'production'; 6 | 7 | module.exports = { 8 | entry: [ 9 | 'babel-polyfill', 10 | path.resolve(__dirname, 'src/index.js'), 11 | ], 12 | output: { 13 | path: path.resolve(__dirname, 'dist'), 14 | filename: 'phaser-list-view.js', 15 | publicPath: './dist/', 16 | }, 17 | resolve: { 18 | extensions: ['', '.js'], 19 | root: __dirname, 20 | alias: { 21 | src: path.join(__dirname, 'src'), 22 | }, 23 | }, 24 | module: { 25 | loaders: [{ 26 | test: /\.js?$/, 27 | loader: 'babel', 28 | exclude: /node_modules/, 29 | include: path.resolve(__dirname, 'src'), 30 | }] 31 | }, 32 | plugins: [ 33 | new webpack.DefinePlugin({ 34 | 'process.env': { 35 | APP_ENV: JSON.stringify(process.env.APP_ENV), 36 | }, 37 | }), 38 | new webpack.ProvidePlugin({ 39 | Promise: 'imports?this=>global!exports?global.Promise!es6-promise', 40 | }), 41 | new webpack.optimize.OccurenceOrderPlugin(), 42 | ].concat(isProd ? [ 43 | new webpack.optimize.DedupePlugin(), 44 | new webpack.optimize.UglifyJsPlugin({ 45 | minimize: true, 46 | compress: { 47 | warnings: false, 48 | }, 49 | }), 50 | ] : []) 51 | }; 52 | -------------------------------------------------------------------------------- /webpack.demos.config.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: 0 */ 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | const BrowserSyncPlugin = require('browser-sync-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: [ 8 | path.resolve(__dirname, 'src/example/index.js'), 9 | ], 10 | output: { 11 | path: path.resolve(__dirname, 'dist'), 12 | publicPath: './dist/', 13 | filename: 'example_app.js', 14 | }, 15 | watch: true, 16 | module: { 17 | loaders: [{ 18 | test: /\.js?$/, 19 | loader: 'babel', 20 | exclude: /node_modules/, 21 | include: path.resolve(__dirname, 'src'), 22 | }] 23 | }, 24 | plugins: [ 25 | new webpack.DefinePlugin({ 26 | 'process.env': { 27 | APP_ENV: JSON.stringify(process.env.APP_ENV), 28 | }, 29 | }), 30 | new BrowserSyncPlugin({ 31 | host: process.env.IP || 'localhost', 32 | port: process.env.PORT || 3001, 33 | server: { 34 | baseDir: ['./', './build'] 35 | } 36 | }) 37 | ] 38 | } 39 | --------------------------------------------------------------------------------