├── .babelrc ├── .gitignore ├── src ├── phasercomps.js ├── components │ ├── UIComponents.js │ ├── UIListBaseItem.js │ ├── UIButtonSelect.js │ ├── UIList.js │ ├── UIScrollPanel.js │ ├── UIContainer.js │ ├── UIButtonRadio.js │ ├── UIButtonDraggable.js │ ├── UIProgressBar.js │ ├── UIButton.js │ ├── UIScrollBar.js │ └── UIComponentPrototype.js ├── plugin │ └── Plugin.js ├── manager │ └── UIManager.js └── clip │ └── ComponentClip.js ├── webpack.config.js ├── LICENSE ├── package.json ├── README.md ├── jsfl └── ExportToPhaser.jsfl └── dist └── phaser-ui-comps.min.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules/ 3 | *.iml 4 | -------------------------------------------------------------------------------- /src/phasercomps.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @namespace PhaserComps 3 | */ 4 | 5 | import ComponentClip from "./clip/ComponentClip"; 6 | import UIComponents from "./components/UIComponents"; 7 | import Plugin from "./plugin/Plugin"; 8 | import UIManager from "./manager/UIManager"; 9 | 10 | const PhaserComps = { 11 | ComponentClip: ComponentClip, 12 | UIComponents: UIComponents, 13 | Plugin: Plugin, 14 | UIManager: UIManager 15 | }; 16 | 17 | export default PhaserComps; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const main = "./src/phasercomps.js"; 3 | const sourcePaths = [main]; 4 | 5 | module.exports = { 6 | entry: sourcePaths, 7 | 8 | mode: "production", 9 | //mode: "development", 10 | 11 | output: { 12 | path: path.resolve(__dirname, "dist"), 13 | filename: "phaser-ui-comps.min.js", 14 | libraryTarget: "umd", 15 | library: "PhaserComps", 16 | sourceMapFilename: "phaser-ui-comps.min.js.map" 17 | }, 18 | 19 | externals: { 20 | phaser: { 21 | umd: "phaser", 22 | commonjs2: "phaser", 23 | commonjs: "phaser", 24 | amd: "phaser", 25 | // indicates global variable should be used 26 | root: "Phaser" 27 | } 28 | }, 29 | 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.js$/, 34 | loader: "babel-loader", 35 | exclude: [ 36 | /node_modules/, 37 | ], 38 | include: path.join(__dirname, 'src/') 39 | }, 40 | ], 41 | }, 42 | 43 | devtool: "source-map" 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/UIComponents.js: -------------------------------------------------------------------------------- 1 | import UIButton from "./UIButton"; 2 | import UIButtonRadio from "./UIButtonRadio"; 3 | import UIComponentPrototype from "./UIComponentPrototype"; 4 | import UIButtonSelect from "./UIButtonSelect"; 5 | import UIButtonDraggable from "./UIButtonDraggable"; 6 | import UIScrollBar from "./UIScrollBar"; 7 | import UIScrollPanel from "./UIScrollPanel"; 8 | import UIProgressBar from "./UIProgressBar"; 9 | import UIContainer from "./UIContainer"; 10 | import UIList from "./UIList"; 11 | import UIListBaseItem from "./UIListBaseItem"; 12 | 13 | /** 14 | * @namespace PhaserComps.UIComponents 15 | */ 16 | 17 | const UIComponents = { 18 | UIComponentPrototype: UIComponentPrototype, 19 | UIButton: UIButton, 20 | UIButtonSelect: UIButtonSelect, 21 | UIButtonRadio: UIButtonRadio, 22 | UIButtonDraggable: UIButtonDraggable, 23 | UIScrollBar: UIScrollBar, 24 | UIScrollPanel: UIScrollPanel, 25 | UIProgressBar: UIProgressBar, 26 | UIContainer: UIContainer, 27 | UIList: UIList, 28 | UIListBaseItem: UIListBaseItem 29 | }; 30 | 31 | export default UIComponents; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Anton Bystrov 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 | -------------------------------------------------------------------------------- /src/components/UIListBaseItem.js: -------------------------------------------------------------------------------- 1 | import UIComponentPrototype from "./UIComponentPrototype"; 2 | import UIList from "./UIList"; 3 | 4 | 5 | /** 6 | * @class UIListBaseItem 7 | * @memberOf PhaserComps.UIComponents 8 | * @classdesc 9 | * 10 | * Base class for UIList component renderer. Extend it to create custom list items renderers. 11 | * 12 | * @emits PhaserComps.UIComponents.UIList.EVENT_ITEM_CHANGE 13 | * @property {*} data any data from UIList data list 14 | * 15 | * @param {PhaserComps.UIComponents.UIComponentPrototype} [parent] UIComponentPrototype instance to find clip inside 16 | * @param {String} [key] key to find clip inside parent 17 | * @param {Class} rendererClass class for items, best if extending `UIListBaseItem` 18 | */ 19 | 20 | export default class UIListBaseItem extends UIComponentPrototype { 21 | constructor(parent, key) { 22 | super(parent, key); 23 | this._data = null; 24 | } 25 | 26 | /** 27 | * @method PhaserComps.UIComponents.UIListBaseItem#notifyChange 28 | * @desc Emits change event to containing UIList instance 29 | */ 30 | notifyChange() { 31 | this.emit(UIList.EVENT_ITEM_CHANGE, this); 32 | } 33 | 34 | /** 35 | * @method PhaserComps.UIComponents.UIListBaseItem#_commitData 36 | * @protected 37 | * @desc apply data from setter, override it 38 | */ 39 | _commitData() { 40 | // override 41 | } 42 | 43 | get data() { 44 | return this._data; 45 | } 46 | 47 | set data(value) { 48 | this._data = value; 49 | this._commitData(); 50 | } 51 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phaser-ui-comps", 3 | "version": "1.1.2", 4 | "description": "Phaser 3 UI Components Framework and JSFL builder for Adobe Animate", 5 | "main": "src/phasercomps.js", 6 | "scripts": { 7 | "build": "webpack", 8 | "analyze": "source-map-explorer 'dist/*.js'" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/xense/phaser-ui-comps.git" 13 | }, 14 | "keywords": [ 15 | "phaser", 16 | "phaser3", 17 | "ui", 18 | "user interface", 19 | "jsfl", 20 | "animate", 21 | "components", 22 | "fla", 23 | "xfl", 24 | "buttons", 25 | "progress bar", 26 | "scroll bar" 27 | ], 28 | "author": "Anton 'Xense' Bystrov ", 29 | "license": "MIT", 30 | "licenseUrl": "http://www.opensource.org/licenses/mit-license.php", 31 | "bugs": { 32 | "url": "https://github.com/xense/phaser-ui-comps/issues" 33 | }, 34 | "homepage": "https://xense.github.io/phaser-ui-comps-docs/", 35 | "devDependencies": { 36 | "babel-core": "^6.26.3", 37 | "babel-loader": "^7.1.5", 38 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 39 | "babel-polyfill": "^6.26.0", 40 | "babel-preset-env": "^1.7.0", 41 | "source-map-explorer": "^2.4.2", 42 | "uglifyjs-webpack-plugin": "^2.2.0", 43 | "webpack": "^4.44.0", 44 | "webpack-cli": "^3.3.12", 45 | "webpack-dev-server": "^3.11.0", 46 | "phaser": "^3.21.0" 47 | }, 48 | "dependencies": {}, 49 | "deprecated": false 50 | } 51 | -------------------------------------------------------------------------------- /src/components/UIButtonSelect.js: -------------------------------------------------------------------------------- 1 | import UIButton from "./UIButton"; 2 | 3 | /** 4 | * @class UIButtonSelect 5 | * @memberOf PhaserComps.UIComponents 6 | * @classdesc 7 | * Checkbox-like button component prototype. 8 | * Has states `up`, `over`, `down`, `disable`, `up_select`, `over_select`, `down_select`, `disable_select`, 9 | * Emits EVENT_CLICK on click. 10 | * When disabled, doesn't interact to mouse events and move to state `disable` 11 | * @extends PhaserComps.UIComponents.UIButton 12 | * @emits PhaserComps.UIComponents.UIButton.EVENT_CLICK 13 | * 14 | * @property {Boolean} enable activate/deactivate button interaction. if false, button state is set to `disable` 15 | * @property {String} label get/set button label text 16 | * @property {Boolean} select get/set switch 17 | * 18 | * @param {PhaserComps.UIComponents.UIComponentPrototype} [parent] UIComponentPrototype instance to find clip inside 19 | * @param {String} [key] key to find clip inside parent 20 | * @param {String} [labelText] text to set for a 'label' key 21 | */ 22 | export default class UIButtonSelect extends UIButton { 23 | 24 | constructor(parent, key, labelText){ 25 | super(parent, key, labelText); 26 | 27 | /** 28 | * button as selected or not 29 | * @type {Boolean} 30 | * @private 31 | */ 32 | this._select = false; 33 | } 34 | 35 | /** 36 | * @method PhaserComps.UIComponents.UIButtonSelect#getStateId 37 | * @inheritDoc 38 | * @returns {String} 39 | */ 40 | getStateId() { 41 | return super.getStateId() + (this._select ? "_select" : ""); 42 | } 43 | 44 | /** 45 | * @method PhaserComps.UIComponents.UIButtonSelect#_onClick 46 | * @inheritDoc 47 | */ 48 | _onClick() { 49 | this._select = !this._select; 50 | this.doState(); 51 | super._onClick(); 52 | } 53 | 54 | get select() { return this._select; } 55 | set select(value) { 56 | if (this._select === value) { 57 | return; 58 | } 59 | this._select = value; 60 | this.doState(); 61 | } 62 | } -------------------------------------------------------------------------------- /src/plugin/Plugin.js: -------------------------------------------------------------------------------- 1 | import Phaser from "phaser"; 2 | import ComponentClip from "../clip/ComponentClip"; 3 | 4 | /** 5 | * @class Plugin 6 | * @memberOf PhaserComps 7 | * @classdesc 8 | * Phaser 3 plugin, adds `ui_component` method to scene GameObjectFactory and GameObjectCreator, 9 | * that creates a {@link PhaserComps.ComponentClip} instance 10 | * 11 | * *Note. Factory method (`scene.make.ui_component()`) also adds clip instance to scene* 12 | * 13 | * Implementation example: 14 | * ```javascript 15 | * import "phaser-ui-comps" 16 | * 17 | * var config = { 18 | * type: Phaser.AUTO, 19 | * parent: "example", 20 | * width: 800, 21 | * height: 600, 22 | * scene: { 23 | * create: create 24 | * }, 25 | * plugins: { 26 | * global: [ 27 | * PhaserComps.Plugin.DefaultCfg 28 | * ] 29 | * } 30 | * var game = new Phaser.Game(config); 31 | * 32 | * create() { 33 | * let configObject = {}; // here must be real jsfl-generated config object 34 | * let texture_name = "your_texture_name"; 35 | * this.add.ui_component(configObject, [texture_name]); 36 | * } 37 | * ``` 38 | * @see PhaserComps.ComponentClip 39 | * 40 | */ 41 | export default class Plugin extends Phaser.Plugins.BasePlugin { 42 | constructor(mgr) { 43 | super(mgr); 44 | mgr.registerGameObject("ui_component", this.addComponent, this.makeComponent); 45 | } 46 | 47 | addComponent(config, textures) { 48 | let clip = new ComponentClip(this.scene, config, textures); 49 | this.scene.add.existing(clip); 50 | return clip; 51 | } 52 | 53 | makeComponent(config, textures) { 54 | return new ComponentClip(this.scene, config, textures); 55 | } 56 | } 57 | 58 | const DefaultCfg = { 59 | key: "UIComponents", 60 | plugin: Plugin, 61 | start: true 62 | }; 63 | 64 | /** 65 | * Default plugin config 66 | * 67 | * @const PhaserComps.Plugin.DefaultCfg 68 | * @memberOf PhaserComps.Plugin 69 | * @type PluginObjectItem 70 | */ 71 | Plugin.DefaultCfg = DefaultCfg; -------------------------------------------------------------------------------- /src/manager/UIManager.js: -------------------------------------------------------------------------------- 1 | let isLock = false; 2 | const enabledIds = []; 3 | const registeredComps = {}; 4 | 5 | /** 6 | * @namespace PhaserComps.UIManager 7 | * @memberOf PhaserComps 8 | * @classdesc Allows to lock all ui, except for provided lock ids. 9 | * For this, you must set `lockId` property to UIComponentPrototype instances you want to enable, 10 | * and then switch theirs availability by UIManager's 11 | * {@link lock} and {@link unlock} methods 12 | * 13 | * For example, locked UIButton will still interact to mouse events, but will not emit click event. 14 | * 15 | * This can be useful in game tutorials. 16 | */ 17 | export default class UIManager { 18 | 19 | /** 20 | * @memberOf PhaserComps.UIManager 21 | * @description Makes only components with provided ids list (or one id string) to emit UI events 22 | * 23 | * @param {String | Array} id component's lock id, or Array of lock ids to be only enabled 24 | * @param {boolean} [rewrite=true] rewrite current list if true, otherwise add to list 25 | */ 26 | static lock(id, rewrite = true) { 27 | if (rewrite) { 28 | this.unlock(); 29 | } 30 | if (typeof id === "string") { 31 | enabledIds.push(id); 32 | } else { 33 | id.forEach(value => enabledIds.push(value)); 34 | } 35 | isLock = true; 36 | } 37 | 38 | /** 39 | * @memberOf PhaserComps.UIManager 40 | * @description Releases all components 41 | */ 42 | static unlock() { 43 | enabledIds.length = 0; 44 | isLock = false; 45 | } 46 | 47 | /** @param {UIComponentPrototype} proto */ 48 | static register(proto) { 49 | registeredComps[proto.lockId] = proto; 50 | } 51 | 52 | /** @param {UIComponentPrototype} proto */ 53 | static unregister(proto) { 54 | if (registeredComps[proto.lockId]) { 55 | registeredComps[proto.lockId] = null; 56 | delete registeredComps[proto.lockId]; 57 | } 58 | } 59 | 60 | /** 61 | * @memberOf PhaserComps.UIManager 62 | * @description called from component to check, if it's allowed to emit UI event. 63 | * @param {String} id 64 | */ 65 | static check(id) { 66 | if (!isLock) { 67 | return true; 68 | } 69 | return enabledIds.indexOf(id) !== -1; 70 | } 71 | 72 | /** 73 | * @param {string} id 74 | * @return {PhaserComps.UIComponents.UIComponentPrototype} 75 | */ 76 | static getById(id) { 77 | return registeredComps[id]; 78 | } 79 | 80 | /** 81 | * @param {string} id 82 | * @returns {Phaser.Geom.Rectangle} 83 | */ 84 | static getBoundsById(id) { 85 | const proto = this.getById(id); 86 | return proto ? proto.lockClipBounds : null; 87 | } 88 | 89 | /** 90 | * @param {string} id 91 | * @returns {Phaser.GameObjects.GameObject|*} 92 | */ 93 | static getClipById(id) { 94 | const proto = this.getById(id); 95 | return proto ? proto.lockClip : null; 96 | } 97 | 98 | } -------------------------------------------------------------------------------- /src/components/UIList.js: -------------------------------------------------------------------------------- 1 | import UIComponentPrototype from "./UIComponentPrototype"; 2 | 3 | const EVENT_ITEM_CHANGE = "event_change"; 4 | 5 | /** 6 | * @class UIList 7 | * @memberOf PhaserComps.UIComponents 8 | * @classdesc 9 | * 10 | * List component. Item clip instances are supposed to exist as it's children, with keys `item_0`, `item_1` and so on. 11 | * 12 | * When data array applied, every array item is applied to its' view instance 13 | * 14 | * Useful for short lists, and for lists with custom items layout. 15 | * 16 | * @emits PhaserComps.UIComponents.UIList.EVENT_ITEM_CHANGE 17 | * @property {Array<*>} data any data array to apply to list items 18 | * 19 | * @param {PhaserComps.UIComponents.UIComponentPrototype} [parent] UIComponentPrototype instance to find clip inside 20 | * @param {String} [key] key to find clip inside parent 21 | * @param {Class} rendererClass class for items, best if extending `UIListBaseItem` 22 | */ 23 | 24 | 25 | export default class UIList extends UIComponentPrototype { 26 | 27 | /** 28 | * @event PhaserComps.UIComponents.UIList.EVENT_ITEM_CHANGE 29 | * @memberOf PhaserComps.UIComponents.UIList 30 | * @description 31 | * Emitted when any item emits such even. 32 | * @param {PhaserComps.UIComponents.UIListBaseItem} item item instance, that emitted change event 33 | */ 34 | static get EVENT_ITEM_CHANGE() { return EVENT_ITEM_CHANGE; } 35 | 36 | constructor(parent, key, rendererClass) { 37 | super(parent, key); 38 | this._rendererClass = rendererClass; 39 | this._items = []; 40 | } 41 | 42 | /**@return {*[]}*/ 43 | get data() { 44 | return this._data; 45 | } 46 | 47 | /** @param {*[]} value */ 48 | set data(value) { 49 | this._data = value; 50 | this._updateData(); 51 | } 52 | 53 | /** 54 | * @method PhaserComps.UIComponents.UIList#clean 55 | * @desc Destroy all items renderer instances 56 | */ 57 | clean() { 58 | while(this._items.length !== 0) { 59 | let item = this._items.pop(); 60 | item.destroy(true); 61 | } 62 | } 63 | 64 | _updateData() { 65 | const len = this._data.length; 66 | for (let index = 0; index < len; index++) { 67 | let dataItem = this._data[index]; 68 | let item = this._getRenderer(index); 69 | item.data = dataItem; 70 | } 71 | this.doState(); 72 | } 73 | 74 | _getRenderer(index) { 75 | if (this._items.length - 1 < index) { 76 | let renderer = new this._rendererClass(this, "item_" + index); 77 | this._items[index] = renderer; 78 | renderer.on(UIList.EVENT_ITEM_CHANGE, this.onItemChange, this); 79 | } 80 | return this._items[index]; 81 | } 82 | 83 | /** 84 | * @method PhaserComps.UIComponents.UIList#getStateId 85 | * @inheritDoc 86 | * @returns {String} 87 | */ 88 | getStateId() { 89 | return "count_" + (this._data ? this._data.length : "0"); 90 | } 91 | 92 | /** 93 | * @method PhaserComps.UIComponents.UIList#destroy 94 | * @protected 95 | * @inheritDoc 96 | */ 97 | destroy(fromScene) { 98 | this.clean(); 99 | super.destroy(fromScene); 100 | } 101 | 102 | onItemChange(item) { 103 | this.emit(UIList.EVENT_ITEM_CHANGE, item); 104 | } 105 | } -------------------------------------------------------------------------------- /src/components/UIScrollPanel.js: -------------------------------------------------------------------------------- 1 | import UIComponentPrototype from "./UIComponentPrototype"; 2 | import UIScrollBar from "./UIScrollBar"; 3 | 4 | /** 5 | * @typedef ScrollBoundsObject 6 | * @description 7 | * Object is generated automatically on container's clip update, by the `dimensions` clip 8 | * @memberOf PhaserComps.UIComponents.UIScrollPanel 9 | * @property {Number} x Start x position of the container. Used, if panel is horizontal 10 | * @property {Number} y Start x position of the container. Used, if panel is vertical 11 | * @property {Number} len Scroll distance of the container. On scroll down/right, 12 | * x or y position will be subtracted by `len` multiplied by scrollbar value 13 | */ 14 | 15 | 16 | /** 17 | * @class UIScrollPanel 18 | * @memberOf PhaserComps.UIComponents 19 | * @classdesc 20 | * Scrolling panel with scrollbar applied to it. 21 | * First parameter is container, where this should find the panel, scroll bar and dimensions instances 22 | * UIScrollBar instance created inside with a provided `scrollBarKey`

23 | * 24 | * **Warning! This component doesn't extend UIComponent.ComponentPrototype** 25 | * 26 | * @param {PhaserComps.UIComponents.UIComponentPrototype} container 27 | * @param {String} panelKey 28 | * @param {String} scrollBarKey 29 | * @param {String} dimensionsKey 30 | * @param {Boolean} [vertical=false] 31 | */ 32 | 33 | export default class UIScrollPanel { 34 | 35 | constructor(container, panelKey, scrollBarKey, dimensionsKey, vertical) { 36 | /** @type PhaserComps.UIComponents.UIComponentPrototype */ 37 | this._container = container; 38 | container.on(UIComponentPrototype.EVENT_STATE, this._onContainerUpdate, this); 39 | 40 | /** @type String */ 41 | this._panelKey = panelKey; 42 | /** @type String */ 43 | this._dimensionsKey = dimensionsKey; 44 | 45 | /** @type PhaserComps.UIComponents.UIScrollBar */ 46 | this._scrollBar = new UIScrollBar(container, scrollBarKey, vertical); 47 | this._scrollBar.on(UIScrollBar.EVENT_CHANGE, this._onScrollBar, this); 48 | 49 | /** @type Boolean */ 50 | this._vertical = vertical || false; 51 | } 52 | 53 | /** 54 | * update clip instances on container update 55 | * @method PhaserComps.UIComponents.UIScrollPanel#_onContainerUpdate 56 | * @private 57 | */ 58 | _onContainerUpdate() { 59 | /** @type PhaserComps.ComponentClip */ 60 | let clip = this._container._clip; 61 | if (!clip) { 62 | return; 63 | } 64 | /** @type PhaserComps.ComponentClip */ 65 | this._panel = clip.getChildClip(this._panelKey); 66 | let dims = clip.getChildClip(this._dimensionsKey); 67 | if (!this._panel || !dims) { 68 | return; 69 | } 70 | /** 71 | * 72 | * @type ScrollBoundsObject 73 | */ 74 | this._scrollBounds = { 75 | x: dims.x, 76 | y: dims.y, 77 | len: this._vertical ? this._panel.height - dims.height : this._panel.width - dims.width 78 | }; 79 | 80 | // update current panel position 81 | this._onScrollBar(this._onScrollBar.value); 82 | } 83 | 84 | /** 85 | * Update panel position on scrollbar change 86 | * @method PhaserComps.UIComponents.UIScrollPanel#_onScrollBar 87 | * @private 88 | */ 89 | _onScrollBar(value) { 90 | if (!this._panel || !this._scrollBounds) { 91 | return; 92 | } 93 | if (this._vertical) { 94 | this._panel.y = this._scrollBounds.y - this._scrollBounds.len * value; 95 | } else { 96 | this._panel.x = this._scrollBounds.x - this._scrollBounds.len * value; 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /src/components/UIContainer.js: -------------------------------------------------------------------------------- 1 | import UIComponentPrototype from "./UIComponentPrototype"; 2 | 3 | /** 4 | * @memberOf PhaserComps.UIComponents 5 | * @class UIContainer 6 | * @classdesc 7 | * Base container component. Allows to add dynamically created UIComponents inside other components. 8 | * 9 | * Child components clips must be root components only, not any clip's children. 10 | * 11 | * Note, that UIContainer can be only a child component of another UIComponentPrototype instance. 12 | * 13 | * @extends PhaserComps.UIComponents.UIComponentPrototype 14 | * @param {PhaserComps.UIComponents.UIComponentPrototype} parent UIComponentPrototype instance to find clip inside 15 | * @param {String} key key to find clip inside parent 16 | */ 17 | 18 | export default class UIContainer extends UIComponentPrototype { 19 | 20 | constructor(parent, key) { 21 | super(parent, key); 22 | 23 | /** 24 | * List of children UIComponentPrototypes added 25 | * @type {Array} 26 | * @private 27 | */ 28 | this._children = []; 29 | } 30 | 31 | /** 32 | * @method PhaserComps.UIComponents.UIContainer#addChild 33 | * @description 34 | * Adds child to children list, and adds it to Phaser container instance, if one exists 35 | * 36 | * @param {PhaserComps.UIComponents.UIComponentPrototype} child child component to add 37 | * @return {PhaserComps.UIComponents.UIComponentPrototype} child 38 | */ 39 | addChild(child) { 40 | if (this._children.indexOf(child) !== -1) { 41 | return child; // TODO move to top? 42 | } 43 | this._children.push(child); 44 | 45 | // add to container instance, or hide 46 | if (this._clip) { 47 | child._clip.visible = true; 48 | this._addUIComponentToContainerClip(child); 49 | } else { 50 | child._clip.visible = false; 51 | } 52 | return child; 53 | } 54 | 55 | /** 56 | * @method PhaserComps.UIComponents.UIContainer#removeChild 57 | * @description 58 | * Removes child from children list, and removes it from Phaser container instance, if one exists 59 | * 60 | * @param {PhaserComps.UIComponents.UIComponentPrototype} child child component to remove 61 | * @return {PhaserComps.UIComponents.UIComponentPrototype} returns child param 62 | */ 63 | removeChild(child) { 64 | let index = this._children.indexOf(child); 65 | if (index === -1) { 66 | return child; 67 | } 68 | this._children.splice(index, 1); 69 | 70 | if (this._clip) { 71 | this._removeUIComponentFromContainerClip(child); 72 | } 73 | return child; 74 | } 75 | 76 | /** 77 | * 78 | * @param {PhaserComps.UIComponents.UIComponentPrototype} child 79 | * @private 80 | */ 81 | _addUIComponentToContainerClip(child) { 82 | this._clip.add(child._clip); 83 | child._clip.visible = true; 84 | } 85 | 86 | /** 87 | * 88 | * @param {PhaserComps.UIComponents.UIComponentPrototype} child 89 | * @param {Boolean} [destroyChild=false] 90 | * @private 91 | */ 92 | _removeUIComponentFromContainerClip(child, destroyChild) { 93 | this._clip.remove(child._clip, destroyChild); 94 | child._clip.visible = false; 95 | } 96 | 97 | onClipAppend(clip) { 98 | super.onClipAppend(clip); 99 | if (clip) { 100 | for (let child of this._children) { 101 | this._addUIComponentToContainerClip(child); 102 | } 103 | } 104 | } 105 | 106 | onClipRemove(clip) { 107 | super.onClipRemove(clip); 108 | // hide and remove children from current container 109 | if (clip) { 110 | for (let child of this._children) { 111 | this._removeUIComponentFromContainerClip(child); 112 | } 113 | } 114 | } 115 | 116 | destroy() { 117 | // remove and destroy children 118 | for (let child of this._children) { 119 | if (this._clip) { // TODO check if needed 120 | this._removeUIComponentFromContainerClip(child); 121 | } 122 | child.destroy(); 123 | } 124 | this._children.length = 0; 125 | super.destroy(); 126 | } 127 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Phaser 3 UI Components, built by Adobe Animate 2 | ---- 3 |

What is it?

4 | Build your UI in [Abode Animate](https://www.adobe.com/ru/products/animate.html), 5 | export to JSON and bitmaps with provided 6 | [JSFL script](https://github.com/xense/phaser-ui-comps/blob/master/jsfl/ExportToPhaser.jsfl) 7 | , and you can forget about lots of positioning magic numbers in your code. 8 | 9 | `ComponentClip` build itself with provided JSON and atlases, 10 | and `UIComponentPrototype` Will help to control them, switch states, 11 | listen to click, drag and other events. 12 | 13 | In addition, `UIComponentPrototype` and it's children classes don't mind, 14 | if they have a real clip instance in current state or at all, 15 | so nothing bad happens, for example, if you remove some button instance in your window in 16 | Animate document and keep it's `UIComponentPrototype` instance. 17 | 18 | All bitmaps are exported to png files with the same folder structure 19 | as in the Animate document library. Pack them to atlases using 20 | [TexturePacker](https://www.codeandweb.com/texturepacker) or other tool you like. 21 | 22 |

Where and how to use?

23 | 24 | [Main framework repo](https://github.com/xense/phaser-ui-comps) 25 | 26 | [Docs, tutorials, examples](https://xense.github.io/phaser-ui-comps-docs) 27 | 28 | [Live example](https://xense.github.io/phaser-ui-comps-docs/tutorial-showcase.html) 29 | 30 | [Issues, bugs, new components ideas](https://github.com/xense/phaser-ui-comps/issues) 31 | 32 | [Animate document example](https://github.com/xense/phaser-ui-comps-docs/raw/master/examples/xfl/UI.fla) 33 | 34 |

Export Animate document

35 | To run JSFL script in Animate select `Commands > Run Command`, 36 | navigate to the script, and click Open. 37 | 38 |

How to install?

39 | 40 | To install the latest version from 41 | [npm](https://www.npmjs.com) 42 | locally and save it in your `package.json` file: 43 | ```bash 44 | npm install --save phaser-ui-comps 45 | ``` 46 | or if you are using [yarn](https://yarnpkg.com) 47 | ```bash 48 | yarn add phaser-ui-comps 49 | ``` 50 | 51 | Or you can download minified version from 52 | [https://github.com/xense/phaser-ui-comps/tree/master/dist](https://github.com/xense/phaser-ui-comps/tree/master/dist) 53 | 54 | Or use [jsdelivr cdn](https://www.jsdelivr.com/) version 55 | ```html 56 | 57 | ``` 58 | 59 | *Note!* 60 | *PhaserComps uses [underscore.js](https://underscorejs.org/) 61 | There are two builds in the /dist folder, 62 | [one](https://github.com/xense/phaser-ui-comps/blob/master/dist/phaser-ui-comps-with-underscore.min.js) 63 | with underscore included and 64 | [other](https://github.com/xense/phaser-ui-comps/blob/master/dist/phaser-ui-comps.min.js) 65 | without it, so you need to load it before loading PhaserComps* 66 | 67 |

Simple usage

68 | 69 | ```html 70 | 71 | 72 | ``` 73 | 74 | ```javascript 75 | const COMPONENT_CONFIG = "comp-config"; 76 | const TEXTURE_CONFIG = "my_texture"; 77 | 78 | 79 | var game = new Phaser.Game({ 80 | type: Phaser.AUTO, 81 | parent: "phaser-example", 82 | width: 800, 83 | height: 600, 84 | scene: { 85 | preload: preload, 86 | create: create 87 | } 88 | }); 89 | 90 | 91 | function preload() { 92 | this.load.json(COMPONENT_CONFIG, "assets/my_component.json"); 93 | this.load.multiatlas(TEXTURE_CONFIG, "assets/atlases/my_atlas.json", "assets/atlases/"); 94 | } 95 | 96 | function create() { 97 | let clip = new PhaserComps.ComponentClip( 98 | this, 99 | this.cache.json.get(COMPONENT_CONFIG), 100 | [ TEXTURE_CONFIG ] 101 | ); 102 | 103 | let component = new PhaserComps.UIComponents.UIComponentPrototype(); 104 | component.appendClip(clip); 105 | } 106 | ``` -------------------------------------------------------------------------------- /src/components/UIButtonRadio.js: -------------------------------------------------------------------------------- 1 | import UIButtonSelect from "./UIButtonSelect"; 2 | 3 | const _EVENT_SELECT = "event_select"; 4 | 5 | /** 6 | * @class UIButtonRadio 7 | * @memberOf PhaserComps.UIComponents 8 | * @extends PhaserComps.UIComponents.UIButtonSelect 9 | * @classdesc 10 | * Radio button. 11 | * Several radio buttons can be grouped by appending them to the first button in the group, 12 | * You can do it via passing first button to `appendTo` constructor argument of other instances,шаг 13 | * or use [appendToRadio]{@link PhaserComps.UIComponents.UIButtonRadio#appendToRadio} method. 14 | * 15 | * @property {Boolean} select set or get, if current instance selected 16 | * @property {*} value get or set current instance value 17 | * @property {*} valueSelected If read, returns value of currently selected radio instance in the group. 18 | * If assigned, selects radio which has provided value. 19 | * 20 | * @extends PhaserComps.UIComponents.UIButtonSelect 21 | * @emits PhaserComps.UIComponents.UIButton.EVENT_SELECT 22 | * 23 | * @param {PhaserComps.UIComponents.UIComponentPrototype} [parent] UIComponentPrototype instance to find clip inside 24 | * @param {String} [key] key to find clip inside parent 25 | * @param {String} [labelText] text to set for a 'label' key 26 | * @param {*} [value] Any value, applied to current radio button instance. 27 | * Use it to find out, what is the current selected value in the group, 28 | * or select radio by provided value 29 | * @param {UIButtonRadio} [appendTo] If specified, this instance will be appended 30 | * to provided radio group immediately 31 | */ 32 | 33 | export default class UIButtonRadio extends UIButtonSelect { 34 | 35 | /** 36 | * 37 | * @event PhaserComps.UIComponents.UIButtonRadio.EVENT_SELECT 38 | * @memberOf PhaserComps.UIComponents.UIButtonRadio 39 | * @description 40 | * Fired when some radio button of the group is selected 41 | */ 42 | static get EVENT_SELECT() { return _EVENT_SELECT; } 43 | 44 | constructor(parent, key, labelText, value, appendTo) { 45 | super(parent, key, labelText); 46 | this._sibling = this; 47 | this._value = value; 48 | if (typeof appendTo !== "undefined") { 49 | this.appendToRadio(appendTo); 50 | } 51 | } 52 | 53 | /** 54 | * @method PhaserComps.UIComponents.UIButtonRadio#appendToRadio 55 | * @description 56 | * Append this radio instance to provided radio sibling ring 57 | * @param {UIButtonRadio} radio radio button to append to sibling ring 58 | */ 59 | appendToRadio(radio) { 60 | if (this._sibling !== this) { 61 | this.removeFromSibling(); 62 | } 63 | this._sibling = radio._sibling; 64 | radio._sibling = this; 65 | } 66 | 67 | /** 68 | * @method PhaserComps.UIComponents.UIButtonRadio#removeFromSibling 69 | * @description 70 | * Remove this radio button from sibling ring 71 | */ 72 | removeFromSibling() { 73 | // TODO 74 | } 75 | 76 | /** 77 | * @method PhaserComps.UIComponents.UIButtonRadio#_onClick 78 | * @inheritDoc 79 | * @protected 80 | */ 81 | _onClick() { 82 | this.select = true; 83 | } 84 | 85 | get select() { return super.select; } 86 | set select(val) { 87 | if (this._select === val) { 88 | return; 89 | } 90 | super.select = val; 91 | if (val) { 92 | let radio = this._sibling; 93 | while (radio !== this) { 94 | radio.select = false; 95 | radio = radio._sibling; 96 | } 97 | this._broadcastSelect(); 98 | } 99 | } 100 | 101 | /** 102 | * @method PhaserComps.UIComponents.UIButtonRadio#_broadcastSelect 103 | * @description 104 | * Broadcast select event from all siblings 105 | * @private 106 | * @ignore 107 | */ 108 | _broadcastSelect() { 109 | this.emit(_EVENT_SELECT, this.value); 110 | let radio = this._sibling; 111 | while (radio !== this) { 112 | radio.emit(_EVENT_SELECT, this._value); 113 | radio = radio._sibling; 114 | } 115 | } 116 | 117 | get value() { 118 | return this._value; 119 | } 120 | 121 | set value(val) { 122 | this._value = val; 123 | } 124 | 125 | get valueSelected() { 126 | if (this.select) { 127 | return this.value; 128 | } 129 | let radio = this._sibling; 130 | while (radio !== this) { 131 | if (radio.select) { 132 | return radio.value; 133 | } 134 | radio = radio._sibling; 135 | } 136 | return null; 137 | } 138 | 139 | set valueSelected(val) { 140 | if (this.value === val) { 141 | this.select = true; 142 | return; 143 | } 144 | let radio = this._sibling; 145 | while (radio !== this) { 146 | if (radio.value === val) { 147 | radio.select = true; 148 | return; 149 | } 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /src/components/UIButtonDraggable.js: -------------------------------------------------------------------------------- 1 | import UIButton from "./UIButton"; 2 | import UIManager from "../manager/UIManager"; 3 | const _EVENT_DRAG = "event_drag"; 4 | 5 | /** 6 | * @typedef DragBounds 7 | * @memberOf PhaserComps.UIComponents.UIButtonDraggable 8 | * @property {Number} minX left drag bound 9 | * @property {Number} minY top drag bound 10 | * @property {Number} maxX right drag bound 11 | * @property {Number} maxY bottom drag bound 12 | */ 13 | 14 | /** 15 | * @class UIButtonDraggable 16 | * @memberOf PhaserComps.UIComponents 17 | * @classdesc 18 | * Same as {@link UIComponents.UIButton}, but also emits EVENT_DRAG with two arguments, horizontal and vertical movement delta 19 | * 20 | * @extends UIComponents.UIButton 21 | * @emits EVENT_CLICK, 22 | * @emits EVENT_DRAG, 23 | * 24 | * @property {Boolean} enable activate/deactivate button interaction. if false, button state is set to `disable` 25 | * @property {String} label get/set button label text 26 | * 27 | * @param {PhaserComps.UIComponents.UIComponentPrototype} [parent] UIComponentPrototype instance to find clip inside 28 | * @param {String} [key] key to find clip inside parent 29 | * @param {String} [labelText] text to set for a 'label' key 30 | */ 31 | 32 | export default class UIButtonDraggable extends UIButton { 33 | 34 | /** 35 | * @event PhaserComps.UIComponents.UIButtonDraggable.EVENT_DRAG 36 | * @memberOf PhaserComps.UIComponents.UIButtonDraggable 37 | * @description 38 | * Emitted on drag move. 39 | * @param {Number} x horizontal drag movement (from drag start) 40 | * @param {Number} y vertical drag movement (from drag start) 41 | */ 42 | static get EVENT_DRAG() { return _EVENT_DRAG; } 43 | 44 | constructor(parent, key, labelText) { 45 | super(parent, key, labelText); 46 | /** 47 | * 48 | * @type DragBounds 49 | * @private 50 | */ 51 | this._dragBounds = { 52 | minX: 0, 53 | maxX: 0, 54 | minY: 0, 55 | maxY: 0 56 | }; 57 | /** 58 | * 59 | * @type {number} 60 | * @private 61 | */ 62 | this._startDragX = 0; 63 | /** 64 | * 65 | * @type {number} 66 | * @private 67 | */ 68 | this._startDragY = 0; 69 | /** 70 | * 71 | * @type {Phaser.GameObjects.Zone} 72 | * @private 73 | */ 74 | this._dragZone = null; 75 | } 76 | 77 | /** 78 | * Set clip drag bounds 79 | * @method PhaserComps.UIComponents.UIButtonDraggable#setDragBounds 80 | * @param {Number} minX left drag bound 81 | * @param {Number} minY top drag bound 82 | * @param {Number} maxX right drag bound 83 | * @param {Number} maxY bottom drag bound 84 | */ 85 | setDragBounds(minX, minY, maxX, maxY) { 86 | this._dragBounds.minX = minX; 87 | this._dragBounds.maxX = maxX; 88 | this._dragBounds.minY = minY; 89 | this._dragBounds.maxY = maxY; 90 | } 91 | 92 | /** 93 | * _dragZone `dragstart` event callback 94 | * @method PhaserComps.UIComponents.UIButtonDraggable#_onDragStart 95 | * @param pointer 96 | * @param {Phaser.GameObjects.GameObject} gameObject 97 | * @protected 98 | */ 99 | _onDragStart(pointer, gameObject) { 100 | if (!this._dragZone || this._dragZone !== gameObject) { 101 | return; 102 | } 103 | if (!this._clip) { 104 | return; 105 | } 106 | this._startDragX = this._clip.x - gameObject.input.dragStartX; 107 | this._startDragY = this._clip.y - gameObject.input.dragStartY; 108 | } 109 | 110 | /** 111 | * _dragZone `drag` event callback 112 | * @method PhaserComps.UIComponents.UIButtonDraggable#_onDrag 113 | * @param pointer 114 | * @param {Phaser.GameObjects.GameObject} gameObject 115 | * @param {Number} dragX 116 | * @param {Number} dragY 117 | * @protected 118 | */ 119 | _onDrag(pointer, gameObject, dragX, dragY) { 120 | if (!UIManager.check(this.lockId)){ 121 | return; 122 | } 123 | if (!this._dragZone || this._dragZone !== gameObject || this.clip) { 124 | return; 125 | } 126 | let newX = this._startDragX + dragX; 127 | let newY = this._startDragY + dragY; 128 | if (newX < this._dragBounds.minX) { 129 | newX = this._dragBounds.minX; 130 | } else if (newX > this._dragBounds.maxX) { 131 | newX = this._dragBounds.maxX; 132 | } 133 | 134 | if (newY < this._dragBounds.minY) { 135 | newY = this._dragBounds.minY; 136 | } else if (newY > this._dragBounds.maxY) { 137 | newY = this._dragBounds.maxY; 138 | } 139 | this.emit(_EVENT_DRAG, newX, newY); 140 | } 141 | 142 | _setupInteractive(zone) { 143 | super._setupInteractive(zone); 144 | this._dragZone = zone; 145 | //zone.scene.input.dragDistanceThreshold = 3; 146 | zone.scene.input.setDraggable(zone, true); 147 | zone.scene.input.on("dragstart", this._onDragStart, this); 148 | zone.scene.input.on("drag", this._onDrag, this); 149 | } 150 | 151 | _removeInteractive(zone) { 152 | super._removeInteractive(zone); 153 | this._dragZone = null; 154 | zone.scene.input.setDraggable(zone, false); 155 | zone.scene.input.removeListener("dragstart", this._onDragStart); 156 | zone.scene.input.removeListener("drag", this._onDrag); 157 | } 158 | 159 | } -------------------------------------------------------------------------------- /src/components/UIProgressBar.js: -------------------------------------------------------------------------------- 1 | import UIComponentPrototype from "./UIComponentPrototype"; 2 | 3 | const PROGRESS_STATE_REGEX = /progress_(\d+)$/; 4 | 5 | /** 6 | * @typedef StepConfig 7 | * @memberOf PhaserComps.UIComponents.UIScrollBar 8 | * @extends Object 9 | * 10 | * @property {Number} stepValue max value of this step 11 | * @property {PhaserComps.ComponentClip.StateConfig} config 12 | */ 13 | 14 | /** 15 | * @memberOf PhaserComps.UIComponents 16 | * @class UIProgressBar 17 | * @classdesc 18 | * Progress bar. 19 | * 20 | * Setup states `progress_0` and `progress_100` in Animate, 21 | * and all differences between them will be interpolated to the current progress value. 22 | * Also you can create intermediate states, if you want to control intermediate interpolation behaviour. 23 | 24 | * For example, if you want an indicator to rotate a full circle, you need to create additional 25 | * intermediate states `progress_30` and `progress_70` with 30% and 70$ of rotation, 26 | * to be sure, that indicator will rotate in the needed direction. 27 | * 28 | * Also you can use intermediate states to make interpolation not linear for all progress range. 29 | * 30 | * Available properties for interpolating are 31 | * `x`, `y`, `scaleX`, `scaleY`, `angle`, `alpha` 32 | * 33 | * @property {Number} value current progress value, between 0 and 1 34 | * 35 | * @extends PhaserComps.UIComponents.UIComponentPrototype 36 | * @param {PhaserComps.UIComponents.UIComponentPrototype} [parent] UIComponentPrototype instance to find clip inside 37 | * @param {String} [key] key to find clip inside parent 38 | */ 39 | export default class UIProgressBar extends UIComponentPrototype { 40 | constructor(parent, key) { 41 | super(parent, key); 42 | 43 | /** 44 | * current progress value 45 | * @type {Number} 46 | * @private 47 | */ 48 | this._value = 0; 49 | /** 50 | * 51 | * @type {Array} 52 | * @private 53 | */ 54 | this._steps = []; 55 | } 56 | 57 | get value() { 58 | return this._value; 59 | } 60 | 61 | set value(v) { 62 | this._value = v; 63 | this._applyValue(); 64 | } 65 | 66 | 67 | onClipAppend(clip) { 68 | super.onClipAppend(clip); 69 | this._makeSteps(clip); 70 | this._applyValue(); 71 | } 72 | 73 | /** 74 | * Apply current value to setup elements positions 75 | * @method PhaserComps.UIComponents.UIScrollBar#_applyValue 76 | * @private 77 | */ 78 | _applyValue() { 79 | if (!this._clip) { 80 | return; 81 | } 82 | let i, lowConfig, highConfig, interpolation, resultConfig, childId; 83 | let stepsCount = this._steps.length; 84 | for (i = 0; i < stepsCount - 1; i++) { 85 | let low = this._steps[i]; 86 | let high = this._steps[i + 1]; 87 | if (low.stepValue === this._value) { 88 | resultConfig = low.config; 89 | break; 90 | } else if (high.stepValue === this._value) { 91 | resultConfig = high.config; 92 | break; 93 | } else if (this._value > low.stepValue && this._value < high.stepValue) { 94 | lowConfig = low.config; 95 | highConfig = high.config; 96 | interpolation = (this._value - low.stepValue) / (high.stepValue - low.stepValue); 97 | break; 98 | } 99 | } 100 | // make interpolated children configs 101 | if (!resultConfig) { 102 | resultConfig = {}; 103 | for (childId in lowConfig) { 104 | if (!highConfig.hasOwnProperty(childId)) { 105 | continue; 106 | } 107 | let lowChildConfig = lowConfig[childId]; 108 | let highChildConfig = highConfig[childId]; 109 | resultConfig[childId] = { 110 | x: lowChildConfig.x + (highChildConfig.x - lowChildConfig.x) * interpolation, 111 | y: lowChildConfig.y + (highChildConfig.y - lowChildConfig.y) * interpolation, 112 | scaleX: lowChildConfig.scaleX + (highChildConfig.scaleX - lowChildConfig.scaleX) * interpolation, 113 | scaleY: lowChildConfig.scaleY + (highChildConfig.scaleY - lowChildConfig.scaleY) * interpolation, 114 | angle: lowChildConfig.angle + (highChildConfig.angle - lowChildConfig.angle) * interpolation, 115 | alpha: lowChildConfig.alpha + (highChildConfig.alpha - lowChildConfig.alpha) * interpolation 116 | } 117 | } 118 | } 119 | // apply children configs 120 | for (childId in resultConfig) { 121 | this._clip.applyChildParams(childId, resultConfig[childId]); 122 | } 123 | } 124 | 125 | /** 126 | * Retreive all progress states and setup all progress steps, that will be used to control 127 | * @param {PhaserComps.ComponentClip} clip 128 | * @private 129 | */ 130 | _makeSteps(clip) { 131 | let stateIds = clip.getStateIds(); 132 | /** 133 | * @type {{stepValue: number, config: {}}[]} 134 | * @protected 135 | */ 136 | this._steps = []; 137 | for (let stateId of stateIds) { 138 | if (!PROGRESS_STATE_REGEX.test(stateId)) { 139 | continue; 140 | } 141 | 142 | let stepConfig = {}; 143 | let stepObject = { 144 | stepValue: parseInt(PROGRESS_STATE_REGEX.exec(stateId)[1]) / 100, 145 | config: stepConfig 146 | }; 147 | let stateConfig = clip.getStateConfig(stateId); 148 | for (let childId in stateConfig) { 149 | stepConfig[childId] = UIProgressBar._makeFullConfig(stateConfig[childId]); 150 | } 151 | this._steps.push(stepObject); 152 | } 153 | this._steps.sort((a, b) => a.stepValue - b.stepValue); 154 | } 155 | 156 | /** 157 | * Obviously create all state properties, even if they have default values 158 | * 159 | * @param config 160 | * @returns {{ 161 | * scaleX: number, 162 | * scaleY: number, 163 | * alpha: number, 164 | * x: number, 165 | * y: number, 166 | * angle: number 167 | * }} 168 | * @private 169 | */ 170 | static _makeFullConfig(config) { 171 | return { 172 | x: config.hasOwnProperty("x") ? config.x : 0, 173 | y: config.hasOwnProperty("y") ? config.y : 0, 174 | scaleX: config.hasOwnProperty("scaleX") ? config.scaleX : 1, 175 | scaleY: config.hasOwnProperty("scaleY") ? config.scaleY : 1, 176 | angle: config.hasOwnProperty("angle") ? config.angle : 0, 177 | alpha: config.hasOwnProperty("alpha") ? config.alpha : 1 178 | } 179 | } 180 | } -------------------------------------------------------------------------------- /src/components/UIButton.js: -------------------------------------------------------------------------------- 1 | import UIComponentPrototype from "./UIComponentPrototype"; 2 | import UIManager from "../manager/UIManager"; 3 | import Phaser from "phaser"; 4 | 5 | const HIT_ZONE = "HIT_ZONE"; 6 | const LABEL = "label"; 7 | 8 | const _STATE_UP = "up"; 9 | const _STATE_DOWN = "down"; 10 | const _STATE_OVER = "over"; 11 | const _STATE_DISABLE = "disable"; 12 | 13 | const _EVENT_CLICK = "event_click"; 14 | 15 | /** 16 | * @class UIButton 17 | * @memberOf PhaserComps.UIComponents 18 | * @classdesc 19 | * Button component prototype, has states `up`, `over`, `down`, `disable` 20 | * Emits EVENT_CLICK on click. 21 | * When disabled, doesn't interact to mouse events and move to state `disable` 22 | * @extends PhaserComps.UIComponents.UIComponentPrototype 23 | * @emits PhaserComps.UIComponents.UIButton.EVENT_CLICK 24 | * 25 | * @property {Boolean} enable activate/deactivate button interaction. if false, button state is set to `disable` 26 | * @property {String} label get/set button label text 27 | * 28 | * @param {PhaserComps.UIComponents.UIComponentPrototype} [parent] UIComponentPrototype instance to find clip inside 29 | * @param {String} [key] key to find clip inside parent 30 | * @param {String} [labelText] text to set for a 'label' key 31 | */ 32 | export default class UIButton extends UIComponentPrototype { 33 | 34 | /** 35 | * @event PhaserComps.UIComponents.UIButton.EVENT_CLICK 36 | * @memberOf PhaserComps.UIComponents.UIButton 37 | * @description 38 | * Emitted on click 39 | */ 40 | static get EVENT_CLICK() { return _EVENT_CLICK; } 41 | 42 | constructor(parent, key, labelText) { 43 | super(parent, key); 44 | this._enable = true; 45 | this._isPressed = false; 46 | this._isOver = false; 47 | /** 48 | * @type {Phaser.GameObjects.Zone} 49 | * @private 50 | */ 51 | this._hitZone = null; 52 | if (labelText) { 53 | this.label = labelText; 54 | } 55 | } 56 | 57 | /** 58 | * @method PhaserComps.UIComponents.UIButton#onClipAppend 59 | * @inheritDoc 60 | */ 61 | onClipAppend(clip) { 62 | this._updateInteractive(); 63 | } 64 | 65 | /** 66 | * @method PhaserComps.UIComponents.UIButton#onClipRemove 67 | * @inheritDoc 68 | */ 69 | onClipRemove(clip) { 70 | let zone = clip.getChildClip(HIT_ZONE); 71 | if (!zone) { 72 | //console.warn("no hit zone for", this._key); 73 | return; 74 | } 75 | this._removeInteractive(zone); 76 | } 77 | 78 | get label() { return this.getText(LABEL); } 79 | set label(value) { 80 | this.setText(LABEL, value); 81 | } 82 | 83 | get enable() { return this._enable; } 84 | set enable(value) { 85 | if (this._enable === value) { 86 | return; 87 | } 88 | this._enable = value; 89 | this._updateInteractive(); 90 | this.doState(); 91 | } 92 | 93 | /** 94 | * @method UIButton#_setupInteractive 95 | * @param {Phaser.GameObjects.Zone} zone 96 | * @private 97 | */ 98 | _setupInteractive(zone) { 99 | zone.setInteractive({ useHandCursor: true }); 100 | zone.on("pointerdown", this._onPointerDown, this); 101 | zone.on("pointerup", this._onPointerUp, this); 102 | zone.on("pointerover", this._onPointerOver, this); 103 | zone.on("pointerout", this._onPointerOut, this); 104 | this._hitZone = zone; 105 | } 106 | 107 | /** 108 | * @method PhaserComps.UIComponents.UIButton#_removeInteractive 109 | * @param {Phaser.GameObjects.Zone} zone 110 | * @private 111 | */ 112 | _removeInteractive(zone) { 113 | zone.disableInteractive(); 114 | zone.removeListener(Phaser.Input.Events.GAMEOBJECT_POINTER_DOWN, this._onPointerDown, this); 115 | zone.removeListener(Phaser.Input.Events.GAMEOBJECT_POINTER_UP, this._onPointerUp, this); 116 | zone.removeListener(Phaser.Input.Events.GAMEOBJECT_POINTER_OVER, this._onPointerOver, this); 117 | zone.removeListener(Phaser.Input.Events.GAMEOBJECT_POINTER_OUT, this._onPointerOut, this); 118 | this._hitZone = null; 119 | } 120 | 121 | get lockClipBounds() { 122 | return this._hitZone ? this._hitZone.getBounds() : null; 123 | } 124 | 125 | get lockClip() { 126 | return this._hitZone; 127 | } 128 | 129 | /** 130 | * @method PhaserComps.UIComponents.UIButton#_updateInteractive 131 | * @private 132 | */ 133 | _updateInteractive() { 134 | if (!this._clip) { 135 | return; 136 | } 137 | let zone = this._clip.getChildClip(HIT_ZONE); 138 | if (!zone) { 139 | //console.warn("no hit zone for", this._key); 140 | return; 141 | } 142 | if (this.enable) { 143 | this._setupInteractive(zone); 144 | } else { 145 | this._removeInteractive(zone); 146 | } 147 | } 148 | 149 | /** 150 | * @method PhaserComps.UIComponents.UIButton#getStateId 151 | * @inheritDoc 152 | * @returns {String} 153 | */ 154 | getStateId() { 155 | if (!this.enable) { 156 | return this.STATE_DISABLE; 157 | } 158 | if (this._isPressed) { 159 | return this.STATE_DOWN; 160 | } 161 | if (this._isOver) { 162 | return this.STATE_OVER; 163 | } 164 | return this.STATE_UP; 165 | } 166 | 167 | /** 168 | * @protected 169 | * @method PhaserComps.UIComponents.UIButton#_onClick 170 | * @description 171 | * called when button hit zone clicked, emits EVENT_CLICK 172 | */ 173 | _onClick() { 174 | this.emit(_EVENT_CLICK); 175 | } 176 | 177 | get STATE_UP() { return _STATE_UP; } 178 | get STATE_DOWN() { return _STATE_DOWN; } 179 | get STATE_OVER() { return _STATE_OVER; } 180 | get STATE_DISABLE() { return _STATE_DISABLE; } 181 | 182 | /** 183 | * @method UIButton#_onPointerOut 184 | * @protected 185 | */ 186 | _onPointerOut() { 187 | this._isOver = false; 188 | this._isPressed = false; 189 | this.doState(); 190 | } 191 | 192 | /** 193 | * @method UIButton#_onPointerOver 194 | * @protected 195 | */ 196 | _onPointerOver() { 197 | this._isOver = true; 198 | this.doState(); 199 | } 200 | 201 | /** 202 | * @method UIButton#_onPointerDown 203 | * @protected 204 | */ 205 | _onPointerDown(pointer, localX, localY, event) { 206 | this._isPressed = true; 207 | this.doState(); 208 | event.stopPropagation(); 209 | } 210 | 211 | /** 212 | * @method UIButton#_onPointerUp 213 | * @protected 214 | */ 215 | _onPointerUp(pointer, localX, localY, event) { 216 | let isClicked = this._isPressed && this._isOver; 217 | this._isPressed = false; 218 | this.doState(); 219 | if (isClicked) { 220 | event.stopPropagation(); 221 | if (UIManager.check(this.lockId)) { 222 | this._onClick(); 223 | } 224 | } 225 | } 226 | 227 | /** 228 | * @method UIButton#destroy 229 | * @protected 230 | * @inheritDoc 231 | */ 232 | destroy(fromScene) { 233 | if (this._clip) { 234 | let zone = this._clip.getChildClip(HIT_ZONE); 235 | if (zone) { 236 | this._removeInteractive(zone); 237 | } 238 | } 239 | super.destroy(fromScene); 240 | } 241 | } -------------------------------------------------------------------------------- /src/components/UIScrollBar.js: -------------------------------------------------------------------------------- 1 | import UIComponentPrototype from "./UIComponentPrototype"; 2 | import UIButton from "./UIButton"; 3 | import UIButtonDraggable from "./UIButtonDraggable"; 4 | import UIManager from "../manager/UIManager"; 5 | 6 | const _EVENT_CHANGE = "event_change"; 7 | 8 | /** 9 | * @class UIScrollBar 10 | * @memberOf PhaserComps.UIComponents 11 | * @classdesc 12 | * Scroll bar component. can be vertical or horizontal
13 | * It have up and down buttons inside, draggable thumb button.
14 | * `DIMENSIONS` zone defines thumb drag bounds. 15 | * 16 | * default value range is 0 to 1, You can change it by setting 17 | * min and max value. Also you can set value step, so value will always 18 | * be stepped by it 19 | * 20 | * @emits PhaserComps.UIComponents.UIScrollBar.EVENT_CHANGE 21 | * @property {Number} value current bar value, from min value to max, default is from 0 to 1 22 | * 23 | * @param {PhaserComps.UIComponents.UIComponentPrototype} [parent] UIComponentPrototype instance to find clip inside 24 | * @param {String} [key] key to find clip inside parent 25 | * @param {Boolean} [vertical=false] scroll bar behave like vertical or horizontal 26 | */ 27 | 28 | export default class UIScrollBar extends UIComponentPrototype { 29 | 30 | /** 31 | * @event PhaserComps.UIComponents.UIScrollBar.EVENT_CHANGE 32 | * @memberOf PhaserComps.UIComponents.UIScrollBar 33 | * @description 34 | * Emitted on scroll bar value change. 35 | * @param {Number} value current scrollbar value 36 | */ 37 | static get EVENT_CHANGE() { return _EVENT_CHANGE; } 38 | 39 | constructor(parent, key, vertical) { 40 | super(parent, key); 41 | this._vertical = vertical || false; 42 | 43 | /** 44 | * 45 | * @type {number} 46 | * @private 47 | */ 48 | this._value = 0; 49 | /** 50 | * 51 | * @type {number} 52 | * @private 53 | */ 54 | this._minValue = 0; 55 | /** 56 | * 57 | * @type {number} 58 | * @private 59 | */ 60 | this._maxValue = 1; 61 | /** 62 | * 63 | * @type {number} 64 | * @private 65 | */ 66 | this._valueStep = 0; 67 | /** 68 | * 69 | * @type {number} 70 | * @private 71 | */ 72 | this._buttonStep = 0.1; 73 | 74 | /** 75 | * scroll up/left button 76 | * @type {PhaserComps.UIComponents.UIButton} 77 | */ 78 | this.btnPrev = new UIButton(this, "btn_up"); 79 | this.btnPrev.on(UIButton.EVENT_CLICK, this.onPrevClick, this); 80 | /** 81 | * scroll down/right button 82 | * @type {PhaserComps.UIComponents.UIButton} 83 | */ 84 | this.btnNext = new UIButton(this, "btn_down"); 85 | this.btnNext.on(UIButton.EVENT_CLICK, this.onNextClick, this); 86 | 87 | /** 88 | * 89 | * @type {PhaserComps.UIComponents.UIButtonDraggable} 90 | */ 91 | this.thumb = new UIButtonDraggable(this, "thumb"); 92 | this.thumb.on(UIButtonDraggable.EVENT_DRAG, this._onThumbDrag, this); 93 | 94 | /** 95 | * 96 | * @type {number} 97 | * @private 98 | */ 99 | this._trackStart = 0; 100 | /** 101 | * 102 | * @type {number} 103 | * @private 104 | */ 105 | this._trackLength = 100; 106 | 107 | /** 108 | * 109 | * @type {PhaserComps.ComponentClip} 110 | * @private 111 | */ 112 | this._thumbClip = null; 113 | } 114 | 115 | /** 116 | * Setup scroll bar value bounds and value step 117 | * @method PhaserComps.UIComponents.UIScrollBar#setValueBounds 118 | * @param {Number} minValue minimum value 119 | * @param {Number} maxValue maximum value 120 | * @param {Number} [valueStep=0] value change step 121 | */ 122 | setValueBounds(minValue, maxValue, valueStep) { 123 | this._minValue = minValue; 124 | this._maxValue = maxValue; 125 | if (typeof valueStep !== "undefined") { 126 | this._valueStep = valueStep; 127 | if (this._buttonStep < valueStep) { 128 | this._buttonStep = valueStep; 129 | } 130 | } else { 131 | this._valueStep = 0; 132 | } 133 | 134 | } 135 | 136 | /** 137 | * @method PhaserComps.UIComponents.UIScrollBar#setButtonStep 138 | * @param {Number} val 139 | */ 140 | setButtonStep(val) { 141 | this._buttonStep = val; 142 | } 143 | 144 | /** 145 | * @method PhaserComps.UIComponents.UIScrollBar#onClipAppend 146 | * @inheritDoc 147 | */ 148 | onClipAppend(clip) { 149 | super.onClipAppend(clip); 150 | this._updateClips(); 151 | } 152 | 153 | /** 154 | * @method PhaserComps.UIComponents.UIScrollBar#_updateClips 155 | * @private 156 | */ 157 | _updateClips() { 158 | if (!this._clip) { 159 | return; 160 | } 161 | this._thumbClip = this.thumb._clip; 162 | let trackClip = this._clip.getChildClip("DIMENSIONS"); 163 | if (trackClip) { 164 | this._trackStart = this._vertical ? trackClip.y : trackClip.x; 165 | this._trackLength = this._vertical ? trackClip.height : trackClip.width; 166 | 167 | if (this._thumbClip) { 168 | this.thumb.setDragBounds( 169 | this._vertical ? this._thumbClip.x : trackClip.x, 170 | this._vertical ? trackClip.y : this._thumbClip.y, 171 | this._vertical ? this._thumbClip.x : trackClip.x + trackClip.width, 172 | this._vertical ? trackClip.y + trackClip.height : this._thumbClip.y 173 | ); 174 | } 175 | } 176 | let hitZone = this._clip.getChildClip("HIT_ZONE"); 177 | if (hitZone) { 178 | hitZone.on("pointerdown", this._onZoneDown, this); 179 | //hitZone.on("pointerup", this.onZoneUp, this); 180 | } 181 | this._updateThumbFromValue(); 182 | } 183 | 184 | /** 185 | * @method PhaserComps.UIComponents.UIScrollBar#onClipProcess 186 | * @inheritDoc 187 | */ 188 | onClipProcess() { 189 | super.onClipProcess(); 190 | if (!this.thumb) {// call from super constructor 191 | return; 192 | } 193 | this._updateClips(); 194 | 195 | } 196 | 197 | /** 198 | * @method PhaserComps.UIComponents.UIScrollBar#onPrevClick 199 | * @protected 200 | */ 201 | onPrevClick() { 202 | if (!UIManager.check(this.lockId)) { 203 | return; 204 | } 205 | this.value -= this._buttonStep; 206 | } 207 | 208 | /** 209 | * @method PhaserComps.UIComponents.UIScrollBar#onNextClick 210 | * @protected 211 | */ 212 | onNextClick() { 213 | if (!UIManager.check(this.lockId)) { 214 | return; 215 | } 216 | this.value += this._buttonStep; 217 | } 218 | 219 | /** 220 | * @method PhaserComps.UIComponents.UIScrollBar#_updateThumbFromValue 221 | * @private 222 | */ 223 | _updateThumbFromValue() { 224 | if (!this._thumbClip) { 225 | return; 226 | } 227 | let barPosition = Math.round(this._trackStart + this._trackLength * this._value); 228 | if (this._vertical) { 229 | this._thumbClip.y = barPosition; 230 | } else { 231 | this._thumbClip.x = barPosition; 232 | } 233 | this.emit(_EVENT_CHANGE, this.value); 234 | } 235 | 236 | get value() { 237 | let v = this._value * (this._maxValue - this._minValue); 238 | if (this._valueStep === 0) { 239 | return v + this._minValue; 240 | } 241 | v = Math.round(v / this._valueStep) * this._valueStep; 242 | return v + this._minValue; 243 | } 244 | 245 | set value(val) { 246 | let v = (val - this._minValue) / (this._maxValue - this._minValue); 247 | if (v < 0) v = 0; 248 | if (v > 1) v = 1; 249 | if (v === this._value) { 250 | return; 251 | } 252 | this._value = v; 253 | this._updateThumbFromValue(); 254 | } 255 | 256 | /** 257 | * Thumb button drag move handler 258 | * @param {Number} positionX 259 | * @param {Number} positionY 260 | * @private 261 | */ 262 | _onThumbDrag(positionX, positionY) { 263 | if (this._trackLength === 0) { 264 | return; 265 | } 266 | if (!UIManager.check(this.lockId)) { 267 | return; 268 | } 269 | let barPosition = this._vertical ? positionY : positionX; 270 | let newValue = (barPosition - this._trackStart) / this._trackLength; 271 | let v = newValue * (this._maxValue - this._minValue); 272 | if (this._valueStep !== 0) { 273 | v = Math.round(v / this._valueStep) * this._valueStep; 274 | } 275 | // minValue added only after step normalization 276 | this.value = v + this._minValue; 277 | } 278 | 279 | /** 280 | * Zone around scrollbar thumb click handler 281 | * @method PhaserComps.UIComponents.UIScrollBar#_onZoneDown 282 | */ 283 | _onZoneDown() { 284 | // TODO 285 | } 286 | } -------------------------------------------------------------------------------- /src/components/UIComponentPrototype.js: -------------------------------------------------------------------------------- 1 | import Phaser from "phaser"; 2 | import PhaserComps from "../phasercomps"; 3 | 4 | const _EVENT_STATE = "event_state"; 5 | 6 | /** 7 | * @memberOf PhaserComps.UIComponents 8 | * @class UIComponentPrototype 9 | * @classdesc Base ComponentView controller class. Used to setup component state and texts. 10 | * Once root instance is created, you must append a [ComponentClip]{@link PhaserComps.ComponentClip} 11 | * instance to it. 12 | * 13 | * Child clips will be appended automatically on every state change, their clips will be found by keys. 14 | * On state change notifies all child components to update their states. 15 | * 16 | * *One of the main ideas of this framework is if there is no clip for UIComponentPrototype 17 | * at current state or at all, nothing bad happens.* 18 | * @inheritDoc 19 | * @extends Phaser.Events.EventEmitter 20 | * @property {String} lockId Used by UIManager, see {@link PhaserComps.UIManager} 21 | * @param {PhaserComps.UIComponents.UIComponentPrototype} [parent] UIComponentPrototype instance to find clip inside 22 | * @param {String} [key] key to find clip inside parent 23 | */ 24 | export default class UIComponentPrototype extends Phaser.Events.EventEmitter { 25 | 26 | static get EVENT_STATE() { return _EVENT_STATE; } 27 | 28 | /** 29 | * @param {PhaserComps.UIComponents.UIComponentPrototype} parent 30 | * @param {String} key 31 | */ 32 | constructor(parent, key) { 33 | super(); 34 | 35 | /** 36 | * @type {String} 37 | */ 38 | this._lockId = null; 39 | 40 | /** 41 | * 42 | * @type {UIComponentPrototype} 43 | * @private 44 | */ 45 | this._parent = parent; 46 | 47 | /** 48 | * 49 | * @type {String} 50 | * @protected 51 | */ 52 | this._key = key; 53 | 54 | /** 55 | * 56 | * @type {PhaserComps.ComponentClip} 57 | * @protected 58 | */ 59 | this._clip = null; 60 | 61 | /** 62 | * 63 | * @type {Object} 64 | * @private 65 | */ 66 | this._texts = {}; 67 | 68 | if (key && parent) { 69 | // sign on parents state update 70 | parent.on(_EVENT_STATE, this._onEventState, this); 71 | } 72 | this._clipUpdate(); 73 | } 74 | 75 | /** 76 | * @public 77 | * @method PhaserComps.UIComponents.UIComponentPrototype#appendClip 78 | * @description 79 | * Append a instance to this to control it. State setup will be processed immediately.
80 | * Use only for root instance, child instances will be appended automatically depending on state of this. 81 | * @param {PhaserComps.ComponentClip} clip ComponentView instance to append 82 | */ 83 | appendClip(clip) { 84 | if (this._clip === clip) { 85 | return; 86 | } 87 | if (this._clip !== null) { 88 | this.removeClip(); 89 | } 90 | this._clip = clip; 91 | if (this._clip) { 92 | this.onClipAppend(this._clip); 93 | } 94 | this._clipProcess(); 95 | } 96 | 97 | /** @return {String} */ 98 | get lockId() { 99 | return this._lockId; 100 | } 101 | 102 | /** 103 | * @return {Phaser.Geom.Rectangle} 104 | */ 105 | get lockClipBounds() { return null; } // override 106 | 107 | /** @return {Phaser.GameObjects.GameObject|*} */ 108 | get lockClip() { return null; } // override 109 | 110 | /** @param {string} value */ 111 | set lockId(value) { 112 | if (this._lockId === value) { 113 | return; 114 | } 115 | if (this._lockId) { 116 | PhaserComps.UIManager.unregister(this); 117 | } 118 | this._lockId = value; 119 | if (this._lockId) { 120 | PhaserComps.UIManager.register(this); 121 | } 122 | } 123 | 124 | /** 125 | * Override this, if you want to do something, when new clip removed, 126 | * @method PhaserComps.UIComponents.UIComponentPrototype#onClipAppend 127 | * @protected 128 | * @param {PhaserComps.ComponentClip} clip 129 | */ 130 | onClipAppend(clip) { 131 | // override me 132 | } 133 | 134 | /** 135 | * @public 136 | * @method PhaserComps.UIComponents.UIComponentPrototype#removeClip 137 | * @protected 138 | */ 139 | removeClip() { 140 | this.onClipRemove(this._clip); 141 | this._clip = null; 142 | } 143 | 144 | /** 145 | * Override this, if you want to do something, when new clip removed, 146 | * like remove clip events listeners. 147 | * @method PhaserComps.UIComponents.UIComponentPrototype#onClipRemove 148 | * @protected 149 | * @param clip 150 | */ 151 | onClipRemove(clip) { 152 | // override me 153 | } 154 | 155 | /** 156 | * Call doState to setup new state, id is provided by [getStateId]{@link PhaserComps.UIComponents.UIComponentPrototype#getStateId} 157 | * @method PhaserComps.UIComponents.UIComponentPrototype#doState 158 | * @protected 159 | * @see #getStateId 160 | */ 161 | doState() { 162 | let stateId = this.getStateId(); 163 | this._setupState(stateId); 164 | } 165 | 166 | /** 167 | * Returns saved text by key, if it was set previously 168 | * @method PhaserComps.UIComponents.UIComponentPrototype#getText 169 | * @param {String} key 170 | * @returns {String|Array} text value 171 | */ 172 | getText(key) { 173 | return this._texts[key]; 174 | } 175 | 176 | /** 177 | * Set text value to the textfield with provided key. 178 | * Text value is saved in the component's instance dictionary and will be set to the textField on every state change 179 | * @method PhaserComps.UIComponents.UIComponentPrototype#setText 180 | * @param {String} key TextField key 181 | * @param {String|Array} text text string 182 | */ 183 | setText(key, text) { 184 | if (this._texts[key] === text) { 185 | return; 186 | } 187 | this._texts[key] = text; 188 | if (this._clip) { 189 | let textField = this._clip.getChildText(key); 190 | if (textField) { 191 | textField.text = text; 192 | } 193 | } 194 | } 195 | 196 | /** 197 | * @method PhaserComps.UIComponents.UIComponentPrototype#getStateId 198 | * @description 199 | * Current state id, used by [doState]{@link PhaserComps.UIComponents.UIComponentPrototype#doState} method 200 | * @returns {String} 201 | * @protected 202 | */ 203 | getStateId() { 204 | return "default"; 205 | } 206 | 207 | /** 208 | * Destroy ComponentPrototype and clip, if exists 209 | * @method PhaserComps.UIComponents.UIComponentPrototype#destroy 210 | * @protected 211 | * @param {Boolean} [fromScene=false] 212 | */ 213 | destroy(fromScene) { 214 | if (this._parent){ 215 | this._parent.removeListener(_EVENT_STATE, this._onEventState); 216 | } 217 | if (this._clip) { 218 | this._clip.destroy(fromScene); 219 | } 220 | super.destroy(); 221 | } 222 | 223 | /** 224 | * @method PhaserComps.UIComponents.UIComponentPrototype#_clipUpdate 225 | * @private 226 | */ 227 | _clipUpdate() { 228 | if (!this._key) { 229 | // parent is clip itself 230 | } else { 231 | if (this._parent._clip) { 232 | let clip = this._parent._clip.getChildClip(this._key); 233 | this.appendClip(clip); 234 | } else { 235 | this.appendClip(null); 236 | } 237 | } 238 | } 239 | 240 | /** 241 | * @method PhaserComps.UIComponents.UIComponentPrototype#_clipProcess 242 | * @private 243 | */ 244 | _clipProcess() { 245 | if (!this._clip) { 246 | return; 247 | } 248 | this.doState(); 249 | this.onClipProcess(); 250 | } 251 | 252 | /** 253 | * Override this, if you want to do something, when state or clip changes. 254 | * @method PhaserComps.UIComponents.UIComponentPrototype#onClipProcess 255 | * @protected 256 | * @override 257 | */ 258 | onClipProcess() { 259 | // override me 260 | } 261 | 262 | /** 263 | * @method PhaserComps.UIComponents.UIComponentPrototype#_setupState 264 | * @param {String} stateId state id to setup 265 | * @private 266 | */ 267 | _setupState(stateId) { 268 | if (this._clip) { 269 | this._clip.setState(stateId); 270 | 271 | // update textfields 272 | for (let textKey in this._texts) { 273 | let textField = this._clip.getChildText(textKey); 274 | if (textField) { 275 | textField.text = this._texts[textKey]; 276 | } 277 | } 278 | } 279 | 280 | this.emit(_EVENT_STATE); 281 | } 282 | 283 | /** 284 | * Parent state change listener 285 | * @method PhaserComps.UIComponents.UIComponentPrototype#_onEventState 286 | * @private 287 | */ 288 | _onEventState() { 289 | this._clipUpdate(); 290 | } 291 | } -------------------------------------------------------------------------------- /jsfl/ExportToPhaser.jsfl: -------------------------------------------------------------------------------- 1 | (function (global) { 2 | var JSON = {}; 3 | JSON.prettyPrint = false; 4 | JSON.stringify = function (obj) { 5 | return _internalStringify(obj, 0); 6 | }; 7 | 8 | function _prepareString(str) { 9 | return str 10 | //.replace(/\b/g, '\\' + 'b') 11 | .replace(/\\/g, '\\' + '\\') 12 | .replace(/\t/g, '\\' + 't') 13 | .replace(/\n/g, '\\' + 'n') 14 | .replace(/\f/g, '\\' + 'f') 15 | .replace(/\r/g, '\\' + 'r') 16 | .replace(/"/g, '\\' + '"') 17 | } 18 | function _internalStringify(obj, depth, fromArray) { 19 | var t = typeof (obj); 20 | if (t !== "object" || obj === null) { 21 | // simple data type 22 | if (t === "string") return '"' + _prepareString(obj) + '"'; 23 | return String(obj); 24 | } else { 25 | // recurse array or object 26 | var n, v, json = [], arr = (obj && obj.constructor === Array); 27 | var joinString, bracketString, firstPropString; 28 | if (JSON.prettyPrint) { 29 | joinString = ",\n"; 30 | bracketString = "\n"; 31 | for (var i = 0; i < depth; ++i) { 32 | joinString += "\t"; 33 | bracketString += "\t"; 34 | } 35 | joinString += "\t";//one extra for the properties of this object 36 | firstPropString = bracketString + "\t"; 37 | } else { 38 | joinString = ","; 39 | firstPropString = bracketString = ""; 40 | } 41 | for (n in obj) { 42 | v = obj[n]; 43 | t = typeof (v); 44 | // Ignore functions 45 | if (t === "function") continue; 46 | if (t === "string") v = '"' + _prepareString(v) + '"'; 47 | else if (t === "object" && v !== null) v = _internalStringify(v, depth + 1, arr); 48 | json.push((arr ? "" : '"' + n + '":') + String(v)); 49 | } 50 | return (fromArray || depth === 0 ? "" : bracketString) + (arr ? "[" : "{") + firstPropString + json.join(joinString) + bracketString + (arr ? "]" : "}"); 51 | } 52 | } 53 | 54 | JSON.parse = function (str) { 55 | if (str === "") str = '""'; 56 | eval("var p=" + str + ";"); // jshint ignore:line 57 | return p; 58 | }; 59 | 60 | global.JSON = JSON; 61 | 62 | }(window)); 63 | 64 | // For debugging 65 | function extractObject(obj, prefix) { 66 | if (prefix === undefined) prefix = ''; 67 | prefix += '-'; 68 | for (var key in obj) { 69 | try { 70 | // TODO recursive parse 71 | fl.trace(prefix + ' ' + key + ': ' + obj[key]) 72 | } catch (e) {} 73 | } 74 | } 75 | 76 | function roundToFract2(value) { 77 | return parseFloat(parseFloat(value).toFixed(2)); 78 | } 79 | 80 | function roundToFract4(value) { 81 | return parseFloat(parseFloat(value).toFixed(4)); 82 | } 83 | 84 | function getLibItemByName(name) { 85 | for (var i in lib.items) { 86 | var item = lib.items[i]; 87 | if (item.name === name) 88 | return item; 89 | } 90 | } 91 | 92 | 93 | var SKIP_LAYER_TYPES = [ 94 | 'guide', 95 | 'folder' 96 | ]; 97 | var ZONE_ELEMENT_NAMES = [ 98 | 'HIT_ZONE', 99 | 'DIMENSIONS' 100 | ]; 101 | var ZONE_ELEMENT_PREFIX = 'DIMENSIONS_'; 102 | var STATE_IDS_LAYER_NAME = 'STATE_IDS'; 103 | 104 | 105 | var dom = fl.getDocumentDOM(); 106 | var lib = dom.library; 107 | var dirUrl = fl.browseForFolderURL("Select folder to save JSON and images"); 108 | fl.outputPanel.clear(); 109 | 110 | //fl.configDirectory 111 | //var dirUrl = 'file:///C|/WORK/Citadels/phaser-test/assets/windows/json'; 112 | var images = {}; 113 | var nextChildId = 1; 114 | 115 | PhaserExporter = function(libraryItem, rootObject) { 116 | this.libraryItem = libraryItem; 117 | if (rootObject) { 118 | this.rootObject = rootObject; 119 | } else { 120 | this.id = libraryItem.linkageClassName; 121 | this.rootObject = { 122 | id: this.id 123 | } 124 | } 125 | this.children = []; 126 | this.maskLayersByChildId = {}; 127 | this.states = {}; 128 | this.stateFramesById = {}; 129 | this.stateIdsByFrameIndex = {}; 130 | this.rootObject['children'] = this.children; 131 | this.rootObject['states'] = this.states; 132 | this.rootObject['type'] = 'component'; 133 | }; 134 | 135 | PhaserExporter.prototype.getStateObjectById = function(stateId) { 136 | if (this.states.hasOwnProperty(stateId)) 137 | return this.states[stateId]; 138 | var stateObject = {}; 139 | this.states[stateId] = stateObject; 140 | return stateObject; 141 | }; 142 | 143 | PhaserExporter.prototype.getStateObjectByFrameIndex = function(frameIndex) { 144 | var stateId = this.stateIdsByFrameIndex[frameIndex]; 145 | return this.states[stateId]; 146 | }; 147 | 148 | PhaserExporter.prototype.parse = function() { 149 | this.generateStates(); 150 | var item, timeline, hasStates, layer, layerIndex; 151 | item = this.libraryItem; 152 | timeline = item.timeline; 153 | for (layerIndex = timeline.layerCount - 1; layerIndex >= 0; layerIndex--) { 154 | layer = timeline.layers[layerIndex]; 155 | /*if (layer.name === STATE_IDS_LAYER_NAME) { 156 | hasStates = true; // even if there is 1 state 157 | continue; 158 | }*/ 159 | if (SKIP_LAYER_TYPES.indexOf(layer.layerType) !== -1) 160 | continue; 161 | if (layer.isEmpty) 162 | continue; 163 | this.parseLayer(layer); 164 | } 165 | this.cleanup(); 166 | }; 167 | 168 | PhaserExporter.prototype.cleanup = function() { 169 | if (this.children.length === 0) { 170 | delete this.rootObject.children; 171 | } 172 | }; 173 | 174 | 175 | PhaserExporter.prototype.parseLayer = function(layer) { 176 | var frame, frameIndex, frameElement; 177 | var isDynamic = false; 178 | for (frameIndex in layer.frames) { 179 | frame = layer.frames[frameIndex]; 180 | if (frame.duration < layer.frameCount) { 181 | isDynamic = true; 182 | } 183 | if (frame.elements.length === 1) { 184 | frameElement = frame.elements[0]; 185 | break; 186 | } else if (frame.elements.length > 1) { 187 | fl.trace("WARN! more than 1 element at frame " + frameIndex + ' at layer ' + layer.name); 188 | } 189 | } 190 | if (!frameElement) { 191 | return; // WHY? 192 | } 193 | 194 | var child = this.createChildFromElement(frameElement); 195 | if (isDynamic) { 196 | for (frameIndex in layer.frames) { 197 | frame = layer.frames[frameIndex]; 198 | if (frame.elements.length === 0) continue; 199 | var stateObject = this.getStateObjectByFrameIndex(frameIndex); 200 | var stateChildObject = {}; 201 | stateObject[child.childId] = stateChildObject; 202 | 203 | var element = frame.elements[0]; 204 | this.collectCommonElementParams(element, stateChildObject); 205 | this.collectExtendedElementParams(element, stateChildObject, true); 206 | } 207 | } else { 208 | this.collectCommonElementParams(frameElement, child); 209 | } 210 | 211 | if (layer.layerType === 'masked') { 212 | this.maskLayersByChildId[child.childId] = layer.parentLayer; 213 | } else if (layer.layerType === 'mask') { 214 | child.masking = []; 215 | for (var maskedChildId in this.maskLayersByChildId) { 216 | if (layer === this.maskLayersByChildId[maskedChildId]) { 217 | child.masking.push(maskedChildId); 218 | } 219 | } 220 | } 221 | 222 | this.children.push(child); 223 | }; 224 | 225 | PhaserExporter.prototype.createChildFromElement = function(element) { 226 | var childObject = { 227 | childId: String(nextChildId++) 228 | }; 229 | this.collectExtendedElementParams(element, childObject, false); 230 | return childObject; 231 | }; 232 | 233 | /** 234 | * 235 | * @param {Element} element 236 | * @param {Object} target 237 | * @param {Boolean} forState 238 | */ 239 | PhaserExporter.prototype.collectExtendedElementParams = function(element, target, forState) { 240 | if (element.elementType === 'text') { 241 | this.collectTextElementParams(element, target, forState); 242 | } else if (element.elementType === 'shape') { 243 | this.collectShapeParams(element, target, forState); 244 | } else if (element.elementType === 'instance') { 245 | var libItem = element.libraryItem; 246 | if (libItem.itemType === 'bitmap') { 247 | this.collectBitmapParams(element, target, forState); 248 | } else if (libItem.itemType === 'movie clip') { 249 | if (!forState) { 250 | if (element.name) { 251 | target.key = element.name; 252 | } 253 | if (ZONE_ELEMENT_NAMES.indexOf(element.name) !== -1 || element.name.indexOf(ZONE_ELEMENT_PREFIX) === 0) { 254 | this.collectHitZoneParams(element, target, forState); 255 | } else { 256 | var childExporter = new PhaserExporter(libItem, target); 257 | childExporter.parse(); 258 | } 259 | 260 | } 261 | } 262 | } 263 | }; 264 | 265 | PhaserExporter.prototype.collectHitZoneParams = function(element, target, forState) { 266 | target.type = 'zone'; 267 | target.width = roundToFract4(element.width); 268 | target.height = roundToFract4(element.height); 269 | }; 270 | 271 | /** 272 | * 273 | * @param {Shape} element 274 | * @param {Object} target 275 | * @param {Boolean} forState 276 | */ 277 | PhaserExporter.prototype.collectShapeParams = function(element, target, forState) { 278 | var color; 279 | var isSolid = false; 280 | var alpha = 1; 281 | for (var contourIndex in element.contours) { 282 | var contour = element.contours[contourIndex]; 283 | if (!contour.fill || contour.fill.style === 'noFill') { 284 | continue; 285 | } 286 | if (contour.fill.style === 'bitmap') { 287 | if (forState) { 288 | return; // TODO update tileSprite scale? 289 | } 290 | target.type = 'tileSprite'; 291 | target.width = element.width; 292 | target.height = element.height; 293 | target.image = contour.fill.bitmapPath; 294 | // add to images to save list 295 | images[target.image] = getLibItemByName(target.image); 296 | return; 297 | } else if (contour.fill.style === 'solid') { 298 | color = parseInt(contour.fill.color.substr(1, 6), 16); 299 | isSolid = true; 300 | if (contour.fill.color.length > 7) { 301 | alpha = roundToFract2(parseInt(contour.fill.color.substr(7, 2), 16) / 256); 302 | } 303 | } 304 | } 305 | if (!isSolid) { 306 | return; 307 | } 308 | if (alpha !== 1) { 309 | target.alpha = alpha; 310 | } 311 | if (forState) { 312 | return; 313 | } 314 | target.color = color; 315 | target.type = 'polygon'; 316 | 317 | 318 | var lines = []; 319 | for (var edgeIndex in element.edges) { 320 | var edge = element.edges[edgeIndex]; 321 | var points = element.getCubicSegmentPoints(edge.cubicSegmentIndex); 322 | var startPoint = points[0]; 323 | var finishPoint = points[3]; 324 | var line = [startPoint.x, startPoint.y, finishPoint.x, finishPoint.y]; 325 | lines.push(line); 326 | } 327 | 328 | // sort and swap line points 329 | for (var i = 0; i < lines.length - 1; i++) { 330 | line = lines[i]; 331 | for (var j = i + 1; j < lines.length - 1; j++) { 332 | var nextLine = lines[j]; 333 | var lineFound = false; 334 | if (nextLine[0] === line[2] && nextLine[1] === line[3]) { 335 | lineFound = true; 336 | } else if (nextLine[0] === line[0] && nextLine[1] === line[1]) { 337 | lineFound = true; 338 | nextLine.push(nextLine[0]); 339 | nextLine.push(nextLine[1]); 340 | nextLine.splice(0, 2); 341 | } 342 | if (lineFound && j !== i + 1) { 343 | // swap 344 | var tempLine = nextLine; 345 | lines[j] = lines[i + 1]; 346 | lines[i + 1] = tempLine; 347 | break; 348 | } 349 | } 350 | } 351 | 352 | var lastLine; 353 | var vertices = [ // first vertex 354 | lines[0][0] - element.x, 355 | lines[0][1] - element.y 356 | ]; 357 | 358 | for (var lineIndex in lines) { 359 | line = lines[lineIndex]; // other vertices 360 | if (lastLine && lastLine[2] === line[2] && lastLine[3] === line[3]) { 361 | // sometimes equal vertices happen 362 | continue; 363 | } 364 | vertices.push(line[2] - element.x, line[3] - element.y); 365 | lastLine = line; 366 | } 367 | target.vertices = vertices; 368 | }; 369 | 370 | PhaserExporter.prototype.collectCommonElementParams = function(element, target) { 371 | if (roundToFract4(element.x) !== 0) { 372 | target.x = target.x || 0; 373 | target.x += roundToFract4(element.x); 374 | } 375 | if (roundToFract4(element.y) !== 0) { 376 | target.y = target.y || 0; 377 | target.y += roundToFract4(element.y); 378 | } 379 | if (roundToFract4(element.scaleX) !== 1) target.scaleX = roundToFract4(element.scaleX); 380 | if (roundToFract4(element.scaleY) !== 1) target.scaleY = roundToFract4(element.scaleY); 381 | 382 | // hacks for flip (animate uses skew instead of flip sometimes) 383 | if (roundToFract2(element.skewY) !== 0 && roundToFract2(element.skewX) === roundToFract2(-element.skewY)) { 384 | if (element.skewX < 0) { 385 | target.angle = element.skewY; 386 | target.scaleY = target.scaleY || 1; 387 | target.scaleY *= -1; 388 | } else { 389 | target.angle = element.skewX; 390 | target.scaleX = target.scaleX || 1; 391 | target.scaleX *= -1; 392 | } 393 | 394 | } 395 | if (element.rotation !== element.skewX && (element.skewX === 180 || element.skewX === -180)) { 396 | target.scaleY = target.scaleY || 1; 397 | target.scaleY *= -1; 398 | } 399 | if (element.rotation !== element.skewX && (element.skewY === 180 || element.skewY === -180)) { 400 | target.scaleX = target.scaleX || 1; 401 | target.scaleX *= -1; 402 | } 403 | 404 | if (element.colorAlphaPercent !== undefined && element.colorAlphaPercent !== 100) 405 | target.alpha = element.colorAlphaPercent / 100; 406 | if (element.rotation) target.angle = element.rotation; 407 | }; 408 | 409 | PhaserExporter.prototype.collectBitmapParams = function(element, target, forState) { 410 | var libItem = element.libraryItem; 411 | var imageName = libItem.name; 412 | // for saving image to file later 413 | images[imageName] = libItem; 414 | 415 | if (!forState) { 416 | target.type = 'image'; 417 | target['image'] = imageName; 418 | } 419 | }; 420 | 421 | PhaserExporter.prototype.collectTextElementParams = function(element, target, forState) { 422 | var attrs = element.textRuns[0].textAttrs; 423 | var style = {}; 424 | 425 | target.y = target.y || 0; 426 | // magical flash textfield offset 427 | target.y += 2; 428 | 429 | if (attrs.alignment === 'center') { 430 | style.align = 'center'; 431 | target.x = target.x || 0; 432 | target.x += element.width / 2; 433 | } else if (attrs.alignment === 'right') { 434 | style.align = 'right'; 435 | target.x = target.x || 0; 436 | target.x += element.width; 437 | } else { 438 | target.x = target.x || 0; 439 | // magical flash textfield offset 440 | target.x += 2; 441 | } 442 | 443 | if (forState) 444 | return; 445 | 446 | target.type = element.textType === 'input' ? 'input_text' : 'text'; 447 | //target.width = element.width; 448 | //target.height = element.height; 449 | if (element.name) 450 | target.key = element.name; 451 | var text = element.getTextString(); 452 | if (text) 453 | target.text = text; 454 | 455 | 456 | target.textStyle = style; 457 | if (element.lineType === 'single line') 458 | style.maxLines = 1; 459 | 460 | //style.fixedWidth = element.width; 461 | //style.fixedHeight = element.height; 462 | style.wordWrap = { width: element.width }; 463 | style.color = attrs.fillColor; // TODO extract alpha 464 | style.fontFamily = attrs.face; 465 | style.fontSize = attrs.size; 466 | 467 | // bold and italic stuff 468 | var fontStyles = []; 469 | if (attrs.bold) fontStyles.push('bold'); 470 | if (attrs.italic) fontStyles.push('italic'); 471 | if (fontStyles.length > 0) 472 | style.fontStyle = fontStyles.join(' '); 473 | 474 | // filters stuff 475 | if (element.filters) { 476 | for (var filterIndex in element.filters) { 477 | var filter = element.filters[filterIndex]; 478 | if (filter.name === 'dropShadowFilter') { 479 | style.shadow = this.generateShadowObject(filter); 480 | } else if (filter.name === 'glowFilter' && filter.enabled === true) { 481 | style.stroke = filter.color; 482 | style.strokeThickness = filter.blurX; 483 | } 484 | } 485 | } 486 | }; 487 | 488 | PhaserExporter.prototype.generateShadowObject = function(filter) { 489 | var obj = { 490 | color: filter.color, 491 | blur: filter.blurX, 492 | fill: true 493 | }; 494 | var angle = filter.angle / 180 * Math.PI; 495 | obj.offsetX = roundToFract2(Math.cos(angle) * filter.distance); 496 | obj.offsetY = roundToFract2(Math.sin(angle) * filter.distance); 497 | return obj; 498 | }; 499 | 500 | PhaserExporter.prototype.generateStates = function() { 501 | var timeline = this.libraryItem.timeline; 502 | for (var layerIndex in timeline.layers) { 503 | var layer = timeline.layers[layerIndex]; 504 | if (layer.name === STATE_IDS_LAYER_NAME) { 505 | for (var frameIndex in layer.frames) { 506 | var frame = layer.frames[frameIndex]; 507 | if (parseInt(frameIndex) !== frame.startFrame) { 508 | continue; // TODO parse state change animation 509 | } 510 | if (!frame.name) 511 | continue; 512 | var stateObject = this.getStateObjectById(frame.name); 513 | this.stateFramesById[frame.name] = frameIndex; 514 | this.stateIdsByFrameIndex[frameIndex] = frame.name; 515 | } 516 | return; // only one frame must have state ids! 517 | } 518 | } 519 | }; 520 | 521 | PhaserExporter.prototype.saveToFiles = function() { 522 | var fileContent = JSON.stringify(this.rootObject); 523 | var fileURI = dirUrl + '/' + this.id + '.json'; 524 | FLfile.write(fileURI, fileContent); 525 | }; 526 | 527 | 528 | fl.trace('--- Script start'); 529 | for (var symbolIndex in lib.items) { 530 | var libraryItem = lib.items[symbolIndex]; 531 | if (libraryItem.linkageClassName) { 532 | var exporter = new PhaserExporter(libraryItem); 533 | exporter.parse(); 534 | exporter.saveToFiles(); 535 | } 536 | } 537 | 538 | for (var imageName in images) { 539 | var imageItem = images[imageName]; 540 | var imageFileName = dirUrl + '/' + imageName; 541 | var slashIndex = imageFileName.lastIndexOf('/'); 542 | if (slashIndex !== -1) { 543 | var subDirURI = imageFileName.substring(0, slashIndex); 544 | FLfile.createFolder(subDirURI); 545 | } 546 | var fileURI = imageFileName + '.png'; 547 | imageItem.exportToFile(fileURI); 548 | } 549 | 550 | fl.trace('--- script complete'); 551 | 552 | 553 | -------------------------------------------------------------------------------- /src/clip/ComponentClip.js: -------------------------------------------------------------------------------- 1 | import Phaser from "phaser"; 2 | 3 | const TYPE_IMAGE = "image"; 4 | const TYPE_COMPONENT = "component"; 5 | const TYPE_TEXT = "text"; 6 | const TYPE_TILE_SPRITE = "tileSprite"; 7 | const TYPE_POLYGON = "polygon"; 8 | const TYPE_ZONE = "zone"; 9 | 10 | 11 | /** 12 | * @typedef {Object} PhaserComps.ComponentClip.StateConfig 13 | * Component state config object, generated by jsfl exporter 14 | * @memberOf PhaserComps.ComponentClip 15 | * @property {number} [x=0] x coordinate of component 16 | * @property {number} [y=0] y coordinate of component 17 | * @property {number} [scaleX=1] x scale of component 18 | * @property {number} [scaleY=1] y scale of component 19 | * @property {number} [angle=0] angle of component 20 | * @property {Number} [alpha=1] Opacity of component (`0` to `1`). 21 | */ 22 | 23 | /** 24 | * @typedef {Object} PhaserComps.ComponentClip.ComponentConfig 25 | * @description 26 | * Component Config object, generated by jsfl exporter 27 | * @memberOf PhaserComps.ComponentClip 28 | * @property {String} type supported types are 29 | * `image`, `component`, `text`, `tileSprite`, `polygon`, `zone` 30 | * @property {Array} [children] component children list 31 | * @property {String} [childId] unique component id, used by {@link StateManager} 32 | * @property {String} [key] key of component to find it with {@link UIComponentPrototype}. 33 | * Must be unique inside one state 34 | * @property {String} [image] Texture frame name. Only for component types `image` and `tileSprite` 35 | * @property {Phaser.GameObjects.TextStyle} [style] text style object, used only for `text` type 36 | * @property {Object} [states] object keys are component ids to be enabled 37 | * at the specified state, and the StateConfig is position and scale params to setup for component 38 | * @property {Array} masking List of component ids, that will be masked by this component. 39 | * Currently only polygon masks available. 40 | * @property {Number} [x=0] x coordinate of component 41 | * @property {Number} [y=0] y coordinate of component 42 | * @property {Number} [scaleX=1] x scale of component 43 | * @property {Number} [scaleY=1] y scale of component 44 | * @property {Number} [angle=0] angle of component in degrees 45 | * @property {Number} [alpha=1] Opacity of component (`0` to `1`). 46 | * @property {Number} [width] Component width. 47 | * only for `zone` type 48 | * @property {Number} [height] Component height. 49 | * only for `zone` type 50 | * @property {Array.} [vertices] Array of polygon vertices coords, `x` and `y` interleaving. 51 | * Only for `polygon` type 52 | * @property {int} [color] polygon color. 53 | * Only for `polygon` type 54 | */ 55 | 56 | /** 57 | * @class ComponentClip 58 | * @memberOf PhaserComps 59 | * @classdesc 60 | * @extends Phaser.GameObjects.Container 61 | * Component clip is Phaser Container instance. 62 | * Builds itself with provided jsfl-generated config object. 63 | * 64 | * Clip supports state switching. Best if controlled by 65 | * [UIComponentPrototype]{@link PhaserComps.UIComponents.UIComponentPrototype} instance 66 | * 67 | * @see PhaserComps.UIComponents.UIComponentPrototype 68 | * 69 | * @param {Phaser.Scene} scene Phaser scene to create component at 70 | * @param {ComponentConfig} config jsfl-generated config object 71 | * @param {Array|String} textures texure name or Array of texture names, where component should find its texture frames 72 | */ 73 | export default class ComponentClip extends Phaser.GameObjects.Container { 74 | constructor(scene, config, textures) { 75 | super(scene, 0, 0); 76 | this.childComponentClips = []; 77 | 78 | /** 79 | * component config object 80 | * @type {Object} 81 | * */ 82 | this._config = config; 83 | 84 | /** 85 | * component key 86 | * @type {String} 87 | */ 88 | this._key = config.key; 89 | 90 | /** 91 | * list of texture names to use in this component 92 | * @type {Array} 93 | * */ 94 | this._textures = Array.isArray(textures) ? textures : [textures]; 95 | 96 | /** 97 | * Texture frames to texture names map 98 | * @type {Object} 99 | */ 100 | this.imageFramesMap = {}; 101 | 102 | /** 103 | * Component's state manager instance. Helps to switch states and find active children by key 104 | * @type {StateManager} 105 | * */ 106 | this._stateManager = new StateManager(this, config); 107 | 108 | this._childrenById = {}; 109 | 110 | this._createImagesMap(textures); 111 | this._parseConfig(); 112 | } 113 | 114 | /** 115 | * @public 116 | * @method PhaserComps.ComponentClip#getStateConfig 117 | * @description 118 | * Get raw state config object by state id, if exists 119 | * @param {String} stateId state id 120 | */ 121 | getStateConfig(stateId) { 122 | return this._stateManager.getStateConfigById(stateId); 123 | } 124 | 125 | /** 126 | * @public 127 | * @method PhaserComps.ComponentClip#getStateIds 128 | * @description 129 | * Component state ids list. 130 | * @returns {Array} 131 | */ 132 | getStateIds() { 133 | return this._stateManager.stateIds; 134 | } 135 | /** 136 | * @public 137 | * @method PhaserComps.ComponentClip#setState 138 | * @description 139 | * Switch component view to specified stateId, if such stateId exists. 140 | * Do not use it manually, if you are using UIComponentPrototype to control the view 141 | * 142 | * @param {String} stateId state id to switch to 143 | * @param {Boolean} [force=false] if true, state will be setup again even if stateId was not changed 144 | */ 145 | setState(stateId, force) { 146 | this._stateManager.setState(stateId, force); 147 | } 148 | 149 | /** 150 | * @public 151 | * @method PhaserComps.ComponentClip#applyChildParams 152 | * @description 153 | * Apply child params 154 | * @param {String} childId 155 | * @param {StateConfig} params 156 | */ 157 | applyChildParams(childId, params) { 158 | if (!this._childrenById.hasOwnProperty(childId)) { 159 | return; 160 | } 161 | ComponentClip._setupCommonParams(this._childrenById[childId], params); 162 | } 163 | 164 | /** 165 | * @public 166 | * @method PhaserComps.ComponentClip#getChildClip 167 | * @description returns current active component child view instance 168 | * @param {String} key child key 169 | * @returns {PhaserComps.ComponentClip|Phaser.GameObjects.GameObject} 170 | */ 171 | getChildClip(key) { 172 | return this._stateManager.getActiveComponentByKey(key); 173 | } 174 | 175 | /** 176 | * @public 177 | * @method PhaserComps.ComponentClip#getChildText 178 | * @description returns current active component child text instance 179 | * @param {String} key child text field key 180 | * @returns {Phaser.GameObjects.Text} 181 | */ 182 | getChildText(key) { 183 | // TODO separate getter 184 | return this._stateManager.getActiveComponentByKey(key); 185 | } 186 | 187 | /** 188 | * @public 189 | * @method PhaserComps.ComponentClip#destroy 190 | * @description destroy all child GameObjects and child clips recursively 191 | * @param {Boolean} [fromScene=false] 192 | */ 193 | destroy(fromScene) { 194 | for (let child of this.childComponentClips) { 195 | child.destroy(fromScene); 196 | } 197 | super.destroy(fromScene) 198 | } 199 | 200 | /** 201 | * @method PhaserComps.ComponentClip#_createImagesMap 202 | * @description 203 | * Fill the imageFramesMap object from provided textures. 204 | * imageFramesMap used to 205 | * @param {Array} textures 206 | * @private 207 | * @ignore 208 | */ 209 | _createImagesMap(textures) { 210 | for (let textureName of textures) { 211 | const texture = this.scene.textures.get(textureName); 212 | if (!texture) { 213 | return; 214 | } 215 | const frames = texture.getFrameNames(); 216 | for (let frameName of frames) { 217 | this.imageFramesMap[frameName] = textureName; 218 | } 219 | } 220 | } 221 | 222 | /** 223 | * @method PhaserComps.ComponentClip#_parseConfig 224 | * @description 225 | * Builds component from config 226 | * @private 227 | * @ignore 228 | */ 229 | _parseConfig() { 230 | //ComponentView._setupCommonParams(this, this._config); 231 | if (this._config.hasOwnProperty("children")) { 232 | for (let childConfig of this._config.children) { 233 | this._createChildFromConfig(childConfig); 234 | } 235 | } 236 | } 237 | 238 | /** 239 | * @method PhaserComps.ComponentClip#_createChildFromConfig 240 | * @description creates child instance, depending on its type, add it to state manager 241 | * @param {ComponentConfig} config child component config object 242 | * @private 243 | * @ignore 244 | */ 245 | _createChildFromConfig(config) { 246 | let child = null; 247 | let childId = config.childId; 248 | let childKey = config.key; 249 | let addAsChild = true; 250 | if (config.type === TYPE_IMAGE) { 251 | child = this._createImageFromConfig(config); 252 | } else if (config.type === TYPE_TEXT) { 253 | child = this._createTextFromConfig(config); 254 | } else if (config.type === TYPE_TILE_SPRITE) { 255 | child = this._createTileSpriteFromConfig(config); 256 | } else if (config.type === TYPE_COMPONENT) { 257 | child = new ComponentClip(this.scene, config, this._textures); 258 | ComponentClip._setupCommonParams(child, config); 259 | } else if (config.type === TYPE_ZONE) { 260 | child = this._createHitZoneFromConfig(config); 261 | } else if (config.type === TYPE_POLYGON) { 262 | child = this._createPolygonFromConfig(config); 263 | if (config.hasOwnProperty("masking")) { 264 | let mask = child.createGeometryMask(); 265 | for (let maskedChildId of config.masking) { 266 | let maskedChild = this._childrenById[maskedChildId]; 267 | maskedChild.setMask(mask); 268 | } 269 | addAsChild = false; 270 | } 271 | } 272 | if (child === null) { 273 | //console.warn("unknown component type", config.type, config); 274 | return; 275 | } 276 | //ComponentView._setupCommonParams(child, config); 277 | this._childrenById[childId] = child; 278 | this.childComponentClips.push(child); 279 | if (addAsChild) { 280 | this.add(child); 281 | } 282 | this._stateManager.addComponent(child, childId, childKey); 283 | } 284 | 285 | /** 286 | * @description Create simple polygon with provided vertices from config 287 | * @method PhaserComps.ComponentClip#_createPolygonFromConfig 288 | * @param {PhaserComps.ComponentClip.ComponentConfig} config 289 | * @returns {Phaser.GameObjects.Graphics} 290 | * @private 291 | * @ignore 292 | */ 293 | _createPolygonFromConfig(config) { 294 | const shape = this.scene.make.graphics(); 295 | shape.fillStyle(config.color, config.hasOwnProperty("alpha") ? config.alpha : 1); 296 | shape.beginPath(); 297 | let vertices = config.vertices; 298 | let verticesLength = vertices.length; 299 | for (let i = 0; i < verticesLength; i += 2) { 300 | shape.lineTo(vertices[i], vertices[i + 1]); 301 | } 302 | shape.closePath(); 303 | shape.fillPath(); 304 | ComponentClip._setupCommonParams(shape, config); 305 | if (!config.hasOwnProperty("masking")) { 306 | this.scene.add.existing(shape); 307 | } 308 | return shape; 309 | } 310 | 311 | /** 312 | * @method PhaserComps.ComponentClip#_createTileSpriteFromConfig 313 | * @description creates Phaser.GameObjects.TileSprite by jsfl-generated config and returns it 314 | * @param {Object} config jsfl-generated TileSprite config object 315 | * @returns {Phaser.GameObjects.TileSprite} 316 | * @private 317 | * @ignore 318 | */ 319 | _createTileSpriteFromConfig(config) { 320 | const sprite = this.scene.add.tileSprite( 321 | 0, 0, 322 | config.width, config.height, 323 | this.imageFramesMap[config.image], 324 | config.image 325 | ); 326 | sprite.setOrigin(0.5, 0.5); // Animate places shape coords to center 327 | ComponentClip._setupCommonParams(sprite, config); 328 | return sprite; 329 | } 330 | 331 | /** 332 | * @method PhaserComps.ComponentClip#_createImageFromConfig 333 | * @description creates Phaser.GameObjects.Image instance by jsfl-generated config and returns it 334 | * @param {Object} config jsfl-generated Image config object 335 | * @returns {Phaser.GameObjects.Image} 336 | * @private 337 | * @ignore 338 | */ 339 | _createImageFromConfig(config) { 340 | const image = this.scene.add.image( 341 | 0, 0, 342 | this.imageFramesMap[config.image], 343 | config.image 344 | ); 345 | image.setOrigin(0); 346 | ComponentClip._setupCommonParams(image, config); 347 | return image; 348 | } 349 | 350 | /** 351 | * @method PhaserComps.ComponentClip#_createTextFromConfig 352 | * @description creates Phaser.GameObjects.Text instance by jsfl-generated config and returns it 353 | * @param {Object} config jsfl-generated Text config object 354 | * @returns {Phaser.GameObjects.Text} 355 | * @private 356 | * @ignore 357 | */ 358 | _createTextFromConfig(config) { 359 | const text = this.scene.add.text(0, 0, config.text, config.textStyle); 360 | if (config.textStyle.align === "center") { 361 | text.setOrigin(0.5, 0); 362 | } else if (config.textStyle.align === "right") { 363 | text.setOrigin(1, 0); 364 | } else { 365 | text.setOrigin(0); 366 | } 367 | ComponentClip._setupCommonParams(text, config); 368 | return text; 369 | } 370 | 371 | /** 372 | * @method PhaserComps.ComponentClip#_createHitZoneFromConfig 373 | * @description creates Phaser.GameObjects.Zone instance by jsfl-generated config and returns it 374 | * @param {Object} config jsfl-generated Zone config object 375 | * @return {Phaser.GameObjects.Zone} 376 | * @private 377 | * @ignore 378 | */ 379 | _createHitZoneFromConfig(config) { 380 | return this.scene.add.zone( 381 | config.x || 0, 382 | config.y || 0, 383 | config.width, 384 | config.height 385 | ).setOrigin(0); 386 | } 387 | 388 | /** 389 | * @memberOf ComponentClip 390 | * @description setup common game object params from jsfl-generated config 391 | * @param {*} component 392 | * @param {Object} config 393 | * @ignore 394 | */ 395 | static _setupCommonParams(component, config) { 396 | let x = config.x || 0; 397 | let y = config.y || 0; 398 | let scaleX = config.scaleX || 1; 399 | let scaleY = config.scaleY || 1; 400 | let angle = config.angle || 0; 401 | let alpha = config.hasOwnProperty("alpha") ? config.alpha : 1; 402 | component.x = x; 403 | component.y = y; 404 | component.scaleX = scaleX; 405 | component.scaleY = scaleY; 406 | component.angle = angle; 407 | component.alpha = alpha; 408 | } 409 | } 410 | 411 | class State { 412 | /** 413 | * @class State 414 | * @classdesc State config decorator, for 415 | * [StateManager]{@link PhaserComps.ComponentClip.StateManager} internal use only 416 | * @param {PhaserComps.ComponentClip.StateConfig} config 417 | * @memberOf PhaserComps.ComponentClip 418 | */ 419 | constructor(config) { 420 | /** 421 | * State config object 422 | * @type {PhaserComps.ComponentClip.StateConfig} 423 | */ 424 | this.config = config; 425 | /** 426 | * Component ids, that are only active in this state 427 | * @type {Array} 428 | */ 429 | this.componentIds = []; 430 | this.componentIds = Object.keys(config); 431 | /*for (let componentId in config) { 432 | this.componentIds.push(componentId); 433 | }*/ 434 | } 435 | } 436 | 437 | class StateManager { 438 | /** 439 | * @class StateManager 440 | * @memberOf PhaserComps.ComponentClip 441 | * @classdesc 442 | * For [ComponentClip]{@link PhaserComps.ComponentClip} 443 | * internal use only 444 | * 445 | * Shows or hides component view instances depending on which state is active. 446 | * Helps to get current active components by keys. 447 | * 448 | * @param {PhaserComps.ComponentClip} clip state manager creator clip instance 449 | * @param {Object} config Main component states config object 450 | * 451 | */ 452 | constructor(clip, config) { 453 | /** 454 | * 455 | * @type {PhaserComps.ComponentClip} 456 | * @private 457 | */ 458 | this._clip = clip; 459 | 460 | /** 461 | * 462 | * @type {Array} 463 | * @private 464 | */ 465 | this._dynamicChildrenIds = []; 466 | 467 | /** 468 | * 469 | * @type {Object} 470 | * @private 471 | */ 472 | this._states = {}; 473 | 474 | /** 475 | * State ids array 476 | * @type {Array} 477 | */ 478 | this.stateIds = []; 479 | 480 | /** 481 | * 482 | * @type {Object} 483 | * @private 484 | */ 485 | this._components = {}; 486 | 487 | /** 488 | * 489 | * @type {State} 490 | * @private 491 | */ 492 | this._currentState = null; 493 | 494 | /** 495 | * 496 | * @type {String} 497 | * @private 498 | */ 499 | 500 | this._currentStateId = null; 501 | /** 502 | * 503 | * @type {Object} 504 | * @private 505 | */ 506 | this._componentKeys = {}; 507 | 508 | this._residentComponentsByKey = {}; 509 | let idsArray = []; 510 | for (let stateId in config.states) { 511 | this.stateIds.push(stateId); 512 | let state = new State(config.states[stateId]); 513 | this._states[stateId] = state; 514 | idsArray.push(...state.componentIds); 515 | } 516 | const uniq = [] 517 | for (let id of idsArray) { 518 | if (!uniq.includes(id)) { 519 | uniq.push(id); 520 | } 521 | } 522 | this._dynamicChildrenIds = uniq; 523 | } 524 | 525 | /** 526 | * @public 527 | * @method PhaserComps.ComponentClip.StateManager#getStateConfigById 528 | * @description 529 | * Get raw state config object by state id 530 | * @param {String} stateId state id 531 | */ 532 | getStateConfigById(stateId) { 533 | if (!this._states.hasOwnProperty(stateId)) { 534 | return null; 535 | } 536 | return this._states[stateId].config; 537 | } 538 | 539 | /** 540 | * @method PhaserComps.ComponentClip.StateManager#addComponent 541 | * @param {PhaserComps.ComponentClip|Phaser.GameObjects.GameObject} component 542 | * component view instance, it may be text, image, sprite, or ComponentView instance 543 | * @param {String} childId unique child id from component config 544 | * @param {String} [childKey] child key from component config 545 | */ 546 | addComponent(component, childId, childKey) { 547 | 548 | if (!this._dynamicChildrenIds.includes(childId)) { 549 | if (typeof childKey !== "undefined") { 550 | this._residentComponentsByKey[childKey] = component; 551 | } 552 | } else { 553 | this._components[childId] = component; 554 | if (typeof childKey !== "undefined") { 555 | this._componentKeys[childId] = childKey; 556 | } 557 | } 558 | } 559 | 560 | /** 561 | * Setup state with provided stateId, if exists 562 | * @method PhaserComps.ComponentClip.StateManager#setState 563 | * @param {String} stateId state id to setup 564 | * @param {Boolean} force if true, update state even if stateId was not changed 565 | */ 566 | setState(stateId, force) { 567 | if (this._currentStateId === stateId && !force) { 568 | return; 569 | } 570 | if (!this._states.hasOwnProperty(stateId)) { 571 | return; 572 | } 573 | this._currentStateId = stateId; 574 | this._currentState = this._states[stateId]; 575 | this.setupState(); 576 | } 577 | 578 | /** 579 | * Get component with provided key, if exists and is present in current state 580 | * @method PhaserComps.ComponentClip.StateManager#getActiveComponentByKey 581 | * @param {String} key Component key to get 582 | * @returns {ComponentClip|Phaser.GameObjects.Image|Phaser.GameObjects.TileSprite|Phaser.GameObjects.Text|null} 583 | */ 584 | getActiveComponentByKey(key) { 585 | if (this._residentComponentsByKey.hasOwnProperty(key)) { 586 | return this._residentComponentsByKey[key]; 587 | } 588 | if (this._currentState === null) { 589 | return null; 590 | } 591 | for (let i in this._currentState.componentIds) { 592 | let id = this._currentState.componentIds[i]; 593 | if (this._componentKeys[id] === key) { 594 | return this._components[id]; 595 | } 596 | } 597 | return null; 598 | } 599 | 600 | /** 601 | * Show state components, apply its' state positions, and hide non-state components 602 | * @method PhaserComps.ComponentClip.tateManager#setupState 603 | * @protected 604 | */ 605 | setupState() { 606 | let idsToShow = this._currentState.componentIds; 607 | for (let id of this._dynamicChildrenIds) { 608 | let component = this._components[id]; 609 | if (idsToShow.includes(id)) { 610 | component.visible = true; 611 | ComponentClip._setupCommonParams(component, this._currentState.config[id]); 612 | } else { 613 | component.visible = false; 614 | } 615 | } 616 | } 617 | } -------------------------------------------------------------------------------- /dist/phaser-ui-comps.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e(require("phaser")):"function"==typeof define&&define.amd?define(["phaser"],e):"object"==typeof exports?exports.PhaserComps=e(require("phaser")):t.PhaserComps=e(t.Phaser)}(window,(function(t){return function(t){var e={};function n(r){if(e[r])return e[r].exports;var i=e[r]={i:r,l:!1,exports:{}};return t[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}return n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var i in t)n.d(r,i,function(e){return t[e]}.bind(null,i));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=10)}([function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function t(t,e){for(var n=0;n1&&void 0!==arguments[1])||arguments[1];e&&this.unlock(),"string"==typeof t?o.push(t):t.forEach((function(t){return o.push(t)})),i=!0}},{key:"unlock",value:function(){o.length=0,i=!1}},{key:"register",value:function(t){a[t.lockId]=t}},{key:"unregister",value:function(t){a[t.lockId]&&(a[t.lockId]=null,delete a[t.lockId])}},{key:"check",value:function(t){return!i||-1!==o.indexOf(t)}},{key:"getById",value:function(t){return a[t]}},{key:"getBoundsById",value:function(t){var e=this.getById(t);return e?e.lockClipBounds:null}},{key:"getClipById",value:function(t){var e=this.getById(t);return e?e.lockClip:null}}]),t}();e.default=u},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=u(n(5)),i=u(n(11)),o=u(n(17)),a=u(n(3));function u(t){return t&&t.__esModule?t:{default:t}}var l={ComponentClip:r.default,UIComponents:i.default,Plugin:o.default,UIManager:a.default};e.default=l},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r,i=function(){function t(t,e){for(var n=0;nthis._dragBounds.maxX&&(i=this._dragBounds.maxX),othis._dragBounds.maxY&&(o=this._dragBounds.maxY),this.emit("event_drag",i,o)}}},{key:"_setupInteractive",value:function(t){r(e.prototype.__proto__||Object.getPrototypeOf(e.prototype),"_setupInteractive",this).call(this,t),this._dragZone=t,t.scene.input.setDraggable(t,!0),t.scene.input.on("dragstart",this._onDragStart,this),t.scene.input.on("drag",this._onDrag,this)}},{key:"_removeInteractive",value:function(t){r(e.prototype.__proto__||Object.getPrototypeOf(e.prototype),"_removeInteractive",this).call(this,t),this._dragZone=null,t.scene.input.setDraggable(t,!1),t.scene.input.removeListener("dragstart",this._onDragStart),t.scene.input.removeListener("drag",this._onDrag)}}]),e}(o.default);e.default=l},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=function t(e,n,r){null===e&&(e=Function.prototype);var i=Object.getOwnPropertyDescriptor(e,n);if(void 0===i){var o=Object.getPrototypeOf(e);return null===o?void 0:t(o,n,r)}if("value"in i)return i.value;var a=i.get;return void 0!==a?a.call(r):void 0},i=function(){function t(t,e){for(var n=0;n1&&(e=1),e!==this._value&&(this._value=e,this._updateThumbFromValue())}}]),e}(o.default);e.default=c},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r,i=function(){function t(t,e){for(var n=0;nu.stepValue&&this._value