├── .prettierrc ├── project.png ├── showcase.gif ├── raw ├── dirt_01.png ├── dirt_02.png ├── dirt_03.png ├── fire_01.png ├── fire_02.png ├── flame_01.png ├── flame_02.png ├── flame_03.png ├── flame_04.png ├── flame_05.png ├── flame_06.png ├── flare_01.png ├── flare_02.png ├── flare_03.png ├── flare_04.png ├── flare_05.png ├── flare_06.png ├── flare_07.png ├── light_01.png ├── light_02.png ├── light_03.png ├── magic_01.png ├── magic_02.png ├── magic_03.png ├── magic_04.png ├── magic_05.png ├── slash_01.png ├── slash_02.png ├── slash_03.png ├── slash_04.png ├── smoke_01.png ├── smoke_02.png ├── smoke_03.png ├── smoke_04.png ├── smoke_05.png ├── smoke_06.png ├── smoke_07.png ├── smoke_08.png ├── smoke_09.png ├── smoke_10.png ├── smoke_11.png ├── spark_01.png ├── spark_02.png ├── spark_03.png ├── spark_04.png ├── spark_05.png ├── spark_06.png ├── spark_07.png ├── star_01.png ├── star_02.png ├── star_03.png ├── star_04.png ├── star_05.png ├── star_06.png ├── star_07.png ├── star_08.png ├── star_09.png ├── trace_01.png ├── trace_02.png ├── trace_03.png ├── trace_04.png ├── trace_05.png ├── trace_06.png ├── trace_07.png ├── twirl_01.png ├── twirl_02.png ├── twirl_03.png ├── circle_01.png ├── circle_02.png ├── circle_03.png ├── circle_04.png ├── circle_05.png ├── muzzle_01.png ├── muzzle_02.png ├── muzzle_03.png ├── muzzle_04.png ├── muzzle_05.png ├── scorch_01.png ├── scorch_02.png ├── scorch_03.png ├── scratch_01.png ├── symbol_01.png ├── symbol_02.png ├── window_01.png ├── window_02.png ├── window_03.png ├── window_04.png └── shapes.tps ├── public ├── favicon.ico ├── manifest.json └── index.html ├── src ├── canvas │ ├── assets │ │ └── shapes.png │ ├── scenes │ │ ├── index.ts │ │ ├── preload.ts │ │ ├── background.ts │ │ └── canvas.ts │ ├── config.ts │ ├── index.ts │ ├── game.ts │ └── utils.ts ├── constants │ ├── blendModes.ts │ ├── emitterConfig.ts │ ├── zoneEdgeSources.ts │ ├── index.ts │ ├── easing.ts │ └── frames.ts ├── stores │ ├── index.ts │ ├── emitterStore.ts │ └── editorStore.ts ├── index.tsx ├── components │ ├── AppBarMenuItem │ │ └── index.tsx │ ├── EmitterConfig │ │ ├── expansionPanelColorPicker.tsx │ │ ├── expansionPanelBlendMode.tsx │ │ ├── expansionPanelBounds.tsx │ │ ├── expansionPanelCompositeProperty.tsx │ │ ├── expansionPanelTransform.tsx │ │ ├── expansionPanelZone.tsx │ │ ├── expansionPanelOptions.tsx │ │ ├── expansionPanelXY.tsx │ │ ├── expansionPanelFrame.tsx │ │ └── index.tsx │ ├── ComplexTextField │ │ └── index.tsx │ ├── Switch │ │ └── index.tsx │ ├── ToggleComplexTextField │ │ └── index.tsx │ ├── ChangeBackgroundModal │ │ └── index.tsx │ ├── Select │ │ └── index.tsx │ ├── Editor │ │ └── index.tsx │ ├── ExportSaveProjectModal │ │ └── index.tsx │ ├── SaveProjectModal │ │ └── index.tsx │ ├── AppBar │ │ └── index.tsx │ ├── EmitterList │ │ └── index.tsx │ ├── ExportProjectModal │ │ └── index.tsx │ ├── TextField │ │ └── index.tsx │ ├── ColorPicker │ │ └── index.tsx │ ├── ImportProjectModal │ │ └── index.tsx │ ├── ComplexZone │ │ └── index.tsx │ ├── MultipleInput │ │ └── index.tsx │ ├── AppBarMenu │ │ └── index.tsx │ ├── CompositeProperty │ │ └── index.tsx │ ├── EmitterItem │ │ └── index.tsx │ ├── Zone │ │ └── index.tsx │ ├── ImportProjectFile │ │ └── index.tsx │ ├── ImportBackground │ │ └── index.tsx │ └── NewProjectModal │ │ └── index.tsx ├── withRoot.tsx ├── pages │ └── index.tsx └── utils.ts ├── tsconfig.test.json ├── tsconfig.prod.json ├── typings └── misc.d.ts ├── .vscode └── settings.json ├── .gitignore ├── CHANGELOG.md ├── .travis.yml ├── CONTRIBUTING.md ├── tsconfig.json ├── LICENSE ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── package.json ├── tslint.json ├── CODE_OF_CONDUCT.md └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/project.png -------------------------------------------------------------------------------- /showcase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/showcase.gif -------------------------------------------------------------------------------- /raw/dirt_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/dirt_01.png -------------------------------------------------------------------------------- /raw/dirt_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/dirt_02.png -------------------------------------------------------------------------------- /raw/dirt_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/dirt_03.png -------------------------------------------------------------------------------- /raw/fire_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/fire_01.png -------------------------------------------------------------------------------- /raw/fire_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/fire_02.png -------------------------------------------------------------------------------- /raw/flame_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/flame_01.png -------------------------------------------------------------------------------- /raw/flame_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/flame_02.png -------------------------------------------------------------------------------- /raw/flame_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/flame_03.png -------------------------------------------------------------------------------- /raw/flame_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/flame_04.png -------------------------------------------------------------------------------- /raw/flame_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/flame_05.png -------------------------------------------------------------------------------- /raw/flame_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/flame_06.png -------------------------------------------------------------------------------- /raw/flare_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/flare_01.png -------------------------------------------------------------------------------- /raw/flare_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/flare_02.png -------------------------------------------------------------------------------- /raw/flare_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/flare_03.png -------------------------------------------------------------------------------- /raw/flare_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/flare_04.png -------------------------------------------------------------------------------- /raw/flare_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/flare_05.png -------------------------------------------------------------------------------- /raw/flare_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/flare_06.png -------------------------------------------------------------------------------- /raw/flare_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/flare_07.png -------------------------------------------------------------------------------- /raw/light_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/light_01.png -------------------------------------------------------------------------------- /raw/light_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/light_02.png -------------------------------------------------------------------------------- /raw/light_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/light_03.png -------------------------------------------------------------------------------- /raw/magic_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/magic_01.png -------------------------------------------------------------------------------- /raw/magic_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/magic_02.png -------------------------------------------------------------------------------- /raw/magic_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/magic_03.png -------------------------------------------------------------------------------- /raw/magic_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/magic_04.png -------------------------------------------------------------------------------- /raw/magic_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/magic_05.png -------------------------------------------------------------------------------- /raw/slash_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/slash_01.png -------------------------------------------------------------------------------- /raw/slash_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/slash_02.png -------------------------------------------------------------------------------- /raw/slash_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/slash_03.png -------------------------------------------------------------------------------- /raw/slash_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/slash_04.png -------------------------------------------------------------------------------- /raw/smoke_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/smoke_01.png -------------------------------------------------------------------------------- /raw/smoke_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/smoke_02.png -------------------------------------------------------------------------------- /raw/smoke_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/smoke_03.png -------------------------------------------------------------------------------- /raw/smoke_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/smoke_04.png -------------------------------------------------------------------------------- /raw/smoke_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/smoke_05.png -------------------------------------------------------------------------------- /raw/smoke_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/smoke_06.png -------------------------------------------------------------------------------- /raw/smoke_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/smoke_07.png -------------------------------------------------------------------------------- /raw/smoke_08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/smoke_08.png -------------------------------------------------------------------------------- /raw/smoke_09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/smoke_09.png -------------------------------------------------------------------------------- /raw/smoke_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/smoke_10.png -------------------------------------------------------------------------------- /raw/smoke_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/smoke_11.png -------------------------------------------------------------------------------- /raw/spark_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/spark_01.png -------------------------------------------------------------------------------- /raw/spark_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/spark_02.png -------------------------------------------------------------------------------- /raw/spark_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/spark_03.png -------------------------------------------------------------------------------- /raw/spark_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/spark_04.png -------------------------------------------------------------------------------- /raw/spark_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/spark_05.png -------------------------------------------------------------------------------- /raw/spark_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/spark_06.png -------------------------------------------------------------------------------- /raw/spark_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/spark_07.png -------------------------------------------------------------------------------- /raw/star_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/star_01.png -------------------------------------------------------------------------------- /raw/star_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/star_02.png -------------------------------------------------------------------------------- /raw/star_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/star_03.png -------------------------------------------------------------------------------- /raw/star_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/star_04.png -------------------------------------------------------------------------------- /raw/star_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/star_05.png -------------------------------------------------------------------------------- /raw/star_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/star_06.png -------------------------------------------------------------------------------- /raw/star_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/star_07.png -------------------------------------------------------------------------------- /raw/star_08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/star_08.png -------------------------------------------------------------------------------- /raw/star_09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/star_09.png -------------------------------------------------------------------------------- /raw/trace_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/trace_01.png -------------------------------------------------------------------------------- /raw/trace_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/trace_02.png -------------------------------------------------------------------------------- /raw/trace_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/trace_03.png -------------------------------------------------------------------------------- /raw/trace_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/trace_04.png -------------------------------------------------------------------------------- /raw/trace_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/trace_05.png -------------------------------------------------------------------------------- /raw/trace_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/trace_06.png -------------------------------------------------------------------------------- /raw/trace_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/trace_07.png -------------------------------------------------------------------------------- /raw/twirl_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/twirl_01.png -------------------------------------------------------------------------------- /raw/twirl_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/twirl_02.png -------------------------------------------------------------------------------- /raw/twirl_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/twirl_03.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /raw/circle_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/circle_01.png -------------------------------------------------------------------------------- /raw/circle_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/circle_02.png -------------------------------------------------------------------------------- /raw/circle_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/circle_03.png -------------------------------------------------------------------------------- /raw/circle_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/circle_04.png -------------------------------------------------------------------------------- /raw/circle_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/circle_05.png -------------------------------------------------------------------------------- /raw/muzzle_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/muzzle_01.png -------------------------------------------------------------------------------- /raw/muzzle_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/muzzle_02.png -------------------------------------------------------------------------------- /raw/muzzle_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/muzzle_03.png -------------------------------------------------------------------------------- /raw/muzzle_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/muzzle_04.png -------------------------------------------------------------------------------- /raw/muzzle_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/muzzle_05.png -------------------------------------------------------------------------------- /raw/scorch_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/scorch_01.png -------------------------------------------------------------------------------- /raw/scorch_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/scorch_02.png -------------------------------------------------------------------------------- /raw/scorch_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/scorch_03.png -------------------------------------------------------------------------------- /raw/scratch_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/scratch_01.png -------------------------------------------------------------------------------- /raw/symbol_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/symbol_01.png -------------------------------------------------------------------------------- /raw/symbol_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/symbol_02.png -------------------------------------------------------------------------------- /raw/window_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/window_01.png -------------------------------------------------------------------------------- /raw/window_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/window_02.png -------------------------------------------------------------------------------- /raw/window_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/window_03.png -------------------------------------------------------------------------------- /raw/window_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/raw/window_04.png -------------------------------------------------------------------------------- /src/canvas/assets/shapes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koreezgames/phaser3-particle-editor/HEAD/src/canvas/assets/shapes.png -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "sourceMap": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/canvas/scenes/index.ts: -------------------------------------------------------------------------------- 1 | import CanvasScene from './canvas'; 2 | import PreloadScene from './preload'; 3 | import BackgroundScene from './background'; 4 | export { CanvasScene, PreloadScene, BackgroundScene }; 5 | -------------------------------------------------------------------------------- /src/constants/blendModes.ts: -------------------------------------------------------------------------------- 1 | const blendModes = [ 2 | { text: 'NORMAL', value: 0 }, 3 | { text: 'ADD', value: 1 }, 4 | { text: 'MULTIPLY', value: 2 }, 5 | { text: 'SCREEN', value: 3 }, 6 | ]; 7 | 8 | export default blendModes; 9 | -------------------------------------------------------------------------------- /typings/misc.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'jss-preset-default'; 2 | declare module 'react-jss/*'; 3 | declare module '*.png'; 4 | declare module '*.json' { 5 | const value: any; 6 | export default value; 7 | } 8 | declare module 'react-color'; 9 | declare module 'github-buttons/dist/react'; 10 | -------------------------------------------------------------------------------- /src/canvas/config.ts: -------------------------------------------------------------------------------- 1 | export default ( 2 | height: number, 3 | width: number, 4 | scenes: (typeof Phaser.Scene)[], 5 | ) => ({ 6 | type: Phaser.WEBGL, 7 | width: width, 8 | height: height, 9 | backgroundColor: '#232222', 10 | parent: 'phaser-canvas', 11 | scene: scenes, 12 | }); 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.implicitProjectConfig.experimentalDecorators": true, 3 | "editor.codeActionsOnSave": { 4 | "source.organizeImports": false 5 | }, 6 | "editor.formatOnSave": true, 7 | "editor.rulers": [120], 8 | "tslint.autoFixOnSave": true, 9 | "javascript.format.enable": false 10 | } 11 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Phaser3 Particle Editor Changelog: 2 | 3 | ## Phaser3 Particle Editor 1.0.0-beta3 4 | 5 | ### 1.0.0-beta3 - Planned stable version 6 | 7 | - ability to set multiple images for single emitter 8 | 9 | ### 1.0.0-beta2 - Planned stable version 10 | 11 | - ability to set background image for editor 12 | 13 | ### 1.0.0-beta1 - Planned stable version 14 | 15 | - first release 16 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import editorStore, { EditorStoreProp } from './editorStore'; 2 | import emitterStore, { EmitterStoreProp } from './emitterStore'; 3 | 4 | const EMITTER_STORE = 'emitterStore'; 5 | const EDITOR_STORE = 'editorStore'; 6 | 7 | export { 8 | editorStore, 9 | emitterStore, 10 | EMITTER_STORE, 11 | EDITOR_STORE, 12 | EditorStoreProp, 13 | EmitterStoreProp, 14 | }; 15 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { Provider } from 'mobx-react'; 2 | import 'phaser'; 3 | import * as React from 'react'; 4 | import * as ReactDOM from 'react-dom'; 5 | import Index from './pages/index'; 6 | import { editorStore, emitterStore } from './stores'; 7 | 8 | const stores = { 9 | editorStore, 10 | emitterStore, 11 | }; 12 | 13 | ReactDOM.render( 14 | 15 | 16 | , 17 | document.querySelector('#root'), 18 | ); 19 | -------------------------------------------------------------------------------- /src/canvas/scenes/preload.ts: -------------------------------------------------------------------------------- 1 | import shapesJSON from '../assets/shapes.json'; 2 | import shapeIMAGE from '../assets/shapes.png'; 3 | 4 | export default class Preload extends Phaser.Scene { 5 | constructor() { 6 | super('Preload'); 7 | } 8 | 9 | preload() { 10 | this.load.atlas('shape', shapeIMAGE, shapesJSON); 11 | } 12 | 13 | create() { 14 | this.scene.start('Background'); 15 | this.scene.start('Canvas'); 16 | this.scene.remove(this); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/canvas/index.ts: -------------------------------------------------------------------------------- 1 | import getGameConfig from './config'; 2 | import { CanvasScene, PreloadScene, BackgroundScene } from './scenes'; 3 | import Game from './game'; 4 | 5 | const initCanvas = (height: number, width: number) => { 6 | const config = getGameConfig(height, width, [ 7 | PreloadScene, 8 | BackgroundScene, 9 | CanvasScene, 10 | ]); 11 | const parent: HTMLDivElement = document.getElementById( 12 | 'phaser-canvas', 13 | ) as HTMLDivElement; 14 | (window as any).game = new Game(config, parent); 15 | }; 16 | 17 | export default initCanvas; 18 | -------------------------------------------------------------------------------- /src/components/AppBarMenuItem/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { MenuItem, ListItemIcon, ListItemText } from '@material-ui/core'; 3 | 4 | type Props = { 5 | onClick: any; 6 | icon: any; 7 | text: string; 8 | }; 9 | 10 | class AppBarMenuItem extends Component { 11 | render() { 12 | const { onClick, icon, text } = this.props; 13 | 14 | return ( 15 | 16 | {icon} 17 | 18 | 19 | ); 20 | } 21 | } 22 | 23 | export default AppBarMenuItem; 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | before_install: 5 | - export CHROME_BIN=chromium-browser 6 | - export DISPLAY=:99.0 7 | - sh -e /etc/init.d/xvfb start 8 | - yarn config set registry "https://registry.npmjs.org" 9 | - yarn global add greenkeeper-lockfile@1 10 | - yarn global add codeclimate-test-reporter 11 | before_script: 12 | - greenkeeper-lockfile-update 13 | script: 14 | - npm run build 15 | after_script: 16 | - greenkeeper-lockfile-upload 17 | deploy: 18 | provider: pages 19 | skip-cleanup: true 20 | github-token: $GITHUB_TOKEN 21 | keep-history: true 22 | local_dir: build 23 | on: 24 | tags: true 25 | -------------------------------------------------------------------------------- /src/withRoot.tsx: -------------------------------------------------------------------------------- 1 | import CssBaseline from '@material-ui/core/CssBaseline'; 2 | import { createMuiTheme, MuiThemeProvider } from '@material-ui/core/styles'; 3 | import * as React from 'react'; 4 | 5 | const theme = createMuiTheme({ 6 | palette: { 7 | type: 'dark', 8 | }, 9 | typography: { 10 | useNextVariants: true, 11 | }, 12 | }); 13 | 14 | function withRoot(Component: React.ComponentType) { 15 | function WithRoot(props: any) { 16 | return ( 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | return WithRoot; 25 | } 26 | 27 | export default withRoot; 28 | -------------------------------------------------------------------------------- /src/canvas/game.ts: -------------------------------------------------------------------------------- 1 | class Game extends Phaser.Game { 2 | private parent: any; 3 | 4 | constructor(config: any, parent: any) { 5 | super(config); 6 | this.parent = parent; 7 | window.onresize = this.resize.bind(this); 8 | this.resize(); 9 | this.resize(); 10 | } 11 | 12 | public resize(): void { 13 | const { width, height } = this.config as any; 14 | const scale: number = Math.min( 15 | this.parent.clientHeight / height, 16 | this.parent.clientWidth / width, 17 | ); 18 | this.canvas.style.position = 'absolute'; 19 | this.canvas.style.width = width * scale + 'px'; 20 | this.canvas.style.height = height * scale + 'px'; 21 | this.canvas.style.left = 22 | (this.parent.clientWidth - width * scale) * 0.5 + 'px'; 23 | this.canvas.style.top = 24 | (this.parent.clientHeight - height * scale) * 0.5 + 'px'; 25 | } 26 | } 27 | 28 | export default Game; 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to phaser3-particle-editor 2 | 3 | ## Setup 4 | 5 | 1 - Clone your fork of the repository: 6 | 7 | ``` 8 | $ git clone https://github.com/koreezgames/phaser3-particle-editor.git 9 | ``` 10 | 11 | 2 - Install npm dependencies using npm: 12 | 13 | ``` 14 | $ npm install 15 | ``` 16 | 17 | 3 - Run start process 18 | 19 | ``` 20 | $ npm run start 21 | ``` 22 | 23 | ## Guidelines 24 | 25 | - Please try to [combine multiple commits before 26 | pushing](http://stackoverflow.com/questions/6934752/combining-multiple-commits-before-pushing-in-git). 27 | 28 | - Always format your code using `npm run autoformat`. 29 | 30 | * Please create an issue before sending a PR if your commit is going to change the 31 | public interface of the package or it includes significant architecture 32 | changes. 33 | 34 | * Feel free to ask for help from other members of the Koreez team via the 35 | [github issues](https://github.com/koreezgames/phaser3-particle-editor/issues). 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "outDir": "build/dist", 5 | "module": "esnext", 6 | "target": "es5", 7 | "lib": ["es2017", "dom"], 8 | "sourceMap": true, 9 | "allowJs": true, 10 | "jsx": "react", 11 | "moduleResolution": "node", 12 | "rootDir": "src", 13 | "types": [], 14 | "forceConsistentCasingInFileNames": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "noImplicitAny": true, 18 | "strictNullChecks": true, 19 | "strictFunctionTypes": true, 20 | "alwaysStrict": true, 21 | "suppressImplicitAnyIndexErrors": true, 22 | "noUnusedLocals": true, 23 | "experimentalDecorators": true, 24 | "allowSyntheticDefaultImports": true, 25 | "esModuleInterop": true 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "build", 30 | "scripts", 31 | "acceptance-tests", 32 | "webpack", 33 | "jest", 34 | "src/setupTests.ts" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/canvas/scenes/background.ts: -------------------------------------------------------------------------------- 1 | import editorStore from '../../stores/editorStore'; 2 | import { autorun, IReactionDisposer } from 'mobx'; 3 | 4 | export default class Background extends Phaser.Scene { 5 | private backgroundImage: Phaser.GameObjects.Image; 6 | private autorun: IReactionDisposer; 7 | constructor() { 8 | super('Background'); 9 | } 10 | 11 | create() { 12 | this.events.once('destroy', this.destroy, this); 13 | this.autorun = autorun(this.syncBgToConfigBg.bind(this)); 14 | } 15 | 16 | destroy() { 17 | this.autorun(); 18 | } 19 | 20 | syncBgToConfigBg() { 21 | const { data } = editorStore.background; 22 | if (this.backgroundImage) { 23 | this.backgroundImage.destroy(); 24 | this.textures.remove('_bg'); 25 | } 26 | if (data) { 27 | this.textures.addBase64('_bg', editorStore.background.data); 28 | this.textures.once('addtexture', () => { 29 | this.backgroundImage = this.add.image(0, 0, '_bg').setOrigin(0, 0); 30 | }); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/constants/emitterConfig.ts: -------------------------------------------------------------------------------- 1 | import frames from './frames'; 2 | 3 | const emitterConfig = { 4 | active: true, 5 | visible: true, 6 | collideBottom: true, 7 | collideLeft: true, 8 | collideRight: true, 9 | collideTop: true, 10 | on: true, 11 | particleBringToTop: true, 12 | radial: true, 13 | frame: { frames: [frames[0].value], cycle: false, quantity: 1 }, 14 | frequency: 0, 15 | gravityX: 0, 16 | gravityY: 0, 17 | maxParticles: 0, 18 | timeScale: 1, 19 | blendMode: 0, 20 | accelerationX: [0], 21 | accelerationY: [0], 22 | alpha: [1], 23 | angle: { min: 0, max: 360, ease: 'Linear' }, 24 | bounce: [0], 25 | delay: [0], 26 | lifespan: [1000], 27 | maxVelocityX: [10000], 28 | maxVelocityY: [10000], 29 | moveToX: [0], 30 | moveToY: [0], 31 | quantity: [1], 32 | rotate: [0], 33 | scale: [1], 34 | speed: [0], 35 | x: [400], 36 | y: [300], 37 | bounds: undefined, 38 | tint: [0xffffff], 39 | emitZone: undefined, 40 | deathZone: undefined, 41 | }; 42 | 43 | export default emitterConfig; 44 | -------------------------------------------------------------------------------- /src/constants/zoneEdgeSources.ts: -------------------------------------------------------------------------------- 1 | const deathZoneOptions = [ 2 | { 3 | shapeType: 'Rectangle', 4 | source: { 5 | x: 0, 6 | y: 0, 7 | width: 50, 8 | height: 50, 9 | }, 10 | }, 11 | { 12 | shapeType: 'Circle', 13 | source: { 14 | x: 0, 15 | y: 0, 16 | radius: 50, 17 | }, 18 | }, 19 | { 20 | shapeType: 'Ellipse', 21 | source: { 22 | x: 0, 23 | y: 0, 24 | width: 80, 25 | height: 50, 26 | }, 27 | }, 28 | { 29 | shapeType: 'Triangle', 30 | source: { 31 | x1: 0, 32 | y1: 0, 33 | x2: 100, 34 | y2: 0, 35 | x3: 50, 36 | y3: 50, 37 | }, 38 | }, 39 | ]; 40 | 41 | const emitZoneOptions = [ 42 | ...deathZoneOptions, 43 | { 44 | shapeType: 'Line', 45 | source: { 46 | x1: 0, 47 | y1: 0, 48 | x2: 50, 49 | y2: 50, 50 | }, 51 | }, 52 | ]; 53 | 54 | const zoneEdgeSources = emitZoneOptions; 55 | 56 | export { emitZoneOptions, deathZoneOptions }; 57 | export default zoneEdgeSources; 58 | -------------------------------------------------------------------------------- /src/components/EmitterConfig/expansionPanelColorPicker.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | ExpansionPanel, 4 | ExpansionPanelSummary, 5 | Typography, 6 | ExpansionPanelDetails, 7 | Grid, 8 | } from '@material-ui/core'; 9 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; 10 | import MultipleInput from '../MultipleInput'; 11 | import { colorPicker } from '../ColorPicker'; 12 | 13 | class ExpansionPanelColorPicker extends Component { 14 | render() { 15 | return ( 16 | 17 | }> 18 | Tint 19 | 20 | 21 | 22 | 23 | {colorPicker} 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | } 31 | 32 | export default ExpansionPanelColorPicker; 33 | -------------------------------------------------------------------------------- /src/components/EmitterConfig/expansionPanelBlendMode.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | ExpansionPanel, 4 | ExpansionPanelSummary, 5 | Typography, 6 | ExpansionPanelDetails, 7 | } from '@material-ui/core'; 8 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; 9 | import { inject, observer } from 'mobx-react'; 10 | import { EMITTER_STORE, EmitterStoreProp } from 'src/stores'; 11 | import Select from '../Select'; 12 | import { blendModes } from 'src/constants'; 13 | 14 | @inject(EMITTER_STORE) 15 | @observer 16 | class ExpansionPanelBlendMode extends Component { 17 | render() { 18 | return ( 19 | 20 | }> 21 | Blend Mode 22 | 23 | 24 | ; 50 | }; 51 | 52 | export { selectComponent }; 53 | 54 | export default Select; 55 | -------------------------------------------------------------------------------- /src/components/Editor/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | import { EditorStoreProp } from '../../stores/editorStore'; 4 | import { Grid, WithStyles, withStyles, createStyles } from '@material-ui/core'; 5 | import initCanvas from '../../canvas'; 6 | import EmitterConfig from '../EmitterConfig'; 7 | import { EDITOR_STORE } from '../../stores'; 8 | import AppBar from '../AppBar'; 9 | import EmitterList from '../EmitterList'; 10 | 11 | const styles = () => 12 | createStyles({ 13 | darkGrid: { 14 | backgroundColor: '#424242', 15 | }, 16 | parentPhaser: { 17 | position: 'relative', 18 | }, 19 | }); 20 | 21 | @inject(EDITOR_STORE) 22 | @observer 23 | class Editor extends React.Component< 24 | WithStyles & EditorStoreProp 25 | > { 26 | componentDidMount() { 27 | const { editorStore } = this.props; 28 | const { height, width } = editorStore!; 29 | initCanvas(height.value, width.value); 30 | } 31 | 32 | render() { 33 | const { classes } = this.props; 34 | return ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ); 48 | } 49 | } 50 | 51 | export default withStyles(styles)(Editor); 52 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Theme } from '@material-ui/core/styles/createMuiTheme'; 2 | import createStyles from '@material-ui/core/styles/createStyles'; 3 | import withStyles, { WithStyles } from '@material-ui/core/styles/withStyles'; 4 | import { inject, observer } from 'mobx-react'; 5 | import React, { Fragment } from 'react'; 6 | import NewProjectModal from '../components/NewProjectModal'; 7 | import { EditorStoreProp } from '../stores/editorStore'; 8 | import withRoot from '../withRoot'; 9 | import Editor from '../components/Editor'; 10 | import { EDITOR_STORE } from '../stores'; 11 | import ExportProjectModal from '../components/ExportProjectModal'; 12 | import ImportProjectModal from '../components/ImportProjectModal'; 13 | import SaveProjectModal from '../components/SaveProjectModal'; 14 | import ChangeBackgroundModal from '../components/ChangeBackgroundModal'; 15 | 16 | const styles = (theme: Theme) => 17 | createStyles({ 18 | root: { 19 | textAlign: 'center', 20 | paddingTop: theme.spacing.unit * 20, 21 | }, 22 | }); 23 | 24 | @inject(EDITOR_STORE) 25 | @observer 26 | class Index extends React.Component< 27 | WithStyles & EditorStoreProp 28 | > { 29 | render() { 30 | const { created } = this.props.editorStore!; 31 | return !created ? ( 32 | 33 | ) : ( 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | } 44 | 45 | export default withRoot(withStyles(styles)(Index)); 46 | -------------------------------------------------------------------------------- /src/components/ExportSaveProjectModal/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Dialog, 4 | DialogActions, 5 | DialogContent, 6 | DialogTitle, 7 | DialogContentText, 8 | } from '@material-ui/core'; 9 | import React from 'react'; 10 | 11 | type Props = { 12 | open: boolean; 13 | actionType: string; 14 | extension: string; 15 | projectName: string; 16 | onClose: () => void; 17 | onTrueClick: () => void; 18 | children?: any; 19 | }; 20 | 21 | class ExportSaveProjectModal extends React.Component { 22 | render() { 23 | const { 24 | open, 25 | onClose, 26 | actionType, 27 | extension, 28 | onTrueClick, 29 | projectName, 30 | children, 31 | } = this.props; 32 | 33 | return ( 34 | 35 | {actionType} 36 | 37 | 38 | {actionType} project {projectName}.{extension} ? 39 | 40 | {children} 41 | 42 | 43 | 51 | 59 | 60 | 61 | ); 62 | } 63 | } 64 | 65 | export default ExportSaveProjectModal; 66 | -------------------------------------------------------------------------------- /src/components/EmitterConfig/expansionPanelOptions.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | ExpansionPanel, 4 | ExpansionPanelSummary, 5 | Typography, 6 | ExpansionPanelDetails, 7 | Grid, 8 | } from '@material-ui/core'; 9 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; 10 | import TextField from '../TextField'; 11 | import Switch from '../Switch'; 12 | 13 | class ExpansionPanelOptions extends Component { 14 | render() { 15 | return ( 16 | 17 | }> 18 | Options 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | } 49 | 50 | export default ExpansionPanelOptions; 51 | -------------------------------------------------------------------------------- /src/components/SaveProjectModal/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | import { 4 | EDITOR_STORE, 5 | EMITTER_STORE, 6 | EmitterStoreProp, 7 | EditorStoreProp, 8 | } from '../../stores'; 9 | import ExportSaveProjectModal from '../ExportSaveProjectModal'; 10 | import { ARCHIVE_EXTENSION } from '../../constants'; 11 | import { saveProject } from '../../utils'; 12 | 13 | @inject(EDITOR_STORE, EMITTER_STORE) 14 | @observer 15 | class SaevProjectModal extends React.Component< 16 | EditorStoreProp & EmitterStoreProp 17 | > { 18 | render() { 19 | const { editorStore, emitterStore } = this.props; 20 | const { 21 | openSaveDialog, 22 | name, 23 | width, 24 | height, 25 | setOpenSaveDialog, 26 | background, 27 | } = editorStore!; 28 | const { emitters } = emitterStore!; 29 | 30 | return ( 31 | setOpenSaveDialog(false)} 37 | onTrueClick={() => { 38 | setOpenSaveDialog(false); 39 | saveProject({ 40 | name: name.value, 41 | emitters: emitters, 42 | canvasSize: { 43 | width: width.value, 44 | height: height.value, 45 | }, 46 | backgroundData: { 47 | data: background.data, 48 | width: background.size.width, 49 | height: background.size.height, 50 | }, 51 | }); 52 | }} 53 | /> 54 | ); 55 | } 56 | } 57 | 58 | export default SaevProjectModal; 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phaser3-particle-editor", 3 | "version": "1.0.0-beta3", 4 | "private": true, 5 | "main": "src/index.tsx", 6 | "homepage": "http://koreezgames.github.io/phaser3-particle-editor", 7 | "dependencies": { 8 | "@material-ui/core": "^3.6.2", 9 | "@material-ui/icons": "^3.0.1", 10 | "@types/file-saver": "^2.0.0", 11 | "@types/jszip": "^3.1.4", 12 | "@types/react": "^16.7.17", 13 | "@types/react-dom": "^16.0.11", 14 | "file-saver": "^2.0.0", 15 | "github-buttons": "git+https://github.com/ntkme/github-buttons.git", 16 | "jszip": "^3.1.5", 17 | "lodash.get": "^4.4.2", 18 | "lodash.isequal": "^4.5.0", 19 | "lodash.isplainobject": "^4.0.6", 20 | "lodash.range": "^3.2.0", 21 | "lodash.set": "^4.3.2", 22 | "lodash.startcase": "^4.4.0", 23 | "lodash.uniqueid": "^4.0.1", 24 | "mobx": "^5.8.0", 25 | "mobx-react": "^5.4.3", 26 | "phaser": "^3.15.1", 27 | "react": "latest", 28 | "react-color": "^2.14.1", 29 | "react-dom": "latest", 30 | "react-scripts-ts": "^3.1.0" 31 | }, 32 | "devDependencies": { 33 | "@types/jest": "^23.3.10", 34 | "@types/lodash": "^4.14.119", 35 | "@types/node": "^10.12.14", 36 | "gh-pages": "^2.0.1", 37 | "prettier": "^1.15.3", 38 | "tslint-config-prettier": "^1.17.0", 39 | "typescript": "^3.2.2" 40 | }, 41 | "scripts": { 42 | "start": "react-scripts-ts start", 43 | "build": "react-scripts-ts build", 44 | "autoformat": "prettier --config .prettierrc --write {src,test}{/,/**/}{*,*.test}.ts", 45 | "test": "react-scripts-ts test --env=jsdom", 46 | "eject": "react-scripts-ts eject", 47 | "predeploy": "npm run build", 48 | "deploy": "gh-pages -d build --remote=upstream" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/components/AppBar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import AppBarMenu from '../AppBarMenu'; 4 | import { EDITOR_STORE, EditorStoreProp } from '../../stores'; 5 | import packageJSON from '../../../package.json'; 6 | import GitHubButton from 'github-buttons/dist/react'; 7 | import { 8 | AppBar as MaterialAppBar, 9 | Toolbar, 10 | Typography, 11 | withStyles, 12 | WithStyles, 13 | } from '@material-ui/core'; 14 | 15 | const styles = { 16 | flex: { 17 | flexGrow: 1, 18 | }, 19 | version: { marginRight: 10 }, 20 | }; 21 | 22 | @inject(EDITOR_STORE) 23 | @observer 24 | class AppBar extends Component> { 25 | render() { 26 | const { classes, editorStore } = this.props; 27 | const { name } = editorStore!; 28 | return ( 29 | 30 | 31 | 32 | 33 | {name.value} 34 | 35 | 36 | v {packageJSON.version} 37 | 38 | 44 | 45 | 46 | ); 47 | } 48 | } 49 | 50 | // < !--Place this tag where you want the button to render. -- > 51 | // Star 53 | 54 | export default withStyles(styles)(AppBar); 55 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | 7 | ## Related Issue 8 | 9 | 10 | 11 | 12 | 13 | 14 | ## Motivation and Context 15 | 16 | 17 | 18 | ## How Has This Been Tested? 19 | 20 | 21 | 22 | 23 | 24 | ## Types of changes 25 | 26 | 27 | 28 | - [ ] Updated docs / Refactor code / Added a tests case (non-breaking change) 29 | - [ ] Bug fix (non-breaking change which fixes an issue) 30 | - [ ] New feature (non-breaking change which adds functionality) 31 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 32 | 33 | ## Checklist: 34 | 35 | 36 | 37 | 38 | - [ ] My code follows the code style of this project. 39 | - [ ] My change requires a change to the documentation. 40 | - [ ] I have updated the documentation accordingly. 41 | - [ ] I have read the **CONTRIBUTING** document. 42 | - [ ] I have added tests to cover my changes. 43 | - [ ] All new and existing tests passed. 44 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 24 | 25 | Phaser 3 particle editor 26 | 27 | 28 | 29 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/constants/easing.ts: -------------------------------------------------------------------------------- 1 | const easing = [ 2 | { text: 'Custom', value: 'Custom' }, 3 | { text: 'Linear', value: 'Linear' }, 4 | { text: 'Stepped', value: 'Stepped' }, 5 | { text: 'Quad.easeIn', value: 'Quad.easeIn' }, 6 | { text: 'Cubic.easeIn', value: 'Cubic.easeIn' }, 7 | { text: 'Quart.easeIn', value: 'Quart.easeIn' }, 8 | { text: 'Quint.easeIn', value: 'Quint.easeIn' }, 9 | { text: 'Sine.easeIn', value: 'Sine.easeIn' }, 10 | { text: 'Expo.easeIn', value: 'Expo.easeIn' }, 11 | { text: 'Circ.easeIn', value: 'Circ.easeIn' }, 12 | { text: 'Elastic.easeIn', value: 'Elastic.easeIn' }, 13 | { text: 'Back.easeIn', value: 'Back.easeIn' }, 14 | { text: 'Bounce.easeIn', value: 'Bounce.easeIn' }, 15 | { text: 'Quad.easeOut', value: 'Quad.easeOut' }, 16 | { text: 'Cubic.easeOut', value: 'Cubic.easeOut' }, 17 | { text: 'Quart.easeOut', value: 'Quart.easeOut' }, 18 | { text: 'Quint.easeOut', value: 'Quint.easeOut' }, 19 | { text: 'Sine.easeOut', value: 'Sine.easeOut' }, 20 | { text: 'Expo.easeOut', value: 'Expo.easeOut' }, 21 | { text: 'Circ.easeOut', value: 'Circ.easeOut' }, 22 | { text: 'Elastic.easeOut', value: 'Elastic.easeOut' }, 23 | { text: 'Back.easeOut', value: 'Back.easeOut' }, 24 | { text: 'Bounce.easeOut', value: 'Bounce.easeOut' }, 25 | { text: 'Quad.easeInOut', value: 'Quad.easeInOut' }, 26 | { text: 'Cubic.easeInOut', value: 'Cubic.easeInOut' }, 27 | { text: 'Quart.easeInOut', value: 'Quart.easeInOut' }, 28 | { text: 'Quint.easeInOut', value: 'Quint.easeInOut' }, 29 | { text: 'Sine.easeInOut', value: 'Sine.easeInOut' }, 30 | { text: 'Expo.easeInOut', value: 'Expo.easeInOut' }, 31 | { text: 'Circ.easeInOut', value: 'Circ.easeInOut' }, 32 | { text: 'Elastic.easeInOut', value: 'Elastic.easeInOut' }, 33 | { text: 'Back.easeInOut', value: 'Back.easeInOut' }, 34 | { text: 'Bounce.easeInOut', value: 'Bounce.easeInOut' }, 35 | ]; 36 | 37 | export default easing; 38 | -------------------------------------------------------------------------------- /src/components/EmitterList/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { 3 | List, 4 | Divider, 5 | withStyles, 6 | WithStyles, 7 | Theme, 8 | createStyles, 9 | Fab, 10 | } from '@material-ui/core'; 11 | import EmitterItem from '../EmitterItem'; 12 | import { EmitterStoreProp, EMITTER_STORE } from '../../stores'; 13 | import { inject, observer } from 'mobx-react'; 14 | import AddIcon from '@material-ui/icons/Add'; 15 | 16 | const styles = (theme: Theme) => 17 | createStyles({ 18 | root: { 19 | position: 'relative', 20 | height: `calc(100vh - 48px)`, 21 | }, 22 | list: { 23 | height: '100%', 24 | overflow: 'auto', 25 | }, 26 | fab: { 27 | position: 'absolute', 28 | bottom: theme.spacing.unit * 2, 29 | right: theme.spacing.unit * 2, 30 | }, 31 | }); 32 | 33 | @inject(EMITTER_STORE) 34 | @observer 35 | class EmitterList extends Component< 36 | WithStyles & EmitterStoreProp 37 | > { 38 | render() { 39 | const { emitterStore, classes } = this.props; 40 | const { emitters, addEmitter } = emitterStore!; 41 | 42 | return ( 43 |
44 |
45 | 46 | {emitters.map((value: any, index: number) => { 47 | return ( 48 | 49 | {index === 0 ? : null} 50 | 51 | 52 | 53 | ); 54 | })} 55 | 56 |
57 | addEmitter()} 61 | > 62 | 63 | 64 |
65 | ); 66 | } 67 | } 68 | 69 | export default withStyles(styles)(EmitterList); 70 | -------------------------------------------------------------------------------- /src/components/EmitterConfig/expansionPanelXY.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { 3 | ExpansionPanel, 4 | ExpansionPanelSummary, 5 | Typography, 6 | ExpansionPanelDetails, 7 | Grid, 8 | FormLabel, 9 | } from '@material-ui/core'; 10 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; 11 | import TextField from '../TextField'; 12 | import CompositeProperty from '../CompositeProperty'; 13 | 14 | type Props = { 15 | typography: string; 16 | configNameX: string; 17 | configNameY: string; 18 | composite: boolean; 19 | }; 20 | 21 | class ExpansionPanelXY extends Component { 22 | render() { 23 | const { composite, typography, configNameX, configNameY } = this.props; 24 | return ( 25 | 26 | }> 27 | {typography} 28 | 29 | 30 | 31 | 32 | {composite ? ( 33 | 34 | X 35 | 36 | 37 | ) : ( 38 | 39 | )} 40 | 41 | 42 | {composite ? ( 43 | 44 | Y 45 | 46 | 47 | ) : ( 48 | 49 | )} 50 | 51 | 52 | 53 | 54 | ); 55 | } 56 | } 57 | 58 | export default ExpansionPanelXY; 59 | -------------------------------------------------------------------------------- /src/components/ExportProjectModal/index.tsx: -------------------------------------------------------------------------------- 1 | import { FormControlLabel, Checkbox } from '@material-ui/core'; 2 | import React from 'react'; 3 | import { inject, observer } from 'mobx-react'; 4 | import { 5 | EDITOR_STORE, 6 | EMITTER_STORE, 7 | EmitterStoreProp, 8 | EditorStoreProp, 9 | } from '../../stores'; 10 | import { exportProject } from '../../utils'; 11 | import ExportSaveProjectModal from '../ExportSaveProjectModal'; 12 | 13 | @inject(EDITOR_STORE, EMITTER_STORE) 14 | @observer 15 | class ExportProjectModal extends React.Component< 16 | EditorStoreProp & EmitterStoreProp 17 | > { 18 | render() { 19 | const { editorStore, emitterStore } = this.props; 20 | const { 21 | openExportDialog, 22 | name, 23 | exportHiddenEmitters, 24 | setOpenExportDialog, 25 | setExportHiddenEmitters, 26 | } = editorStore!; 27 | const { emitters } = emitterStore!; 28 | const anyHiddenEmitter = emitters.some(emitter => !emitter.config.visible); 29 | 30 | return ( 31 | setOpenExportDialog(false)} 37 | onTrueClick={() => { 38 | setOpenExportDialog(false); 39 | exportProject(name.value, emitters, exportHiddenEmitters); 40 | }} 41 | > 42 | {anyHiddenEmitter ? ( 43 | 48 | setExportHiddenEmitters(event.target.checked) 49 | } 50 | color="primary" 51 | /> 52 | } 53 | label="export hidden emitters" 54 | /> 55 | ) : null} 56 | 57 | ); 58 | } 59 | } 60 | 61 | export default ExportProjectModal; 62 | -------------------------------------------------------------------------------- /src/components/TextField/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { EMITTER_STORE } from '../../stores'; 3 | import { observer, inject } from 'mobx-react'; 4 | import { TextField as MaterialTextField } from '@material-ui/core'; 5 | import { EmitterStoreProp } from '../../stores/emitterStore'; 6 | import _get from 'lodash/get'; 7 | import _startCase from 'lodash/startCase'; 8 | 9 | type Props = { 10 | configName: string; 11 | hideLabel?: boolean; 12 | label?: string; 13 | disabled?: boolean; 14 | } & Partial; 15 | 16 | type DefaultProps = Readonly; 17 | 18 | const defaultProps = { 19 | type: 'number' as 'number' | 'text', 20 | hideLabel: false, 21 | }; 22 | 23 | @inject(EMITTER_STORE) 24 | @observer 25 | class TextField extends Component { 26 | static defaultProps = defaultProps; 27 | 28 | render() { 29 | const { 30 | configName, 31 | label, 32 | type, 33 | hideLabel, 34 | emitterStore, 35 | disabled, 36 | } = this.props; 37 | const { currentEmitterConfig, changeEmitterConfig } = emitterStore!; 38 | const value = _get(currentEmitterConfig, configName.split('>')); 39 | const textFieldLabel = !hideLabel ? label || _startCase(configName) : null; 40 | const textFieldError = value === ''; 41 | 42 | return ( 43 | { 51 | const newValue = event.target.value; 52 | const configValue = 53 | type === 'number' && newValue !== '' ? +newValue : newValue; 54 | changeEmitterConfig(configName, configValue); 55 | }} 56 | /> 57 | ); 58 | } 59 | } 60 | 61 | const textField = (params: any) => { 62 | return ; 63 | }; 64 | 65 | export { textField }; 66 | export default TextField; 67 | -------------------------------------------------------------------------------- /src/components/ColorPicker/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import { EMITTER_STORE } from '../../stores'; 4 | import { getPickerColor } from '../../utils'; 5 | import { EmitterStoreProp } from '../../stores/emitterStore'; 6 | import _get from 'lodash/get'; 7 | import _startCase from 'lodash/startCase'; 8 | import { ChromePicker } from 'react-color'; 9 | import { TextField } from '@material-ui/core'; 10 | 11 | type Props = { 12 | configName: string; 13 | }; 14 | 15 | @inject(EMITTER_STORE) 16 | @observer 17 | class ColorPicker extends Component { 18 | state = { 19 | showPicker: false, 20 | }; 21 | 22 | render() { 23 | const { showPicker } = this.state; 24 | const { configName, emitterStore } = this.props; 25 | const { currentEmitterConfig, changeEmitterConfig } = emitterStore!; 26 | const value = _get(currentEmitterConfig, configName.split('>')); 27 | const valueHex = getPickerColor(value); // `#${value.toString(16)}`; 28 | 29 | return ( 30 |
31 | this.setState({ showPicker: true })} 34 | InputProps={{ style: { color: valueHex } }} 35 | /> 36 | {showPicker ? ( 37 |
38 |
this.setState({ showPicker: false })} 47 | /> 48 | { 52 | const colorValue = parseInt(hex.substring(1), 16); 53 | changeEmitterConfig(configName, colorValue); 54 | }} 55 | /> 56 |
57 | ) : null} 58 |
59 | ); 60 | } 61 | } 62 | 63 | const colorPicker = (params: any) => { 64 | return ; 65 | }; 66 | 67 | export { colorPicker }; 68 | export default ColorPicker; 69 | -------------------------------------------------------------------------------- /src/components/ImportProjectModal/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Dialog, 4 | DialogActions, 5 | DialogContent, 6 | DialogTitle, 7 | } from '@material-ui/core'; 8 | import React from 'react'; 9 | import { inject, observer } from 'mobx-react'; 10 | import { 11 | EDITOR_STORE, 12 | EMITTER_STORE, 13 | EmitterStoreProp, 14 | EditorStoreProp, 15 | } from '../../stores'; 16 | import ImportProjectFile, { ImportProjectButton } from '../ImportProjectFile'; 17 | import initCanvas from '../../canvas'; 18 | 19 | @inject(EDITOR_STORE, EMITTER_STORE) 20 | @observer 21 | class ImportProjectModal extends React.Component< 22 | EditorStoreProp & EmitterStoreProp 23 | > { 24 | render() { 25 | const { editorStore, emitterStore } = this.props; 26 | const { 27 | openImportDialog, 28 | setOpenImportDialog, 29 | setEditorProps, 30 | } = editorStore!; 31 | const { setEmitters } = emitterStore!; 32 | 33 | return ( 34 | setOpenImportDialog(false)} 37 | fullWidth={true} 38 | maxWidth={'sm'} 39 | > 40 | Open Project 41 | 42 | 43 | 44 | 45 | 53 | setOpenImportDialog(false)} 55 | onReadyResult={result => { 56 | const { game } = window as any; 57 | for (let scene in game.scene.keys) { 58 | if (game.scene.keys.hasOwnProperty(scene)) { 59 | game.scene.remove(scene); 60 | } 61 | } 62 | game.destroy(true); 63 | setEditorProps(result.editor); 64 | setEmitters(result.emitters); 65 | initCanvas(result.editor.height, result.editor.width); 66 | }} 67 | /> 68 | 69 | 70 | ); 71 | } 72 | } 73 | 74 | export default ImportProjectModal; 75 | -------------------------------------------------------------------------------- /src/components/ComplexZone/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { EMITTER_STORE } from '../../stores'; 3 | import { observer, inject } from 'mobx-react'; 4 | import { EmitterStoreProp } from '../../stores/emitterStore'; 5 | import Zone from '../Zone'; 6 | import TextField from '../TextField'; 7 | import Switch from '../Switch'; 8 | import { edgeZoneComplexProps } from '../../constants'; 9 | 10 | interface Props { 11 | configName: string; 12 | types: [string, string]; 13 | options: any; 14 | } 15 | 16 | @inject(EMITTER_STORE) 17 | @observer 18 | class ComplexZone extends Component { 19 | onEnable = (zoneValue: any) => { 20 | const zoneValueCopy = { ...zoneValue }; 21 | if (zoneValue && zoneValue.type === this.props.types[0]) { 22 | Object.assign(zoneValueCopy, edgeZoneComplexProps); 23 | } 24 | return zoneValueCopy; 25 | }; 26 | 27 | onTypeChange = (zoneValue: any, type: string) => { 28 | const zoneValueCopy = { ...zoneValue }; 29 | if (type === this.props.types[1]) { 30 | Object.keys(edgeZoneComplexProps).forEach( 31 | propName => delete zoneValueCopy[propName], 32 | ); 33 | } else if (type === this.props.types[0]) { 34 | Object.assign(zoneValueCopy, edgeZoneComplexProps); 35 | } 36 | return zoneValueCopy; 37 | }; 38 | 39 | render() { 40 | const { configName, types, emitterStore, options } = this.props; 41 | const { currentEmitterConfig } = emitterStore!; 42 | const zone = currentEmitterConfig[configName]; 43 | 44 | return ( 45 | 46 | 53 | {zone && zone.type === types[0] ? ( 54 | 55 | quantity`} label="quantity" /> 56 | stepRate`} label="stepRate" /> 57 | yoyo`} label="yoyo" /> 58 | seamless`} label="seamless" /> 59 | 60 | ) : null} 61 | 62 | ); 63 | } 64 | } 65 | 66 | export default ComplexZone; 67 | -------------------------------------------------------------------------------- /src/components/EmitterConfig/expansionPanelFrame.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | ExpansionPanel, 4 | ExpansionPanelSummary, 5 | Typography, 6 | ExpansionPanelDetails, 7 | Grid, 8 | } from '@material-ui/core'; 9 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; 10 | import { inject, observer } from 'mobx-react'; 11 | import { EMITTER_STORE, EmitterStoreProp } from 'src/stores'; 12 | import Switch from '../Switch'; 13 | import TextField from '../TextField'; 14 | import MultipleInput from '../MultipleInput'; 15 | import { selectComponent } from '../Select'; 16 | import { frames } from 'src/constants'; 17 | import _get from 'lodash/get'; 18 | 19 | @inject(EMITTER_STORE) 20 | @observer 21 | class ExpansionPanelFrame extends Component { 22 | render() { 23 | const { emitterStore } = this.props; 24 | const { currentEmitterConfig } = emitterStore!; 25 | return ( 26 | 27 | }> 28 | Frame 29 | 30 | 31 | 32 | 33 | 38 | 39 | 40 | 46 | 47 | 48 | 49 | {(params: any) => { 50 | const { configName } = params; 51 | return selectComponent({ 52 | ...params, 53 | options: frames, 54 | value: _get(currentEmitterConfig, configName.split('>')), 55 | }); 56 | }} 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | } 64 | } 65 | 66 | export default ExpansionPanelFrame; 67 | -------------------------------------------------------------------------------- /src/components/MultipleInput/index.tsx: -------------------------------------------------------------------------------- 1 | import _range from 'lodash/range'; 2 | import _uniqueId from 'lodash/uniqueId'; 3 | import React, { Component } from 'react'; 4 | import { EMITTER_STORE } from '../../stores'; 5 | import { observer, inject } from 'mobx-react'; 6 | import { EmitterStoreProp } from '../../stores/emitterStore'; 7 | import { Grid, IconButton } from '@material-ui/core'; 8 | import AddIcon from '@material-ui/icons/Add'; 9 | import DeleteIcon from '@material-ui/icons/Delete'; 10 | import { emitterConfig } from '../../constants'; 11 | import _get from 'lodash/get'; 12 | 13 | type Props = { 14 | configName: string; 15 | }; 16 | 17 | @inject(EMITTER_STORE) 18 | @observer 19 | class MultipleInput extends Component { 20 | getInputComponent = (length: number) => { 21 | const { configName, emitterStore, children } = this.props; 22 | const { currentEmitterConfig, changeEmitterConfig } = emitterStore!; 23 | const values: any[] = _get(currentEmitterConfig, configName.split('>')); 24 | 25 | return _range(length).map((value, i) => { 26 | const last = value === length - 1; 27 | const addBtn = last ? ( 28 | 29 | 33 | changeEmitterConfig(configName, [ 34 | ...values, 35 | _get(emitterConfig, configName.split('>'))[0], 36 | ]) 37 | } 38 | > 39 | 40 | 41 | 42 | ) : null; 43 | 44 | return ( 45 | 46 | 47 | {(children as any)({ 48 | configName: `${configName}>${value}`, 49 | })} 50 | 51 | 52 | { 56 | values.splice(value, 1); 57 | changeEmitterConfig(configName, values); 58 | }} 59 | > 60 | 61 | 62 | 63 | {addBtn} 64 | 65 | ); 66 | }); 67 | }; 68 | 69 | render() { 70 | const { configName, emitterStore } = this.props; 71 | const { currentEmitterConfig } = emitterStore!; 72 | const values = _get(currentEmitterConfig, configName.split('>')); 73 | const length = values.length; 74 | return this.getInputComponent(length); 75 | } 76 | } 77 | 78 | export default MultipleInput; 79 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-react", "tslint-config-prettier"], 3 | "rules": { 4 | "align": [true, "parameters", "arguments", "statements"], 5 | "ban": false, 6 | "class-name": true, 7 | "comment-format": [true, "check-space"], 8 | "curly": true, 9 | "eofline": false, 10 | "forin": true, 11 | "indent": [true, "spaces"], 12 | "interface-name": [true, "never-prefix"], 13 | "jsdoc-format": true, 14 | "jsx-boolean-value": false, 15 | "jsx-no-lambda": false, 16 | "jsx-no-multiline-js": false, 17 | "label-position": true, 18 | "max-line-length": [true, 120], 19 | "member-ordering": [ 20 | true, 21 | { 22 | "order": [ 23 | "public-before-private", 24 | "static-before-instance", 25 | "variables-before-functions" 26 | ] 27 | } 28 | ], 29 | "no-any": false, 30 | "no-arg": true, 31 | "no-bitwise": true, 32 | "no-console": [ 33 | false, 34 | "log", 35 | "error", 36 | "debug", 37 | "info", 38 | "time", 39 | "timeEnd", 40 | "trace" 41 | ], 42 | "no-consecutive-blank-lines": true, 43 | "no-construct": true, 44 | "no-debugger": true, 45 | "no-duplicate-variable": true, 46 | "no-empty": true, 47 | "no-eval": false, 48 | "no-shadowed-variable": true, 49 | "no-string-literal": true, 50 | "no-switch-case-fall-through": true, 51 | "no-trailing-whitespace": false, 52 | "no-unused-expression": true, 53 | "no-use-before-declare": false, 54 | "one-line": [ 55 | true, 56 | "check-catch", 57 | "check-else", 58 | "check-open-brace", 59 | "check-whitespace" 60 | ], 61 | "quotemark": [true, "single", "jsx-double"], 62 | "radix": true, 63 | "semicolon": [true, "always", "ignore-bound-class-methods"], 64 | "switch-default": true, 65 | "trailing-comma": [false], 66 | "triple-equals": [true, "allow-null-check"], 67 | "typedef": [true, "parameter", "property-declaration"], 68 | "typedef-whitespace": [ 69 | true, 70 | { 71 | "call-signature": "nospace", 72 | "index-signature": "nospace", 73 | "parameter": "nospace", 74 | "property-declaration": "nospace", 75 | "variable-declaration": "nospace" 76 | } 77 | ], 78 | "variable-name": [ 79 | true, 80 | "ban-keywords", 81 | "check-format", 82 | "allow-leading-underscore", 83 | "allow-pascal-case" 84 | ], 85 | "whitespace": [ 86 | true, 87 | "check-branch", 88 | "check-decl", 89 | "check-module", 90 | "check-operator", 91 | "check-separator", 92 | "check-type", 93 | "check-typecast" 94 | ] 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/canvas/utils.ts: -------------------------------------------------------------------------------- 1 | import { deepCopy, getEmitterConfig, initialConfig } from '../utils'; 2 | import { CanvasScene } from './scenes'; 3 | 4 | const createEmitter = (scene: CanvasScene, config: any, name: string) => { 5 | let emitterConfig = config ? getEmitterConfig(config) : initialConfig; 6 | const emitter = scene.particle.createEmitter(emitterConfig); 7 | emitter.name = name; 8 | }; 9 | 10 | const removeEmitter = (scene: CanvasScene, emitter: any) => { 11 | const emitters = scene.particle.emitters.list; 12 | const index = emitters.indexOf(emitter); 13 | emitters.splice(index, 1); 14 | }; 15 | 16 | const changeEmitter = ( 17 | emitter: Phaser.GameObjects.Particles.ParticleEmitter, 18 | config: any, 19 | ) => { 20 | const newConfig: { 21 | deathZone?: any; 22 | emitZone?: any; 23 | bounds?: any; 24 | } = getEmitterConfig(config); 25 | emitter.fromJSON(newConfig); 26 | 27 | const { deathZone, emitZone, bounds } = newConfig; 28 | 29 | if (!bounds) { 30 | (emitter as any).bounds = null; 31 | } 32 | if (!emitZone) { 33 | (emitter as any).emitZone = null; 34 | } 35 | if (!deathZone) { 36 | (emitter as any).deathZone = null; 37 | } 38 | }; 39 | 40 | const drawDebugZoneGraphic = ( 41 | zone: any, 42 | scene: Phaser.Scene, 43 | color: number, 44 | type?: string, 45 | config?: any, 46 | ) => { 47 | const { shapeType, source } = zone; 48 | const shapeTypeGraphic = shapeType === 'Rectangle' ? 'Rect' : shapeType; 49 | const _source = deepCopy(source); 50 | 51 | const graphic = scene.add.graphics(); 52 | graphic.lineStyle(1, color, 1); 53 | 54 | if (type === 'EmitZone') { 55 | const { x, y } = config; 56 | switch (shapeType) { 57 | case 'Rectangle': 58 | case 'Ellipse': 59 | case 'Circle': { 60 | _source.x = _source.x + x; 61 | _source.y = _source.y + y; 62 | break; 63 | } 64 | case 'Line': { 65 | _source.x1 = _source.x1 + x; 66 | _source.y1 = _source.y1 + y; 67 | _source.x2 = _source.x2 + x; 68 | _source.y2 = _source.y2 + y; 69 | break; 70 | } 71 | case 'Triangle': { 72 | _source.x1 = _source.x1 + x; 73 | _source.y1 = _source.y1 + y; 74 | _source.x2 = _source.x2 + x; 75 | _source.y2 = _source.y2 + y; 76 | _source.x3 = _source.x3 + x; 77 | _source.y3 = _source.y3 + y; 78 | break; 79 | } 80 | default: 81 | break; 82 | } 83 | } 84 | 85 | const shape = new Phaser.Geom[shapeType](...Object.values(_source)); 86 | graphic[`stroke${shapeTypeGraphic}Shape`](shape); 87 | return graphic; 88 | }; 89 | 90 | const clearZoneGraphic = (graphic: Phaser.GameObjects.Graphics) => { 91 | if (graphic) { 92 | graphic.clear(); 93 | } 94 | }; 95 | 96 | export { 97 | createEmitter, 98 | removeEmitter, 99 | changeEmitter, 100 | drawDebugZoneGraphic, 101 | clearZoneGraphic, 102 | }; 103 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@koreez.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /src/components/AppBarMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, Component } from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | import { EditorStoreProp, EDITOR_STORE } from '../../stores'; 4 | import MenuIcon from '@material-ui/icons/Menu'; 5 | import SaveAltIcon from '@material-ui/icons/SaveAlt'; 6 | import FolderOpenIcon from '@material-ui/icons/FolderOpen'; 7 | import SaveIcon from '@material-ui/icons/Save'; 8 | import DeleteForeverIcon from '@material-ui/icons/DeleteForever'; 9 | import InsertPhotoIcon from '@material-ui/icons/InsertPhoto'; 10 | import { Menu, Fade, IconButton } from '@material-ui/core'; 11 | import AppBarMenuItem from '../AppBarMenuItem'; 12 | 13 | @inject(EDITOR_STORE) 14 | @observer 15 | class AppBarMenu extends Component { 16 | state = { 17 | anchorEl: null, 18 | }; 19 | 20 | handleMenuClick = (event: any) => { 21 | this.setState({ anchorEl: event.currentTarget }); 22 | }; 23 | 24 | handleMenuClose = () => { 25 | this.setState({ anchorEl: null }); 26 | }; 27 | 28 | render() { 29 | const { editorStore } = this.props; 30 | const { 31 | setOpenExportDialog, 32 | setOpenImportDialog, 33 | setOpenSaveDialog, 34 | setOpenBackgroundDialog, 35 | setInitialImportProps, 36 | resetBackground, 37 | background, 38 | } = editorStore!; 39 | const { anchorEl } = this.state; 40 | const menuOpen = Boolean(anchorEl); 41 | 42 | return ( 43 | 44 | 45 | 46 | 47 | 48 | 54 | { 56 | setOpenSaveDialog(true); 57 | this.handleMenuClose(); 58 | }} 59 | icon={} 60 | text="Save" 61 | /> 62 | { 64 | setInitialImportProps(); 65 | setOpenImportDialog(true); 66 | this.handleMenuClose(); 67 | }} 68 | icon={} 69 | text="Open" 70 | /> 71 | { 73 | setOpenExportDialog(true); 74 | this.handleMenuClose(); 75 | }} 76 | icon={} 77 | text="Export" 78 | /> 79 | { 81 | setOpenBackgroundDialog(true); 82 | this.handleMenuClose(); 83 | }} 84 | icon={} 85 | text={`${background.data ? 'Change' : 'Set'} Background`} 86 | /> 87 | {background.data ? ( 88 | { 90 | resetBackground(); 91 | this.handleMenuClose(); 92 | }} 93 | icon={} 94 | text="Remove Background" 95 | /> 96 | ) : null} 97 | 98 | 99 | ); 100 | } 101 | } 102 | 103 | export default AppBarMenu; 104 | -------------------------------------------------------------------------------- /src/components/CompositeProperty/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { EMITTER_STORE } from '../../stores'; 3 | import { observer, inject } from 'mobx-react'; 4 | import { EmitterStoreProp } from '../../stores/emitterStore'; 5 | import MultipleInput from '../MultipleInput'; 6 | import _isPlainObject from 'lodash/isPlainObject'; 7 | import Switch from '../Switch'; 8 | import TextField, { textField } from '../TextField'; 9 | import Select from '../Select'; 10 | import { hasBoth, hasKey } from '../../utils'; 11 | import { Grid } from '@material-ui/core'; 12 | import { easing } from '../../constants'; 13 | 14 | type Props = { 15 | configName: string; 16 | label?: string; 17 | }; 18 | 19 | @inject(EMITTER_STORE) 20 | @observer 21 | class CompositeProperty extends Component { 22 | getBody(isObject: boolean, random: boolean) { 23 | const { configName, emitterStore } = this.props; 24 | const { 25 | currentEmitterConfig, 26 | toggleRandom, 27 | changeSelectDropdown, 28 | } = emitterStore!; 29 | const value = currentEmitterConfig[configName]; 30 | 31 | let start = 'start'; 32 | let end = 'end'; 33 | 34 | if (random) { 35 | start = 'min'; 36 | end = 'max'; 37 | } 38 | 39 | return isObject ? ( 40 | 41 | toggleRandom(configName)} 45 | /> 46 | 47 | 48 | ${start}`} label={start} /> 49 | 50 | 51 | ${end}`} label={end} /> 52 | 53 | 54 | source`} 85 | options={options} 86 | value={zone.shapeType} 87 | onChange={event => { 88 | const zoneValue = { 89 | ...zone, 90 | ...getZoneShapeProps(event.target.value), 91 | }; 92 | changeEmitterConfig(configName, zoneValue); 93 | }} 94 | /> 95 | source`} /> 96 | 97 | ) : null} 98 | 99 | ); 100 | } 101 | } 102 | 103 | export default Zone; 104 | -------------------------------------------------------------------------------- /src/components/EmitterConfig/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { withStyles, WithStyles } from '@material-ui/core'; 3 | import { deathZoneEdgeShapes, emitZoneEdgeShapes } from '../../constants'; 4 | import ExpansionPanelXY from './expansionPanelXY'; 5 | import ExpansionPanelCompositeProperty from './expansionPanelCompositeProperty'; 6 | import ExpansionPanelZone from './expansionPanelZone'; 7 | import ExpansionPanelOptions from './expansionPanelOptions'; 8 | import ExpansionPanelFrame from './expansionPanelFrame'; 9 | import ExpansionPanelColorPicker from './expansionPanelColorPicker'; 10 | import ExpansionPanelTransform from './expansionPanelTransform'; 11 | import ExpansionPanelBounds from './expansionPanelBounds'; 12 | import ExpansionPanelBlendMode from './expansionPanelBlendMode'; 13 | 14 | const styles = { 15 | root: { 16 | height: 'calc(100vh - 48px)', 17 | overflow: 'auto', 18 | }, 19 | }; 20 | 21 | class EmitterConfig extends Component> { 22 | render() { 23 | return ( 24 |
25 | 26 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 46 | 47 | 53 | 54 | 60 | 61 | 67 | 68 | 72 | 73 | 77 | 78 | 79 | 80 | 84 | 85 | 89 | 90 | 94 | 95 | 99 | 100 | 101 | 102 | 109 | 110 | 117 |
118 | ); 119 | } 120 | } 121 | 122 | export default withStyles(styles)(EmitterConfig); 123 | -------------------------------------------------------------------------------- /src/canvas/scenes/canvas.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createEmitter, 3 | changeEmitter, 4 | removeEmitter, 5 | clearZoneGraphic, 6 | drawDebugZoneGraphic, 7 | } from '../utils'; 8 | import emitterStore from '../../stores/emitterStore'; 9 | import { autorun, toJS, IReactionDisposer } from 'mobx'; 10 | import { getEmitterConfig } from '../../utils'; 11 | 12 | export default class Canvas extends Phaser.Scene { 13 | public particle: Phaser.GameObjects.Particles.ParticleEmitterManager; 14 | 15 | public deathZoneDebugGraphics: Phaser.GameObjects.Graphics; 16 | public emitZoneDebugGraphics: Phaser.GameObjects.Graphics; 17 | 18 | private autoRedraw: IReactionDisposer; 19 | 20 | constructor() { 21 | super('Canvas'); 22 | } 23 | 24 | create() { 25 | const { emitters } = emitterStore; 26 | this.particle = this.add.particles('shape'); 27 | 28 | emitters.forEach(emitter => { 29 | createEmitter(this, emitter.config, emitter.name); 30 | }); 31 | 32 | let clicked = false; 33 | 34 | this.input.on('pointerdown', () => { 35 | clicked = true; 36 | }); 37 | 38 | this.input.on('pointerup', () => { 39 | clicked = false; 40 | }); 41 | 42 | this.input.on('pointermove', ({ x, y }: { x: number; y: number }) => { 43 | if (clicked) { 44 | emitterStore.setEmitterPosition(x, y); 45 | } 46 | }); 47 | this.events.once('destroy', this.destroy, this); 48 | this.autoRedraw = autorun(this.redraw.bind(this)); 49 | } 50 | 51 | syncPhaserEmittersToEmitters( 52 | emitters: any, 53 | currentEmitter: any, 54 | emitterIndex: number, 55 | ) { 56 | if (this.particle.emitters.list.length === toJS(emitters).length) { 57 | emitters.forEach((emitter: any, i: number) => { 58 | this.particle.emitters.list[i].visible = emitter.config.visible; 59 | }); 60 | changeEmitter( 61 | this.particle.emitters.list[emitterIndex], 62 | currentEmitter.config, 63 | ); 64 | } else if (this.particle.emitters.list.length < emitters.length) { 65 | createEmitter(this, currentEmitter.config, currentEmitter.name); 66 | } else { 67 | const removedEmitter = this.particle.emitters.list.filter( 68 | phaserEmitter => { 69 | for (let i = 0; i < emitters.length; i++) { 70 | if (phaserEmitter.name === emitters[i].name) { 71 | return false; 72 | } 73 | } 74 | return true; 75 | }, 76 | ); 77 | removeEmitter(this, removedEmitter[0]); 78 | } 79 | } 80 | 81 | drawDebugZones(debugModes: any, emitters: any, emitterIndex: number) { 82 | const configZone: { deathZone?: any; emitZone?: any } = getEmitterConfig( 83 | emitters[emitterIndex].config, 84 | ); 85 | 86 | clearZoneGraphic(this.deathZoneDebugGraphics); 87 | clearZoneGraphic(this.emitZoneDebugGraphics); 88 | 89 | if (debugModes.deathZone && configZone.deathZone !== undefined) { 90 | this.deathZoneDebugGraphics = drawDebugZoneGraphic( 91 | configZone.deathZone, 92 | this, 93 | 0x00ff00, 94 | ); 95 | } 96 | if (debugModes.emitZone && configZone.emitZone !== undefined) { 97 | this.emitZoneDebugGraphics = drawDebugZoneGraphic( 98 | configZone.emitZone, 99 | this, 100 | 0x00ffff, 101 | 'EmitZone', 102 | configZone, 103 | ); 104 | } 105 | } 106 | 107 | redraw() { 108 | const { emitters, currentEmitter, emitterIndex } = emitterStore; 109 | const { debugModes } = currentEmitter; 110 | this.syncPhaserEmittersToEmitters(emitters, currentEmitter, emitterIndex); 111 | this.drawDebugZones(debugModes, emitters, emitterIndex); 112 | } 113 | 114 | destroy() { 115 | this.autoRedraw(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/components/ImportProjectFile/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Grid, 4 | WithStyles, 5 | withStyles, 6 | createStyles, 7 | Theme, 8 | TextField, 9 | Typography, 10 | Tooltip, 11 | } from '@material-ui/core'; 12 | import React from 'react'; 13 | import { inject, observer } from 'mobx-react'; 14 | import AttachmentIcon from '@material-ui/icons/Attachment'; 15 | import { EDITOR_STORE, EditorStoreProp } from '../../stores'; 16 | import { ARCHIVE_EXTENSION } from '../../constants'; 17 | 18 | const styles = (theme: Theme) => 19 | createStyles({ 20 | input: { 21 | display: 'none', 22 | }, 23 | button: { 24 | margin: theme.spacing.unit, 25 | marginLeft: 0, 26 | }, 27 | rightIcon: { 28 | marginLeft: theme.spacing.unit, 29 | }, 30 | }); 31 | 32 | @inject(EDITOR_STORE) 33 | @observer 34 | class ImportProjectFile extends React.Component< 35 | EditorStoreProp & WithStyles 36 | > { 37 | handleChange = (event: any) => { 38 | const { setFile } = this.props.editorStore!; 39 | const file = event.target.files[0]; 40 | if (file) { 41 | setFile(file); 42 | } 43 | }; 44 | 45 | render() { 46 | const { editorStore, classes } = this.props; 47 | const { file, fileError, fileErrorText } = editorStore!; 48 | const inputLabel = file ? file.name : ' '; 49 | 50 | return ( 51 | 52 | 53 | 54 | 61 | 79 | 80 | 81 | 82 | 83 | 84 | {fileError && ( 85 | {fileErrorText} 86 | )} 87 | 88 | 89 | 90 | ); 91 | } 92 | } 93 | 94 | type Props = { 95 | onSuccessLoad: () => void; 96 | onReadyResult: (emitters: any) => void; 97 | }; 98 | 99 | @inject(EDITOR_STORE) 100 | @observer 101 | class ImportProjectButton extends React.Component { 102 | handleImport = async () => { 103 | const { editorStore, onSuccessLoad, onReadyResult } = this.props; 104 | const { importProject } = editorStore!; 105 | const result = await importProject(); 106 | if (result) { 107 | onReadyResult(result); 108 | onSuccessLoad(); 109 | } 110 | }; 111 | 112 | render() { 113 | const { editorStore } = this.props; 114 | const { file, fileError, fileLoadingStatus } = editorStore!; 115 | 116 | return ( 117 | 126 | ); 127 | } 128 | } 129 | 130 | export { ImportProjectButton }; 131 | export default withStyles(styles)(ImportProjectFile); 132 | -------------------------------------------------------------------------------- /src/components/ImportBackground/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Grid, 4 | WithStyles, 5 | withStyles, 6 | createStyles, 7 | Theme, 8 | Tooltip, 9 | Typography, 10 | } from '@material-ui/core'; 11 | import React, { Fragment } from 'react'; 12 | import { inject, observer } from 'mobx-react'; 13 | import AttachmentIcon from '@material-ui/icons/Attachment'; 14 | import { EDITOR_STORE, EditorStoreProp } from '../../stores'; 15 | 16 | const styles = (theme: Theme) => 17 | createStyles({ 18 | previewImgRoot: { 19 | position: 'relative', 20 | '&:hover div': { 21 | visibility: 'visible', 22 | }, 23 | }, 24 | removeImage: { 25 | height: theme.spacing.unit * 3, 26 | width: theme.spacing.unit * 3, 27 | top: -theme.spacing.unit, 28 | right: -theme.spacing.unit, 29 | fontFamily: theme.typography.fontFamily, 30 | background: '#d20000', 31 | position: 'absolute', 32 | cursor: 'pointer', 33 | color: '#fff', 34 | borderRadius: '50%', 35 | display: 'flex', 36 | justifyContent: 'center', 37 | alignItems: 'center', 38 | visibility: 'hidden', 39 | }, 40 | input: { 41 | display: 'none', 42 | }, 43 | preview: { 44 | display: 'flex', 45 | justifyContent: 'center', 46 | flexDirection: 'row', 47 | }, 48 | previewImg: { 49 | height: theme.spacing.unit * 12, 50 | boxSizing: 'border-box', 51 | border: `1px dotted ${theme.palette.background.default}`, 52 | }, 53 | button: { 54 | margin: theme.spacing.unit, 55 | marginLeft: 0, 56 | }, 57 | rightIcon: { 58 | marginLeft: theme.spacing.unit, 59 | }, 60 | }); 61 | 62 | @inject(EDITOR_STORE) 63 | @observer 64 | class ImportBackground extends React.Component< 65 | EditorStoreProp & WithStyles 66 | > { 67 | handleChange = (event: any) => { 68 | const { setBackground, setOpenBackgroundDialog } = this.props.editorStore!; 69 | const background = event.target.files[0]; 70 | if (background) { 71 | setOpenBackgroundDialog(false); 72 | setBackground(background); 73 | } 74 | }; 75 | 76 | render() { 77 | const { editorStore, classes } = this.props; 78 | const { background, resetBackground } = editorStore!; 79 | const imageData: any = background.data; 80 | 81 | return ( 82 | 83 | 84 | 85 | 86 | 87 | 94 | 112 | 113 | 114 | {imageData ? ( 115 | 116 | 117 |
118 | 119 |
123 | x 124 |
125 |
126 |
127 | 128 | 129 | {background.size.width}x{background.size.height} 130 | 131 | 132 |
133 | ) : null} 134 |
135 |
136 |
137 | ); 138 | } 139 | } 140 | 141 | export default withStyles(styles)(ImportBackground); 142 | -------------------------------------------------------------------------------- /src/stores/emitterStore.ts: -------------------------------------------------------------------------------- 1 | import _set from 'lodash/set'; 2 | import { action, computed, observable } from 'mobx'; 3 | import { 4 | DEFAULT_DEBUG_MODES, 5 | emitterConfig as emitterInitialConfig, 6 | EMITTER_NAME_PREFIX, 7 | } from '../constants'; 8 | import { deepCopy, getNewEmitterID, hasBoth, hasKey } from '../utils'; 9 | import _isPlainObject from 'lodash/isPlainObject'; 10 | 11 | export class EmitterStore { 12 | @observable 13 | emitters = [ 14 | { 15 | id: 1, 16 | name: `${EMITTER_NAME_PREFIX}1`, 17 | config: emitterInitialConfig, 18 | debugModes: { ...DEFAULT_DEBUG_MODES }, 19 | }, 20 | ]; 21 | 22 | @action.bound 23 | changeDebugMode(configName: string, checked: boolean) { 24 | this.currentEmitter.debugModes[configName] = checked; 25 | } 26 | 27 | @computed 28 | get currentEmitter() { 29 | return this.emitters[this.emitterIndex]; 30 | } 31 | 32 | @computed 33 | get currentEmitterConfig() { 34 | return this.currentEmitter.config; 35 | } 36 | 37 | @observable 38 | lastEmitters: any[]; 39 | 40 | @observable 41 | emitterIndex = 0; 42 | 43 | @action.bound 44 | changeEmitterConfig(configName: string, value: any, index?: number) { 45 | const emitter = 46 | index !== undefined ? this.emitters[index] : this.currentEmitter; 47 | _set(emitter.config, configName.split('>'), value); 48 | } 49 | 50 | @action.bound 51 | changePropertyType(configName: string) { 52 | const currentValue = this.currentEmitter.config[configName]; 53 | const initialValue = emitterInitialConfig[configName]; 54 | const isObjectInitValue = _isPlainObject(initialValue); 55 | const isObjectCurrentValue = _isPlainObject(currentValue); 56 | 57 | let newConfig; 58 | if (isObjectCurrentValue) { 59 | newConfig = isObjectInitValue ? [0] : initialValue; 60 | } else { 61 | newConfig = isObjectInitValue 62 | ? initialValue 63 | : { start: 0, end: 0, ease: 'Linear' }; 64 | } 65 | this.changeEmitterConfig(configName, newConfig); 66 | } 67 | 68 | @action.bound 69 | toggleRandom(configName: string) { 70 | const currentValue = this.currentEmitter.config[configName]; 71 | const newConfig = { ...currentValue }; 72 | 73 | if (hasBoth(currentValue, 'min', 'max')) { 74 | const [start, end] = [newConfig.min, newConfig.max]; 75 | newConfig.start = start; 76 | newConfig.end = end; 77 | delete newConfig.min; 78 | delete newConfig.max; 79 | } else { 80 | const [min, max] = [newConfig.start, newConfig.end]; 81 | newConfig.min = min; 82 | newConfig.max = max; 83 | delete newConfig.start; 84 | delete newConfig.end; 85 | } 86 | this.changeEmitterConfig(configName, newConfig); 87 | } 88 | 89 | @action.bound 90 | changeSelectDropdown(configName: string, value: any) { 91 | if (value === 'Custom') { 92 | this.toggleSteps(configName); 93 | } else { 94 | this.toggleSteps(configName, true); 95 | this.changeEmitterConfig(`${configName}>ease`, value); 96 | } 97 | } 98 | 99 | toggleSteps(configName: string, hide: boolean = false) { 100 | const initialValue = emitterInitialConfig[configName]; 101 | const currentValue = this.currentEmitter.config[configName]; 102 | const newConfig = { ...currentValue }; 103 | 104 | if (hide) { 105 | delete newConfig.steps; 106 | newConfig.ease = hasKey(initialValue, 'ease') 107 | ? initialValue.ease 108 | : 'Linear'; 109 | } else { 110 | delete newConfig.ease; 111 | newConfig.steps = hasKey(initialValue, 'steps') ? initialValue.steps : 10; 112 | } 113 | this.changeEmitterConfig(configName, newConfig); 114 | } 115 | 116 | // emitter position 117 | @action.bound 118 | setEmitterPosition(x: number, y: number) { 119 | this.changeEmitterConfig('x', [parseInt(x as any, 10)]); 120 | this.changeEmitterConfig('y', [parseInt(y as any, 10)]); 121 | } 122 | 123 | @action.bound 124 | removeEmitter(index: number) { 125 | this.emitters.splice(index, 1); 126 | const currentEmitterIndex = index === 0 ? index : index - 1; 127 | this.changeEmitterIndex(currentEmitterIndex); 128 | } 129 | 130 | @action.bound 131 | addEmitter(emitterConfig?: any, prevDebugModes?: any) { 132 | const id = getNewEmitterID(this.emitters); 133 | const name = `${EMITTER_NAME_PREFIX}${id}`; 134 | const config = emitterConfig ? emitterConfig : emitterInitialConfig; 135 | const debugModes = prevDebugModes 136 | ? { ...prevDebugModes } 137 | : { ...DEFAULT_DEBUG_MODES }; 138 | this.emitters.push({ id, name, config, debugModes }); 139 | this.setEmitterIndex(this.emitters.length - 1); 140 | } 141 | 142 | @action.bound 143 | copyEmitter(index: number) { 144 | const { config, debugModes } = this.emitters[index]; 145 | this.addEmitter(deepCopy(config), debugModes); 146 | } 147 | 148 | @action.bound 149 | changeEmitterIndex(index: number) { 150 | this.emitterIndex = index; 151 | } 152 | 153 | @action.bound 154 | setEmitters(emitters: any) { 155 | this.emitters = emitters; 156 | this.setEmitterIndex(0); 157 | } 158 | 159 | setEmitterIndex(index: number) { 160 | this.emitterIndex = index; 161 | } 162 | } 163 | 164 | export interface EmitterStoreProp { 165 | emitterStore?: EmitterStore; 166 | } 167 | 168 | export default new EmitterStore(); 169 | -------------------------------------------------------------------------------- /src/constants/frames.ts: -------------------------------------------------------------------------------- 1 | const frames = [ 2 | { 3 | text: 'Circle 01', 4 | value: 'circle_01', 5 | }, 6 | { 7 | text: 'Circle 02', 8 | value: 'circle_02', 9 | }, 10 | { 11 | text: 'Circle 03', 12 | value: 'circle_03', 13 | }, 14 | { 15 | text: 'Circle 04', 16 | value: 'circle_04', 17 | }, 18 | { 19 | text: 'Circle 05', 20 | value: 'circle_05', 21 | }, 22 | { 23 | text: 'Dirt 01', 24 | value: 'dirt_01', 25 | }, 26 | { 27 | text: 'Dirt 02', 28 | value: 'dirt_02', 29 | }, 30 | { 31 | text: 'Dirt 03', 32 | value: 'dirt_03', 33 | }, 34 | { 35 | text: 'Fire 01', 36 | value: 'fire_01', 37 | }, 38 | { 39 | text: 'Fire 02', 40 | value: 'fire_02', 41 | }, 42 | { 43 | text: 'Flame 01', 44 | value: 'flame_01', 45 | }, 46 | { 47 | text: 'Flame 02', 48 | value: 'flame_02', 49 | }, 50 | { 51 | text: 'Flame 03', 52 | value: 'flame_03', 53 | }, 54 | { 55 | text: 'Flame 04', 56 | value: 'flame_04', 57 | }, 58 | { 59 | text: 'Flame 05', 60 | value: 'flame_05', 61 | }, 62 | { 63 | text: 'Flame 06', 64 | value: 'flame_06', 65 | }, 66 | { 67 | text: 'Light 01', 68 | value: 'light_01', 69 | }, 70 | { 71 | text: 'Light 02', 72 | value: 'light_02', 73 | }, 74 | { 75 | text: 'Light 03', 76 | value: 'light_03', 77 | }, 78 | { 79 | text: 'Magic 01', 80 | value: 'magic_01', 81 | }, 82 | { 83 | text: 'Magic 02', 84 | value: 'magic_02', 85 | }, 86 | { 87 | text: 'Magic 03', 88 | value: 'magic_03', 89 | }, 90 | { 91 | text: 'Magic 04', 92 | value: 'magic_04', 93 | }, 94 | { 95 | text: 'Magic 05', 96 | value: 'magic_05', 97 | }, 98 | { 99 | text: 'Muzzle 01', 100 | value: 'muzzle_01', 101 | }, 102 | { 103 | text: 'Muzzle 02', 104 | value: 'muzzle_02', 105 | }, 106 | { 107 | text: 'Muzzle 03', 108 | value: 'muzzle_03', 109 | }, 110 | { 111 | text: 'Muzzle 04', 112 | value: 'muzzle_04', 113 | }, 114 | { 115 | text: 'Muzzle 05', 116 | value: 'muzzle_05', 117 | }, 118 | { 119 | text: 'Scorch 01', 120 | value: 'scorch_01', 121 | }, 122 | { 123 | text: 'Scorch 02', 124 | value: 'scorch_02', 125 | }, 126 | { 127 | text: 'Scorch 03', 128 | value: 'scorch_03', 129 | }, 130 | { 131 | text: 'Scratch 01', 132 | value: 'scratch_01', 133 | }, 134 | { 135 | text: 'Slash 01', 136 | value: 'slash_01', 137 | }, 138 | { 139 | text: 'Slash 02', 140 | value: 'slash_02', 141 | }, 142 | { 143 | text: 'Slash 03', 144 | value: 'slash_03', 145 | }, 146 | { 147 | text: 'Slash 04', 148 | value: 'slash_04', 149 | }, 150 | { 151 | text: 'Smoke 01', 152 | value: 'smoke_01', 153 | }, 154 | { 155 | text: 'Smoke 02', 156 | value: 'smoke_02', 157 | }, 158 | { 159 | text: 'Smoke 03', 160 | value: 'smoke_03', 161 | }, 162 | { 163 | text: 'Smoke 04', 164 | value: 'smoke_04', 165 | }, 166 | { 167 | text: 'Smoke 05', 168 | value: 'smoke_05', 169 | }, 170 | { 171 | text: 'Smoke 06', 172 | value: 'smoke_06', 173 | }, 174 | { 175 | text: 'Smoke 07', 176 | value: 'smoke_07', 177 | }, 178 | { 179 | text: 'Smoke 08', 180 | value: 'smoke_08', 181 | }, 182 | { 183 | text: 'Smoke 09', 184 | value: 'smoke_09', 185 | }, 186 | { 187 | text: 'Smoke 10', 188 | value: 'smoke_10', 189 | }, 190 | { 191 | text: 'Smoke 11', 192 | value: 'smoke_11', 193 | }, 194 | { 195 | text: 'Spark 01', 196 | value: 'spark_01', 197 | }, 198 | { 199 | text: 'Spark 02', 200 | value: 'spark_02', 201 | }, 202 | { 203 | text: 'Spark 03', 204 | value: 'spark_03', 205 | }, 206 | { 207 | text: 'Spark 04', 208 | value: 'spark_04', 209 | }, 210 | { 211 | text: 'Spark 05', 212 | value: 'spark_05', 213 | }, 214 | { 215 | text: 'Spark 06', 216 | value: 'spark_06', 217 | }, 218 | { 219 | text: 'Spark 07', 220 | value: 'spark_07', 221 | }, 222 | { 223 | text: 'Star 01', 224 | value: 'star_01', 225 | }, 226 | { 227 | text: 'Star 02', 228 | value: 'star_02', 229 | }, 230 | { 231 | text: 'Star 03', 232 | value: 'star_03', 233 | }, 234 | { 235 | text: 'Star 04', 236 | value: 'star_04', 237 | }, 238 | { 239 | text: 'Star 05', 240 | value: 'star_05', 241 | }, 242 | { 243 | text: 'Star 06', 244 | value: 'star_06', 245 | }, 246 | { 247 | text: 'Star 07', 248 | value: 'star_07', 249 | }, 250 | { 251 | text: 'Star 08', 252 | value: 'star_08', 253 | }, 254 | { 255 | text: 'Star 09', 256 | value: 'star_09', 257 | }, 258 | { 259 | text: 'Symbol 01', 260 | value: 'symbol_01', 261 | }, 262 | { 263 | text: 'Symbol 02', 264 | value: 'symbol_02', 265 | }, 266 | { 267 | text: 'Trace 01', 268 | value: 'trace_01', 269 | }, 270 | { 271 | text: 'Trace 02', 272 | value: 'trace_02', 273 | }, 274 | { 275 | text: 'Trace 03', 276 | value: 'trace_03', 277 | }, 278 | { 279 | text: 'Trace 04', 280 | value: 'trace_04', 281 | }, 282 | { 283 | text: 'Trace 05', 284 | value: 'trace_05', 285 | }, 286 | { 287 | text: 'Trace 06', 288 | value: 'trace_06', 289 | }, 290 | { 291 | text: 'Trace 07', 292 | value: 'trace_07', 293 | }, 294 | { 295 | text: 'Twirl 01', 296 | value: 'twirl_01', 297 | }, 298 | { 299 | text: 'Twirl 02', 300 | value: 'twirl_02', 301 | }, 302 | { 303 | text: 'Twirl 03', 304 | value: 'twirl_03', 305 | }, 306 | { 307 | text: 'Window 01', 308 | value: 'window_01', 309 | }, 310 | { 311 | text: 'Window 02', 312 | value: 'window_02', 313 | }, 314 | { 315 | text: 'Window 03', 316 | value: 'window_03', 317 | }, 318 | ]; 319 | 320 | export default frames; 321 | -------------------------------------------------------------------------------- /src/components/NewProjectModal/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Dialog, 4 | DialogActions, 5 | DialogContent, 6 | DialogTitle, 7 | TextField, 8 | Grid, 9 | DialogContentText, 10 | Tabs, 11 | Tab, 12 | AppBar, 13 | createStyles, 14 | Theme, 15 | WithStyles, 16 | withStyles, 17 | } from '@material-ui/core'; 18 | import React from 'react'; 19 | import { inject, observer } from 'mobx-react'; 20 | import { EditorStoreProp } from '../../stores/editorStore'; 21 | import CreateIcon from '@material-ui/icons/Create'; 22 | import FolderOpenIcon from '@material-ui/icons/FolderOpen'; 23 | import { Fragment } from 'react'; 24 | import ImportProjectFile, { ImportProjectButton } from '../ImportProjectFile'; 25 | import { EDITOR_STORE, EMITTER_STORE, EmitterStoreProp } from '../../stores'; 26 | import ImportBackground from '../ImportBackground'; 27 | 28 | enum Project { 29 | Create, 30 | Upload, 31 | } 32 | 33 | const styles = (theme: Theme) => 34 | createStyles({ 35 | input: { 36 | display: 'none', 37 | }, 38 | button: { 39 | margin: theme.spacing.unit, 40 | }, 41 | leftIcon: { 42 | marginRight: theme.spacing.unit, 43 | }, 44 | rightIcon: { 45 | marginLeft: theme.spacing.unit, 46 | }, 47 | }); 48 | 49 | @inject(EDITOR_STORE, EMITTER_STORE) 50 | @observer 51 | class NewProjectModal extends React.Component< 52 | WithStyles & EditorStoreProp & EmitterStoreProp 53 | > { 54 | state = { 55 | value: Project.Create, 56 | }; 57 | 58 | handleChange = (event: any, value: Project) => { 59 | this.setState({ value }); 60 | }; 61 | 62 | getBodyCreate() { 63 | const { 64 | name, 65 | height, 66 | width, 67 | changeConfig, 68 | setError, 69 | } = this.props.editorStore!; 70 | 71 | return ( 72 | 73 | 74 | Lets start something new. 75 | 76 | 77 | { 84 | setError('name', false); 85 | }} 86 | onChange={event => changeConfig('name', event.target.value)} 87 | /> 88 | 89 | 90 | { 97 | setError('width', false); 98 | }} 99 | onChange={event => changeConfig('width', event.target.value)} 100 | /> 101 | 102 | 103 | { 110 | setError('height', false); 111 | }} 112 | onChange={event => changeConfig('height', event.target.value)} 113 | /> 114 | 115 | 116 | 117 | 118 | 119 | ); 120 | } 121 | 122 | getBodyUpload() { 123 | return ( 124 | 125 | 126 | 127 | Lets continue something awesome. 128 | 129 | 130 | 131 | 132 | 133 | 134 | ); 135 | } 136 | 137 | handleKeyDown = (event: any) => { 138 | const { setStatus } = this.props.editorStore!; 139 | if (event.keyCode === 13 && this.state.value === Project.Create) { 140 | setStatus(true); 141 | } 142 | }; 143 | 144 | render() { 145 | const { value } = this.state; 146 | const { editorStore, emitterStore } = this.props; 147 | const { setStatus, setEditorProps, background } = editorStore!; 148 | const { setEmitters } = emitterStore!; 149 | return ( 150 | 155 | 156 | {value === Project.Create ? 'Create Project' : 'Open Project'} 157 | 158 | 159 | 160 | 161 | 162 | 169 | } value={Project.Create} /> 170 | } value={Project.Upload} /> 171 | 172 | 173 | 174 | {value === Project.Create 175 | ? this.getBodyCreate() 176 | : this.getBodyUpload()} 177 | 178 | 179 | 180 | {value === Project.Create ? ( 181 | 190 | ) : ( 191 | setStatus(true)} 193 | onReadyResult={result => { 194 | setEditorProps(result.editor); 195 | setEmitters(result.emitters); 196 | }} 197 | /> 198 | )} 199 | 200 | 201 | ); 202 | } 203 | } 204 | 205 | export default withStyles(styles)(NewProjectModal); 206 | -------------------------------------------------------------------------------- /src/stores/editorStore.ts: -------------------------------------------------------------------------------- 1 | import { action, observable } from 'mobx'; 2 | import { 3 | validateForm, 4 | getFileExtension, 5 | validateZip, 6 | getImageSize, 7 | } from '../utils'; 8 | import JSZip from 'jszip'; 9 | import { ARCHIVE_EXTENSION, ATLAS_FILE_NAME } from '../constants'; 10 | 11 | export class EditorStore { 12 | @observable 13 | created: boolean; 14 | 15 | @observable 16 | openSaveDialog: boolean = false; 17 | 18 | @observable 19 | openExportDialog: boolean = false; 20 | 21 | @observable 22 | openImportDialog: boolean = false; 23 | 24 | @observable 25 | openBackgroundDialog: boolean = false; 26 | 27 | @observable 28 | exportHiddenEmitters: boolean = false; 29 | 30 | @observable 31 | background = { 32 | data: null, 33 | loading: false, 34 | size: { 35 | width: 0, 36 | height: 0, 37 | }, 38 | }; 39 | 40 | @observable 41 | name = { 42 | value: 'MyProject', 43 | error: false, 44 | }; 45 | 46 | @observable 47 | height = { 48 | value: 600, 49 | error: false, 50 | }; 51 | 52 | @observable 53 | width = { 54 | value: 800, 55 | error: false, 56 | }; 57 | 58 | @observable 59 | file: any = null; 60 | 61 | @observable 62 | fileError = false; 63 | 64 | @observable 65 | fileErrorText = ''; 66 | 67 | @observable 68 | fileLoadingStatus = false; 69 | 70 | @action.bound 71 | setEditorProps(editorProps: any) { 72 | if (editorProps.backgroundData) { 73 | this.background.data = editorProps.backgroundData.data; 74 | this.background.size.width = editorProps.backgroundData.width; 75 | this.background.size.height = editorProps.backgroundData.height; 76 | } else { 77 | this.resetBackground(); 78 | } 79 | this.width.value = editorProps.width; 80 | this.height.value = editorProps.height; 81 | this.name.value = editorProps.name; 82 | } 83 | 84 | @action.bound 85 | setInitialImportProps() { 86 | this.setFile(null); 87 | this.setFileError(false); 88 | this.setFileLoadingStatus(false); 89 | } 90 | 91 | setFileErrorText(text: string) { 92 | this.fileErrorText = text; 93 | } 94 | 95 | setFileLoadingStatus(status: boolean) { 96 | this.fileLoadingStatus = status; 97 | } 98 | 99 | @action.bound 100 | setStatus(status: boolean) { 101 | validateForm(this, () => (this.created = status), this.setError.bind(this)); 102 | } 103 | 104 | @action.bound 105 | changeConfig(configName: string, value: string) { 106 | this[configName].value = value; 107 | } 108 | 109 | @action.bound 110 | setError(configName: string, errorStatus: boolean = true) { 111 | this[configName].error = errorStatus; 112 | } 113 | 114 | @action.bound 115 | setOpenExportDialog(value: boolean) { 116 | this.openExportDialog = value; 117 | } 118 | 119 | @action.bound 120 | setOpenSaveDialog(value: boolean) { 121 | this.openSaveDialog = value; 122 | } 123 | 124 | @action.bound 125 | setOpenImportDialog(value: boolean) { 126 | this.openImportDialog = value; 127 | } 128 | 129 | @action.bound 130 | setOpenBackgroundDialog(value: boolean) { 131 | this.openBackgroundDialog = value; 132 | } 133 | 134 | @action.bound 135 | setExportHiddenEmitters(value: boolean) { 136 | this.exportHiddenEmitters = value; 137 | } 138 | 139 | @action.bound 140 | setFile(file: any) { 141 | this.setFileLoadingStatus(false); 142 | this.file = file; 143 | if (file !== null) { 144 | this.setFileError(getFileExtension(file.name) !== ARCHIVE_EXTENSION); 145 | this.setFileErrorText(`Invalid file - ${this.file && this.file.name}`); 146 | } 147 | } 148 | 149 | @action.bound 150 | resetBackground() { 151 | this.background = { 152 | data: null, 153 | loading: false, 154 | size: { 155 | width: 0, 156 | height: 0, 157 | }, 158 | }; 159 | } 160 | 161 | @action.bound 162 | setBackground(background: any) { 163 | if (!/\.(jpe?g|png)$/i.test(background.name)) { 164 | this.resetBackground(); 165 | } else { 166 | this.loadBackground(background); 167 | } 168 | } 169 | 170 | loadBackground(background: any) { 171 | console.log('loadBackground'); 172 | this.background.loading = true; 173 | const reader = new FileReader(); 174 | reader.readAsDataURL(background); 175 | reader.onload = (e: any) => { 176 | const base64 = e.target.result; 177 | getImageSize(base64, (width: number, height: number) => { 178 | this.background = { 179 | data: base64, 180 | loading: false, 181 | size: { 182 | width, 183 | height, 184 | }, 185 | }; 186 | }); 187 | }; 188 | } 189 | 190 | setFileError(value: boolean) { 191 | this.fileError = value; 192 | } 193 | 194 | loadProject(file: any) { 195 | return JSZip.loadAsync(file); 196 | } 197 | 198 | onLoadSuccess(zip: any) { 199 | const result = Object.keys(zip.files).map(fileName => { 200 | const file = zip.files[fileName]; 201 | const asyncType = /\.(jpe?g|png)$/i.test(file.name) 202 | ? 'uint8array' 203 | : 'text'; 204 | return { 205 | value: null, 206 | name: fileName, 207 | promise: file.async(asyncType), 208 | }; 209 | }); 210 | 211 | const promises = result.map(fileInfo => fileInfo.promise); 212 | 213 | return Promise.all(promises).then(values => { 214 | const filtredResult = result.map((fileInfo, i) => ({ 215 | value: values[i], 216 | name: fileInfo.name, 217 | })); 218 | return filtredResult; 219 | }); 220 | } 221 | 222 | @action.bound 223 | async importProject() { 224 | this.setFileLoadingStatus(true); 225 | try { 226 | const zip = await this.loadProject(this.file); 227 | this.setFileLoadingStatus(false); 228 | const isValidZip = validateZip(zip); 229 | 230 | if (isValidZip) { 231 | try { 232 | const zipResult = await this.onLoadSuccess(zip); 233 | return zipResult.reduce( 234 | (acc: any, fileData) => { 235 | const { value, name } = fileData; 236 | switch (name) { 237 | case `${ATLAS_FILE_NAME}.png`: { 238 | acc.atlas.image = value; 239 | break; 240 | } 241 | case `${ATLAS_FILE_NAME}.json`: { 242 | acc.atlas.json = JSON.parse(value); 243 | break; 244 | } 245 | case 'emitters.json': { 246 | acc.emitters = JSON.parse(value); 247 | break; 248 | } 249 | case 'editor.json': { 250 | acc.editor = JSON.parse(value); 251 | break; 252 | } 253 | default: 254 | break; 255 | } 256 | return acc; 257 | }, 258 | { atlas: { image: null, json: null } }, 259 | ); 260 | } catch (err) { 261 | console.error(err); 262 | this.setFileError(true); 263 | this.setFileErrorText(`Invalid content!`); 264 | } 265 | } else { 266 | this.setFileError(true); 267 | this.setFileErrorText(`Invalid .${ARCHIVE_EXTENSION} file!`); 268 | } 269 | } catch (err) { 270 | console.error(err); 271 | this.setFileError(true); 272 | this.setFileErrorText('Loading error!'); 273 | } 274 | 275 | return false; 276 | } 277 | } 278 | 279 | export interface EditorStoreProp { 280 | editorStore?: EditorStore; 281 | } 282 | 283 | export default new EditorStore(); 284 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { saveAs } from 'file-saver'; 2 | import JSZip from 'jszip'; 3 | import shapesIMAGE from './canvas/assets/shapes.png'; 4 | import shapesJSON from './canvas/assets/shapes.json'; 5 | import _isEqual from 'lodash/isEqual'; 6 | 7 | import { 8 | emitterConfig as emitterInitialConfig, 9 | zoneEdgeSources, 10 | ATLAS_FILE_NAME, 11 | ARCHIVE_EXTENSION, 12 | } from './constants'; 13 | 14 | const validateForm = ( 15 | { name, height, width }: { name: any; height: any; width: any }, 16 | onSuccess: () => {}, 17 | onFail: (field: string) => {}, 18 | ) => { 19 | let isValid = true; 20 | 21 | if (!isValidName(name.value)) { 22 | onFail('name'); 23 | isValid = false; 24 | } 25 | 26 | if (!isValidSize(Number(height.value))) { 27 | onFail('height'); 28 | isValid = false; 29 | } 30 | 31 | if (!isValidSize(Number(width.value))) { 32 | onFail('width'); 33 | isValid = false; 34 | } 35 | 36 | if (isValid) { 37 | onSuccess(); 38 | } 39 | }; 40 | 41 | const isValidName = (name: string) => { 42 | return name !== ''; 43 | }; 44 | 45 | const isValidSize = (value: number) => { 46 | return value > 0; 47 | }; 48 | 49 | const getEmitterConfig = ( 50 | config: any, 51 | onSourceChange?: (source: any, shapeProps: any) => {}, 52 | ) => { 53 | const newConfig = {}; 54 | for (const key in config) { 55 | if (config.hasOwnProperty(key)) { 56 | const value = config[key]; 57 | if (value === undefined) { 58 | continue; 59 | } 60 | if (value.hasOwnProperty('source')) { 61 | newConfig[key] = { 62 | ...value, 63 | source: new Phaser.Geom[value.shapeType]( 64 | ...Object.values(value.source), 65 | ), 66 | }; 67 | if (typeof onSourceChange === 'function') { 68 | newConfig[key] = onSourceChange(newConfig[key], value.source); 69 | } 70 | } else { 71 | newConfig[key] = 72 | Array.isArray(value) && value.length === 1 && key !== 'tint' 73 | ? value[0] 74 | : value; 75 | } 76 | } 77 | } 78 | return newConfig; 79 | }; 80 | 81 | const getNewEmitterID = (emitters: any) => { 82 | const IDs = emitters 83 | .map((emitter: any) => emitter.id) 84 | .sort((a: any, b: any) => a - b); 85 | let emitterID = null; 86 | 87 | if (IDs[0] !== 1) { 88 | emitterID = 1; 89 | } else { 90 | for (let i = 1; i < IDs.length; i++) { 91 | if (IDs[i] !== IDs[i - 1] + 1) { 92 | emitterID = IDs[i - 1] + 1; 93 | break; 94 | } 95 | } 96 | if (!emitterID) { 97 | emitterID = IDs.length + 1; 98 | } 99 | } 100 | 101 | return emitterID; 102 | }; 103 | 104 | const initialConfig = getEmitterConfig(emitterInitialConfig); 105 | 106 | const hasKey = (object: {}, key: string) => object.hasOwnProperty(key); 107 | 108 | const hasBoth = (object: {}, key1: string, key2: string) => 109 | object.hasOwnProperty(key1) && object.hasOwnProperty(key2); 110 | 111 | const deepCopy = (object: {}) => JSON.parse(JSON.stringify(object)); 112 | 113 | const getPickerColor = (color: number) => { 114 | let result = `${color.toString(16)}`; 115 | const zeroCount = 6 - result.length; 116 | result = `#${'0'.repeat(zeroCount)}${result}`; 117 | return result; 118 | }; 119 | 120 | const getEmitterIndex = (newEmitters: any, prevEmitters: any) => { 121 | const newEmittersCopy = deepCopy(newEmitters); 122 | const prevEmittersCopy = deepCopy(prevEmitters); 123 | const maxLength = Math.max(newEmittersCopy.length, prevEmittersCopy.length); 124 | let index = -1; 125 | for (let i = 0; i < maxLength; i++) { 126 | if ( 127 | JSON.stringify(newEmittersCopy[i]) !== JSON.stringify(prevEmittersCopy[i]) 128 | ) { 129 | index = i; 130 | break; 131 | } 132 | } 133 | return index; 134 | }; 135 | 136 | const saveProject = async (props: any) => { 137 | const { canvasSize, name, emitters, backgroundData } = props; 138 | const shapeData = await fetch(shapesIMAGE); 139 | const shapeBuffer = await shapeData.arrayBuffer(); 140 | const editor = { ...canvasSize, name, backgroundData }; 141 | 142 | saveZip({ 143 | zipName: name, 144 | extension: ARCHIVE_EXTENSION, 145 | jsonFiles: [ 146 | { 147 | fileName: ATLAS_FILE_NAME, 148 | json: JSON.stringify(shapesJSON), 149 | }, 150 | { 151 | fileName: 'emitters', 152 | json: JSON.stringify(emitters), 153 | }, 154 | { 155 | fileName: 'editor', 156 | json: JSON.stringify(editor), 157 | }, 158 | ], 159 | pngFiles: [ 160 | { 161 | fileName: ATLAS_FILE_NAME, 162 | buffer: shapeBuffer, 163 | }, 164 | ], 165 | }); 166 | }; 167 | 168 | const exportProject = async ( 169 | name: string, 170 | emitters: any[], 171 | exportHidden: boolean, 172 | ) => { 173 | const zoneSources: any[] = []; 174 | let configs: any = emitters.map(emitter => 175 | getEmitterConfig(emitter.config, (source: any, shapeProps: any) => { 176 | const sourceCopy = { ...source }; 177 | sourceCopy.source = `new Phaser.Geom.${source.shapeType}(${[ 178 | Object.values(shapeProps), 179 | ]})`; 180 | delete sourceCopy.shapeType; 181 | zoneSources.push(sourceCopy.source); 182 | return sourceCopy; 183 | }), 184 | ); 185 | 186 | if (exportHidden === false) { 187 | configs = configs.filter((config: any) => config.visible); 188 | } 189 | 190 | let configsJSON = JSON.stringify(configs); 191 | zoneSources.forEach((source: string) => { 192 | configsJSON = configsJSON.replace(`"${source}"`, `${source}`); 193 | }); 194 | 195 | const shapeData = await fetch(shapesIMAGE); 196 | const shapeBuffer = await shapeData.arrayBuffer(); 197 | 198 | saveZip({ 199 | zipName: name, 200 | extension: 'zip', 201 | jsonFiles: [ 202 | { 203 | fileName: ATLAS_FILE_NAME, 204 | json: JSON.stringify(shapesJSON), 205 | }, 206 | { 207 | fileName: name, 208 | json: configsJSON, 209 | }, 210 | ], 211 | pngFiles: [ 212 | { 213 | fileName: ATLAS_FILE_NAME, 214 | buffer: shapeBuffer, 215 | }, 216 | ], 217 | }); 218 | }; 219 | 220 | interface SaveZipProps { 221 | zipName: string; 222 | extension: string; 223 | jsonFiles: any[]; 224 | pngFiles: any[]; 225 | } 226 | 227 | const saveZip = (config: SaveZipProps) => { 228 | let { zipName, jsonFiles, pngFiles, extension } = config; 229 | zipName = zipName === 'shapes' ? 'particle_shapes' : zipName; 230 | 231 | const zip = new JSZip(); 232 | 233 | pngFiles.forEach((pngFile: any) => { 234 | zip.file(`${pngFile.fileName}.png`, pngFile.buffer); 235 | }); 236 | 237 | jsonFiles.forEach((jsonFile: any) => { 238 | zip.file(`${jsonFile.fileName}.json`, jsonFile.json); 239 | }); 240 | 241 | zip.generateAsync({ type: 'blob' }).then( 242 | (blob: any) => { 243 | saveAs(blob, `${zipName}.${extension}`); 244 | }, 245 | (err: any) => { 246 | console.error(err); 247 | }, 248 | ); 249 | }; 250 | 251 | const getZoneShapeProps = (type: string) => { 252 | return zoneEdgeSources.find(({ shapeType }) => shapeType === type); 253 | }; 254 | 255 | const getFileExtension = (fileName: string) => { 256 | return fileName.slice(fileName.lastIndexOf('.') + 1); 257 | }; 258 | 259 | const validateZip = (zip: any) => { 260 | const validKeys = [ 261 | `${ATLAS_FILE_NAME}.png`, 262 | `${ATLAS_FILE_NAME}.json`, 263 | 'emitters.json', 264 | 'editor.json', 265 | ].sort(); 266 | const keys = Object.keys(zip.files).sort(); 267 | return _isEqual(validKeys, keys); 268 | }; 269 | 270 | const execute = (jsonString: string) => { 271 | return new Function(`return ${jsonString}`)(); 272 | }; 273 | 274 | const getImageSize = (base64: any, cb: any) => { 275 | const image = new Image(); 276 | image.src = base64; 277 | 278 | image.onload = () => { 279 | cb(image.width, image.height); 280 | }; 281 | }; 282 | 283 | export { 284 | getImageSize, 285 | execute, 286 | getFileExtension, 287 | validateZip, 288 | hasBoth, 289 | deepCopy, 290 | hasKey, 291 | getPickerColor, 292 | getEmitterIndex, 293 | getEmitterConfig, 294 | getNewEmitterID, 295 | saveZip, 296 | initialConfig, 297 | saveProject, 298 | validateForm, 299 | getZoneShapeProps, 300 | exportProject, 301 | }; 302 | -------------------------------------------------------------------------------- /raw/shapes.tps: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | fileFormatVersion 5 | 4 6 | texturePackerVersion 7 | 4.8.3 8 | autoSDSettings 9 | 10 | 11 | scale 12 | 1 13 | extension 14 | 15 | spriteFilter 16 | 17 | acceptFractionalValues 18 | 19 | maxTextureSize 20 | 21 | width 22 | -1 23 | height 24 | -1 25 | 26 | 27 | 28 | allowRotation 29 | 30 | shapeDebug 31 | 32 | dpi 33 | 72 34 | dataFormat 35 | phaser 36 | textureFileName 37 | 38 | flipPVR 39 | 40 | pvrCompressionQuality 41 | PVR_QUALITY_NORMAL 42 | atfCompressData 43 | 44 | mipMapMinSize 45 | 32768 46 | etc1CompressionQuality 47 | ETC1_QUALITY_LOW_PERCEPTUAL 48 | etc2CompressionQuality 49 | ETC2_QUALITY_LOW_PERCEPTUAL 50 | dxtCompressionMode 51 | DXT_PERCEPTUAL 52 | jxrColorFormat 53 | JXR_YUV444 54 | jxrTrimFlexBits 55 | 0 56 | jxrCompressionLevel 57 | 0 58 | ditherType 59 | PngQuantLow 60 | backgroundColor 61 | 0 62 | libGdx 63 | 64 | filtering 65 | 66 | x 67 | Linear 68 | y 69 | Linear 70 | 71 | 72 | shapePadding 73 | 0 74 | jpgQuality 75 | 80 76 | pngOptimizationLevel 77 | 1 78 | webpQualityLevel 79 | 101 80 | textureSubPath 81 | 82 | atfFormats 83 | 84 | textureFormat 85 | png8 86 | borderPadding 87 | 0 88 | maxTextureSize 89 | 90 | width 91 | 2048 92 | height 93 | 2048 94 | 95 | fixedTextureSize 96 | 97 | width 98 | -1 99 | height 100 | -1 101 | 102 | algorithmSettings 103 | 104 | algorithm 105 | MaxRects 106 | freeSizeMode 107 | Best 108 | sizeConstraints 109 | AnySize 110 | forceSquared 111 | 112 | maxRects 113 | 114 | heuristic 115 | Best 116 | 117 | basic 118 | 119 | sortBy 120 | Best 121 | order 122 | Ascending 123 | 124 | polygon 125 | 126 | alignToGrid 127 | 1 128 | 129 | 130 | dataFileNames 131 | 132 | json 133 | 134 | name 135 | ../src/canvas/assets/shapes.json 136 | 137 | 138 | multiPack 139 | 140 | forceIdenticalLayout 141 | 142 | outputFormat 143 | RGBA8888 144 | alphaHandling 145 | ClearTransparentPixels 146 | contentProtection 147 | 148 | key 149 | 150 | 151 | autoAliasEnabled 152 | 153 | trimSpriteNames 154 | 155 | prependSmartFolderName 156 | 157 | autodetectAnimations 158 | 159 | globalSpriteSettings 160 | 161 | scale 162 | 1 163 | scaleMode 164 | Smooth 165 | extrude 166 | 1 167 | trimThreshold 168 | 1 169 | trimMargin 170 | 1 171 | trimMode 172 | Crop 173 | tracerTolerance 174 | 200 175 | heuristicMask 176 | 177 | defaultPivotPoint 178 | 0.5,0.5 179 | writePivotPoints 180 | 181 | 182 | individualSpriteSettings 183 | 184 | circle_01.png 185 | circle_02.png 186 | circle_03.png 187 | circle_04.png 188 | circle_05.png 189 | dirt_01.png 190 | dirt_02.png 191 | dirt_03.png 192 | fire_01.png 193 | flare_01.png 194 | flare_02.png 195 | flare_03.png 196 | flare_04.png 197 | flare_05.png 198 | flare_06.png 199 | flare_07.png 200 | light_01.png 201 | light_02.png 202 | light_03.png 203 | magic_01.png 204 | magic_02.png 205 | magic_03.png 206 | magic_04.png 207 | magic_05.png 208 | muzzle_01.png 209 | muzzle_02.png 210 | muzzle_03.png 211 | muzzle_04.png 212 | muzzle_05.png 213 | scorch_01.png 214 | scorch_02.png 215 | scorch_03.png 216 | scratch_01.png 217 | slash_01.png 218 | slash_02.png 219 | slash_03.png 220 | slash_04.png 221 | smoke_01.png 222 | smoke_02.png 223 | smoke_03.png 224 | smoke_04.png 225 | smoke_05.png 226 | smoke_06.png 227 | smoke_07.png 228 | smoke_08.png 229 | smoke_09.png 230 | smoke_11.png 231 | spark_01.png 232 | spark_02.png 233 | spark_03.png 234 | spark_04.png 235 | spark_05.png 236 | spark_06.png 237 | spark_07.png 238 | star_01.png 239 | star_02.png 240 | star_03.png 241 | star_04.png 242 | star_05.png 243 | star_06.png 244 | star_07.png 245 | star_08.png 246 | star_09.png 247 | symbol_01.png 248 | symbol_02.png 249 | trace_01.png 250 | trace_02.png 251 | trace_03.png 252 | trace_04.png 253 | trace_05.png 254 | trace_06.png 255 | trace_07.png 256 | twirl_01.png 257 | twirl_02.png 258 | twirl_03.png 259 | window_01.png 260 | window_02.png 261 | window_03.png 262 | window_04.png 263 | 264 | pivotPoint 265 | 0.5,0.5 266 | scale9Enabled 267 | 268 | scale9Borders 269 | 13,13,25,25 270 | scale9Paddings 271 | 13,13,25,25 272 | scale9FromFile 273 | 274 | 275 | fire_02.png 276 | 277 | pivotPoint 278 | 0.5,0.5 279 | scale9Enabled 280 | 281 | scale9Borders 282 | 10,11,20,21 283 | scale9Paddings 284 | 10,11,20,21 285 | scale9FromFile 286 | 287 | 288 | flame_01.png 289 | 290 | pivotPoint 291 | 0.5,0.5 292 | scale9Enabled 293 | 294 | scale9Borders 295 | 9,9,18,19 296 | scale9Paddings 297 | 9,9,18,19 298 | scale9FromFile 299 | 300 | 301 | flame_02.png 302 | 303 | pivotPoint 304 | 0.5,0.5 305 | scale9Enabled 306 | 307 | scale9Borders 308 | 11,11,21,21 309 | scale9Paddings 310 | 11,11,21,21 311 | scale9FromFile 312 | 313 | 314 | flame_03.png 315 | 316 | pivotPoint 317 | 0.5,0.5 318 | scale9Enabled 319 | 320 | scale9Borders 321 | 8,9,15,19 322 | scale9Paddings 323 | 8,9,15,19 324 | scale9FromFile 325 | 326 | 327 | flame_04.png 328 | 329 | pivotPoint 330 | 0.5,0.5 331 | scale9Enabled 332 | 333 | scale9Borders 334 | 10,9,20,19 335 | scale9Paddings 336 | 10,9,20,19 337 | scale9FromFile 338 | 339 | 340 | flame_05.png 341 | 342 | pivotPoint 343 | 0.5,0.5 344 | scale9Enabled 345 | 346 | scale9Borders 347 | 2,7,5,13 348 | scale9Paddings 349 | 2,7,5,13 350 | scale9FromFile 351 | 352 | 353 | flame_06.png 354 | 355 | pivotPoint 356 | 0.5,0.5 357 | scale9Enabled 358 | 359 | scale9Borders 360 | 3,8,7,15 361 | scale9Paddings 362 | 3,8,7,15 363 | scale9FromFile 364 | 365 | 366 | smoke_10.png 367 | 368 | pivotPoint 369 | 0.5,0.5 370 | scale9Enabled 371 | 372 | scale9Borders 373 | 10,10,19,19 374 | scale9Paddings 375 | 10,10,19,19 376 | scale9FromFile 377 | 378 | 379 | 380 | fileList 381 | 382 | . 383 | 384 | ignoreFileList 385 | 386 | replaceList 387 | 388 | ignoredWarnings 389 | 390 | commonDivisorX 391 | 1 392 | commonDivisorY 393 | 1 394 | packNormalMaps 395 | 396 | autodetectNormalMaps 397 | 398 | normalMapFilter 399 | 400 | normalMapSuffix 401 | 402 | normalMapSheetFileName 403 | 404 | exporterProperties 405 | 406 | 407 | 408 | --------------------------------------------------------------------------------