├── .babelrc ├── .circleci └── config.yml ├── .codeclimate.yml ├── .codesandbox └── ci.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .nvmrc ├── .prettierrc.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── config ├── jest.dev.json ├── jest.prod.json ├── jest │ ├── setupGlobals.js │ └── setupPixi.js └── rollup.config.js ├── examples ├── .env ├── .gitignore ├── README.md ├── link.sh ├── package-lock.json ├── package.json ├── public │ ├── bunnys.png │ ├── favicon.ico │ ├── index.html │ └── manifest.json └── src │ ├── AnimatedExample │ ├── AnimatedSprite.js │ ├── animatedPixiTarget.js │ └── index.js │ ├── App │ ├── App.css │ └── App.js │ ├── Bunny │ ├── bunnys.png │ └── index.js │ ├── BunnyExample │ └── index.js │ ├── BunnymarkExample │ ├── Bunnymark.js │ ├── bunnys.png │ └── index.js │ ├── CanvasPropsExample │ └── index.js │ ├── ClickExample │ └── index.js │ ├── CustomApplicationExample │ └── index.js │ ├── CustomBunnymarkExample │ ├── CustomBunnymark.js │ ├── Particle.js │ ├── bunnys.png │ └── index.js │ ├── CustomPIXIComponentExample │ ├── Circle.js │ ├── DraggableContainer.js │ ├── Rect.js │ ├── index.js │ └── useInterval.js │ ├── CustomPIXIPropertyExample │ └── index.js │ ├── ExampleList │ └── index.js │ ├── HooksExample │ └── index.js │ ├── LayersExample │ ├── ColoredBunny.js │ ├── Layer.js │ ├── LayeredStage.js │ └── index.js │ ├── PointsExample │ └── PointsExample.js │ ├── RotatingBunny │ └── index.js │ ├── SmokeTest │ └── index.js │ ├── Stats │ └── index.js │ ├── SuspenseExample │ ├── TextureRenderer.js │ ├── bunnys.png │ └── index.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ └── registerServiceWorker.js ├── index.d.ts ├── index.es.js ├── index.js ├── index.umd.js ├── mapCoverage.js ├── package-lock.json ├── package.json ├── react-pixi-alias.js ├── react-pixi.svg ├── src ├── AppProvider.js ├── CustomPIXIComponent.js ├── PixiProperty.js ├── PixiPropertyOperations.js ├── ReactGlobalSharedState.js ├── ReactPixiFiber.js ├── ReactPixiFiberComponent.js ├── ReactPixiFiberUnknownPropertyHook.js ├── Stage │ ├── common.js │ ├── hooks.js │ ├── index.js │ ├── legacy.js │ └── propTypes.js ├── compat.js ├── hooks.js ├── index.js ├── inject.js ├── possibleStandardNames.js ├── propTypes.js ├── props.js ├── react-pixi-alias │ └── index.js ├── render.js ├── types.js └── utils.js ├── test ├── AppProvider.test.js ├── CustomPIXIComponent.test.js ├── PixiProperty.test.js ├── PixiPropertyOperations.test.js ├── ReactPixiFiber.test.js ├── ReactPixiFiberComponent.test.js ├── ReactPixiFiberUnknownPropertyHook.test.js ├── Stage │ ├── hooks.test.js │ ├── index.test.js │ ├── legacy.test.js │ └── propTypes.test.js ├── __snapshots__ │ ├── index.test.js.snap │ └── props.test.js.snap ├── hooks.test.js ├── index.test.js ├── inject.test.js ├── propTypes.test.js ├── props.test.js ├── react-pixi-alias │ └── index.test.js ├── render.test.js ├── types.test.js ├── typescript │ └── index.tsx └── utils.test.js ├── tsconfig.json └── wallaby.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": ["@babel/preset-env", "@babel/preset-react"], 5 | "plugins": ["rewire", "@babel/plugin-syntax-dynamic-import"] 6 | } 7 | }, 8 | "plugins": ["@babel/plugin-syntax-dynamic-import"], 9 | "presets": [ 10 | [ 11 | "@babel/preset-env", 12 | { 13 | "modules": false, 14 | "targets": { 15 | "browsers": ["last 2 versions"] 16 | } 17 | } 18 | ], 19 | "@babel/preset-react" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: cimg/node:18.16.0 6 | 7 | working_directory: ~/react-pixi-fiber 8 | 9 | steps: 10 | - checkout 11 | 12 | # Download and cache dependencies 13 | - restore_cache: 14 | keys: 15 | - npm-dependencies-{{ checksum ".nvmrc" }}-{{ checksum "package-lock.json" }} 16 | # fallback to using the latest cache if no exact match is found 17 | - npm-dependencies- 18 | 19 | - run: npm ci 20 | 21 | # Cache dependencies 22 | - save_cache: 23 | paths: 24 | - node_modules 25 | key: npm-dependencies-{{ checksum ".nvmrc" }}-{{ checksum "package-lock.json" }} 26 | 27 | # run build 28 | - run: npm run build 29 | 30 | # run tests 31 | - run: npm run test --coverage --ci --silent 32 | - run: npm run test:ts 33 | 34 | # upload coverage report 35 | # TODO: Migrate to new uploader 36 | # - run: npm exec codecov 37 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | checks: 4 | argument-count: 5 | config: 6 | threshold: 4 7 | complex-logic: 8 | config: 9 | threshold: 4 10 | file-lines: 11 | config: 12 | threshold: 250 13 | method-complexity: 14 | config: 15 | threshold: 5 16 | method-count: 17 | config: 18 | threshold: 20 19 | method-lines: 20 | config: 21 | threshold: 30 22 | nested-control-flow: 23 | config: 24 | threshold: 4 25 | return-statements: 26 | config: 27 | threshold: 4 28 | 29 | plugins: 30 | eslint: 31 | enabled: true 32 | channel: "eslint-5" 33 | config: 34 | config: .eslintrc.json 35 | fixme: 36 | enabled: true 37 | git-legal: 38 | enabled: true 39 | nodesecurity: 40 | enabled: true 41 | 42 | exclude_patterns: 43 | - "examples/" 44 | - "test/" 45 | - "**/*.d.ts" 46 | -------------------------------------------------------------------------------- /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "sandboxes": [ 3 | "new", 4 | "react-ts", 5 | "/examples", 6 | "react-pixi-fiber-template-ohk6z", 7 | "react-pixi-fiber-typescript-template-613ly", 8 | "q7oj1p0jo6", 9 | "react-pixi-fiber-demo-animatedsprite-d6udu", 10 | "react-pixi-fiber-text-alignment-th5eg", 11 | "react-pixi-fiber-with-redux-g4k7n", 12 | "9qyxrljyo" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 80 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | 17 | [COMMIT_EDITMSG] 18 | max_line_length = 0 19 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Third party 2 | **/node_modules 3 | 4 | # Build products 5 | dist/ 6 | coverage/ 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:react/recommended", "prettier"], 3 | "plugins": ["react", "react-hooks", "prettier"], 4 | "parser": "babel-eslint", 5 | "parserOptions": { 6 | "sourceType": "module", 7 | "ecmaFeatures": { 8 | "experimentalObjectRestSpread": true, 9 | "jsx": true 10 | } 11 | }, 12 | "env": { 13 | "es6": true, 14 | "node": true 15 | }, 16 | "rules": { 17 | "prettier/prettier": "error", 18 | "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks 19 | "react-hooks/exhaustive-deps": "warn" // Checks effect dependencies 20 | }, 21 | "globals": { 22 | "__DEV__": false 23 | }, 24 | "settings": { 25 | "react": { 26 | "pragma": "React", 27 | "version": "detect" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | node_modules 3 | *~ 4 | cjs/ 5 | es/ 6 | umd/ 7 | coverage/ 8 | .module-cache 9 | *.log* 10 | chrome-user-data 11 | *.sublime-project 12 | *.sublime-workspace 13 | .idea 14 | *.iml 15 | .vscode 16 | *.swp 17 | *.swo 18 | .envrc 19 | /stats.*.html 20 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.16.0 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "jsxBracketSameLine": false, 5 | "printWidth": 120, 6 | "proseWrap": "preserve", 7 | "semi": true, 8 | "singleQuote": false, 9 | "tabWidth": 2, 10 | "trailingComma": "es5", 11 | "useTabs": false 12 | } 13 | -------------------------------------------------------------------------------- /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 michal@tango.agency. 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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Open Development 4 | 5 | All work on React Pixi Fiber happens directly on [GitHub](https://github.com/michalochman/react-pixi-fiber). Both core team members and external contributors send pull requests which go through the same review process. 6 | 7 | 8 | ## Branch Organization 9 | 10 | We will do our best to keep the [`master` branch](https://github.com/michalochman/react-pixi-fiber/tree/master) in good shape, with tests passing at all times. But in order to move fast, we will make API changes that your application might not be compatible with. We recommend that you use [the latest stable version of React](https://reactjs.org/downloads.html). 11 | 12 | If you send a pull request, please do it against the `master` branch. 13 | 14 | 15 | ## Semantic Versioning 16 | 17 | React Pixi Fiber follows [semantic versioning](http://semver.org/). We release patch versions for bugfixes, minor versions for new features, and major versions for any breaking changes. When we make breaking changes, we also introduce deprecation warnings in a minor version so that our users learn about the upcoming changes and migrate their code in advance. 18 | 19 | 20 | ## Changelog 21 | 22 | Every significant change is documented in the [changelog file](https://github.com/michalochman/react-pixi-fiber/blob/master/CHANGELOG.md). 23 | 24 | 25 | ## Bugs 26 | 27 | We are using [GitHub Issues](https://github.com/michalochman/react-pixi-fiber/issues) for our public bugs. We keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new task, try to make sure your problem doesn't already exist. 28 | 29 | 30 | ## Proposing a Change 31 | 32 | If you intend to change the public API, or make any non-trivial changes to the implementation, we recommend [filing an issue](https://github.com/michalochman/react-pixi-fiber/issues/new). This lets us reach an agreement on your proposal before you put significant effort into it. 33 | 34 | If you're only fixing a bug, it's fine to submit a pull request right away but we still recommend to file an issue detailing what you're fixing. This is helpful in case we don't accept that specific fix but want to keep track of the issue. 35 | 36 | 37 | ## Sending a Pull Request 38 | 39 | The core team is monitoring for pull requests. We will review your pull request and either merge it, request changes to it, or close it with an explanation. 40 | 41 | **Before submitting a pull request**, please make sure the following is done: 42 | 43 | 1. Fork [the repository](https://github.com/michalochman/react-pixi-fiber) and create your branch from `master`. 44 | 2. Run `npm install` in the repository root. 45 | 3. If you've fixed a bug or added code that should be tested, add tests! 46 | 4. Ensure the test suite passes (`npm run test`). Tip: `npm run test --watch TestName` is helpful in development. 47 | 5. Format your code with [prettier](https://github.com/prettier/prettier) (`npm run prettier`). 48 | 6. Make sure your code lints (`npm run eslint`). 49 | 50 | ## Style Guide 51 | 52 | We use an automatic code formatter called [Prettier](https://prettier.io/). Run `npm run prettier` after making any changes to the code. 53 | 54 | Then, our linter will catch most issues that may exist in your code. You can check the status of your code styling by simply running `npm run eslint`. 55 | 56 | However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at [Airbnb's Style Guide](https://github.com/airbnb/javascript) will guide you in the right direction. 57 | 58 | 59 | ## License 60 | 61 | By contributing to React Pixi Fiber, you agree that your contributions will be licensed under its MIT license. 62 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | 5 | ### Steps to reproduce 6 | 7 | 1. 8 | 2. 9 | 10 | ### Additional info 11 | 12 | - react-pixi-fiber version: 13 | - React version: 14 | - ReactDOM version: 15 | - PixiJS version: 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Michał Ochman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/jest.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "coverageDirectory": "coverage/dev", 3 | "coverageReporters": ["json", "lcov", "text-summary"], 4 | "collectCoverageFrom": ["src/**/*.js"], 5 | "globals": { 6 | "__DEV__": true 7 | }, 8 | "rootDir": "..", 9 | "setupFiles": [ 10 | "jest-webgl-canvas-mock", 11 | "./config/jest/setupGlobals.js", 12 | "./config/jest/setupPixi.js" 13 | ], 14 | "moduleNameMapper": { 15 | "^react-pixi-fiber$": "src/index.js" 16 | }, 17 | "modulePaths": [""], 18 | "transform": { 19 | "^.+\\.js$": "babel-jest" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /config/jest.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "coverageDirectory": "coverage/prod", 3 | "coverageReporters": ["json", "lcov", "text-summary"], 4 | "collectCoverageFrom": ["src/**/*.js"], 5 | "globals": { 6 | "__DEV__": false 7 | }, 8 | "rootDir": "..", 9 | "setupFiles": [ 10 | "jest-webgl-canvas-mock", 11 | "./config/jest/setupGlobals.js", 12 | "./config/jest/setupPixi.js" 13 | ], 14 | "moduleNameMapper": { 15 | "^react-pixi-fiber$": "src/index.js" 16 | }, 17 | "modulePaths": [""], 18 | "transform": { 19 | "^.+\\.js$": "babel-jest" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /config/jest/setupGlobals.js: -------------------------------------------------------------------------------- 1 | const pkg = require("../../package.json"); 2 | const PIXI = require("pixi.js"); 3 | 4 | global.__PACKAGE_NAME__ = pkg.name; 5 | 6 | global.PIXI = PIXI; 7 | -------------------------------------------------------------------------------- /config/jest/setupPixi.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from "pixi.js"; 2 | 3 | // Do not show PIXI banner in the console 4 | // See: http://pixijs.download/release/docs/PIXI.utils.html#.skipHello 5 | PIXI.utils.skipHello(); 6 | -------------------------------------------------------------------------------- /config/rollup.config.js: -------------------------------------------------------------------------------- 1 | const pkg = require("../package.json"); 2 | const { babel } = require("@rollup/plugin-babel"); 3 | const commonjs = require("@rollup/plugin-commonjs"); 4 | const replace = require("@rollup/plugin-replace"); 5 | const peerDepsExternal = require("rollup-plugin-peer-deps-external"); 6 | const { nodeResolve } = require("@rollup/plugin-node-resolve"); 7 | const { terser } = require("rollup-plugin-terser"); 8 | const { visualizer } = require("rollup-plugin-visualizer"); 9 | 10 | const NODE_ENV = process.env.NODE_ENV || "production"; 11 | const isProduction = NODE_ENV === "production"; 12 | 13 | const getOutputFile = (entry, format) => { 14 | const suffix = isProduction ? "production.min" : "development"; 15 | return `${format}/${entry}.${suffix}.js`; 16 | }; 17 | 18 | const getPlugins = entry => [ 19 | peerDepsExternal(), 20 | nodeResolve({ 21 | mainFields: ["module", "jsnext:main", "main"], 22 | }), 23 | babel({ 24 | babelHelpers: "bundled", 25 | exclude: "node_modules/**", 26 | }), 27 | replace({ 28 | preventAssignment: true, 29 | values: { 30 | __DEV__: isProduction ? JSON.stringify(false) : JSON.stringify(true), 31 | __PACKAGE_NAME__: JSON.stringify(pkg.name), 32 | "process.env.NODE_ENV": isProduction ? JSON.stringify("production") : JSON.stringify("development"), 33 | }, 34 | }), 35 | commonjs(), 36 | isProduction && terser(), 37 | visualizer({ 38 | filename: `./stats.${entry}.${isProduction ? "production" : "development"}.html`, 39 | }), 40 | ]; 41 | 42 | const getExternal = entry => { 43 | return { 44 | "react-pixi-alias": ["react-pixi-fiber"], 45 | }[entry]; 46 | }; 47 | 48 | const getInput = entry => { 49 | return { 50 | "react-pixi-alias": "src/react-pixi-alias/index.js", 51 | "react-pixi-fiber": "src/index.js", 52 | }[entry]; 53 | }; 54 | 55 | const getConfig = (entry, format) => ({ 56 | input: getInput(entry), 57 | output: { 58 | file: getOutputFile(entry, format), 59 | name: "ReactPixiFiber", 60 | exports: "named", 61 | format: format, 62 | globals: { 63 | react: "React", 64 | "react-dom": "ReactDOM", 65 | "prop-types": "PropTypes", 66 | "pixi.js": "PIXI", 67 | "react-pixi-fiber": "ReactPixiFiber", 68 | }, 69 | sourcemap: isProduction, 70 | }, 71 | plugins: getPlugins(entry), 72 | external: getExternal(entry), 73 | }); 74 | 75 | export default [ 76 | getConfig("react-pixi-fiber", "cjs"), 77 | getConfig("react-pixi-alias", "cjs"), 78 | getConfig("react-pixi-fiber", "es"), 79 | getConfig("react-pixi-alias", "es"), 80 | getConfig("react-pixi-fiber", "umd"), 81 | getConfig("react-pixi-alias", "umd"), 82 | ]; 83 | -------------------------------------------------------------------------------- /examples/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | DISABLE_ESLINT_PLUGIN=true 3 | -------------------------------------------------------------------------------- /examples/.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 | -------------------------------------------------------------------------------- /examples/link.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Removing react-pixi-fiber from node_modules" 4 | rm -rf node_modules/react-pixi-fiber && \ 5 | echo "- ok" 6 | 7 | echo "Injecting local react-pixi-fiber into node_modules" 8 | mkdir -p node_modules/react-pixi-fiber && \ 9 | cp -R ../index.js node_modules/react-pixi-fiber/ && \ 10 | cp -R ../cjs node_modules/react-pixi-fiber/ && \ 11 | cp ../package.json node_modules/react-pixi-fiber/ && \ 12 | echo "- ok" 13 | 14 | echo "Invalidate node_modules cache" 15 | rm -rf node_modules/.cache && \ 16 | echo "- ok" 17 | 18 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "version": "0.0.1", 4 | "private": true, 5 | "dependencies": { 6 | "animated": "^0.2.2", 7 | "pixi-layers": "^0.3.1", 8 | "pixi.js": "^6.0.4", 9 | "prop-types": "^15.7.2", 10 | "react": "^18.2.0", 11 | "react-dom": "^18.2.0", 12 | "react-pixi-fiber": "2.0.0-alpha.1", 13 | "react-router-dom": "^5.2.0", 14 | "react-scripts": "5.0.1", 15 | "stats.js": "^0.17.0", 16 | "use-asset": "^0.2.0" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 11", 28 | "not op_mini all" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /examples/public/bunnys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michalochman/react-pixi-fiber/ef268a5e89e69a51306d7305fcd3c455521d3388/examples/public/bunnys.png -------------------------------------------------------------------------------- /examples/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michalochman/react-pixi-fiber/ef268a5e89e69a51306d7305fcd3c455521d3388/examples/public/favicon.ico -------------------------------------------------------------------------------- /examples/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/src/AnimatedExample/AnimatedSprite.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michalochman/react-pixi-fiber/ef268a5e89e69a51306d7305fcd3c455521d3388/examples/src/AnimatedExample/AnimatedSprite.js -------------------------------------------------------------------------------- /examples/src/AnimatedExample/animatedPixiTarget.js: -------------------------------------------------------------------------------- 1 | import { Sprite, applyDisplayObjectProps } from "react-pixi-fiber"; 2 | import * as Animated from "animated"; 3 | import * as PIXI from "pixi.js"; 4 | 5 | function ApplyAnimatedValues(instance, props) { 6 | if (instance instanceof PIXI.DisplayObject) { 7 | // Component has custom way of applying props - use that 8 | if (typeof instance._customApplyProps === "function") { 9 | instance._customApplyProps(instance, {}, props); 10 | } else { 11 | // TODO check if this is safe 12 | const type = instance.constructor.name; 13 | applyDisplayObjectProps(type, instance, {}, props); 14 | } 15 | } else { 16 | return false; 17 | } 18 | } 19 | 20 | function mapStyle(style) { 21 | return style; 22 | } 23 | 24 | Animated.inject.ApplyAnimatedValues(ApplyAnimatedValues, mapStyle); 25 | 26 | const ReactPixiFiberAnimated = { 27 | ...Animated, 28 | Sprite: Animated.createAnimatedComponent(Sprite), 29 | }; 30 | 31 | export default ReactPixiFiberAnimated; 32 | -------------------------------------------------------------------------------- /examples/src/AnimatedExample/index.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useRef } from "react"; 2 | import { Stage } from "react-pixi-fiber"; 3 | import Animated from "./animatedPixiTarget"; 4 | import * as PIXI from "pixi.js"; 5 | import Bunny from "../Bunny"; 6 | 7 | const OPTIONS = { 8 | backgroundColor: 0x1099bb, 9 | height: 600, 10 | width: 800, 11 | }; 12 | 13 | const centerAnchor = new PIXI.Point(0.5, 0.5); 14 | 15 | function BunnyExample() { 16 | const animationProgress = useRef(new Animated.Value(0)); 17 | const handleDown = useCallback(() => { 18 | Animated.spring(animationProgress.current, { toValue: 1 }).start(); 19 | }, []); 20 | const handleUp = useCallback(() => { 21 | Animated.spring(animationProgress.current, { toValue: 0 }).start(); 22 | }, []); 23 | 24 | return ( 25 | 26 | 42 | 43 | ); 44 | } 45 | 46 | export default BunnyExample; 47 | -------------------------------------------------------------------------------- /examples/src/App/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-title { 18 | font-size: 1.5em; 19 | } 20 | 21 | .App-intro { 22 | font-size: large; 23 | padding-top: 1.5rem; 24 | } 25 | 26 | @keyframes App-logo-spin { 27 | from { transform: rotate(0deg); } 28 | to { transform: rotate(360deg); } 29 | } 30 | 31 | .bordered { 32 | border: 5px solid #222; 33 | } 34 | -------------------------------------------------------------------------------- /examples/src/App/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import logo from "../logo.svg"; 3 | import { Route, Switch } from "react-router-dom"; 4 | import "./App.css"; 5 | import ExampleList from "../ExampleList"; 6 | import AnimatedExample from "../AnimatedExample"; 7 | import BunnyExample from "../BunnyExample"; 8 | import BunnymarkExample from "../BunnymarkExample"; 9 | import CanvasPropsExample from "../CanvasPropsExample"; 10 | import ClickExample from "../ClickExample"; 11 | import CustomApplicationExample from "../CustomApplicationExample"; 12 | import CustomBunnymarkExample from "../CustomBunnymarkExample"; 13 | import CustomPIXIComponentExample from "../CustomPIXIComponentExample"; 14 | import CustomPIXIPropertyExample from "../CustomPIXIPropertyExample"; 15 | import HooksExample from "../HooksExample"; 16 | import LayersExample from "../LayersExample"; 17 | import PointsExample from "../PointsExample/PointsExample"; 18 | import SmokeTest from "../SmokeTest"; 19 | import SuspenseExample from "../SuspenseExample"; 20 | import Stats from "../Stats"; 21 | 22 | const examples = [ 23 | { 24 | name: "Animated", 25 | slug: "animated", 26 | component: AnimatedExample, 27 | }, 28 | { 29 | name: "Bunny", 30 | slug: "bunny", 31 | component: BunnyExample, 32 | }, 33 | { 34 | name: "Bunnymark", 35 | slug: "bunnymark", 36 | component: BunnymarkExample, 37 | }, 38 | { 39 | name: "Bunnymark (using custom components)", 40 | slug: "custombunnymark", 41 | component: CustomBunnymarkExample, 42 | }, 43 | { 44 | name: "Canvas Props", 45 | slug: "canvasprops", 46 | component: CanvasPropsExample, 47 | }, 48 | { 49 | name: "Click", 50 | slug: "click", 51 | component: ClickExample, 52 | }, 53 | { 54 | name: "CustomApplication", 55 | slug: "customapplication", 56 | component: CustomApplicationExample, 57 | }, 58 | { 59 | name: "CustomPIXIComponent", 60 | slug: "custompixicomponent", 61 | component: CustomPIXIComponentExample, 62 | }, 63 | { 64 | name: "CustomPIXIProperty", 65 | slug: "custompixiproperty", 66 | component: CustomPIXIPropertyExample, 67 | }, 68 | { 69 | name: "Hooks", 70 | slug: "hooks", 71 | component: HooksExample, 72 | }, 73 | { 74 | name: "Layers", 75 | slug: "layers", 76 | component: LayersExample, 77 | }, 78 | { 79 | name: "Point-like props", 80 | slug: "points", 81 | component: PointsExample, 82 | }, 83 | { 84 | name: "Suspense", 85 | slug: "suspense", 86 | component: SuspenseExample, 87 | }, 88 | { 89 | name: "Smoke Test", 90 | slug: "smoketest", 91 | component: SmokeTest, 92 | }, 93 | ]; 94 | 95 | function App() { 96 | return ( 97 |
98 | 99 |
100 | logo 101 |

react-pixi-fiber Examples

102 |
103 |
104 | 105 | } /> 106 | {examples.map(example => ( 107 | 108 | ))} 109 | 110 |
111 |
112 | ); 113 | } 114 | 115 | export default App; 116 | -------------------------------------------------------------------------------- /examples/src/Bunny/bunnys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michalochman/react-pixi-fiber/ef268a5e89e69a51306d7305fcd3c455521d3388/examples/src/Bunny/bunnys.png -------------------------------------------------------------------------------- /examples/src/Bunny/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Sprite } from "react-pixi-fiber"; 4 | import * as PIXI from "pixi.js"; 5 | import bunnys from "./bunnys.png"; 6 | 7 | const centerAnchor = new PIXI.Point(0.5, 0.5); 8 | 9 | const bunnyTextures = new PIXI.Texture.from(bunnys); 10 | const textures = [ 11 | new PIXI.Texture(bunnyTextures.baseTexture, new PIXI.Rectangle(2, 47, 26, 37)), 12 | new PIXI.Texture(bunnyTextures.baseTexture, new PIXI.Rectangle(2, 86, 26, 37)), 13 | new PIXI.Texture(bunnyTextures.baseTexture, new PIXI.Rectangle(2, 125, 26, 37)), 14 | new PIXI.Texture(bunnyTextures.baseTexture, new PIXI.Rectangle(2, 164, 26, 37)), 15 | new PIXI.Texture(bunnyTextures.baseTexture, new PIXI.Rectangle(2, 2, 26, 37)), 16 | ]; 17 | 18 | function Bunny({ as: Component, ...passedProps }) { 19 | const texture = textures[passedProps.texture]; 20 | return ; 21 | } 22 | Bunny.propTypes = { 23 | as: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), 24 | texture: PropTypes.number, 25 | }; 26 | Bunny.defaultProps = { 27 | as: Sprite, 28 | texture: 0, 29 | }; 30 | 31 | export default Bunny; 32 | -------------------------------------------------------------------------------- /examples/src/BunnyExample/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Container, Stage } from "react-pixi-fiber"; 3 | import RotatingBunny from "../RotatingBunny"; 4 | 5 | const OPTIONS = { 6 | backgroundColor: 0x1099bb, 7 | height: 600, 8 | width: 800, 9 | }; 10 | 11 | function BunnyExample() { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export default BunnyExample; 26 | -------------------------------------------------------------------------------- /examples/src/BunnymarkExample/Bunnymark.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useCallback, useLayoutEffect, useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { usePixiTicker, withApp, ParticleContainer, Sprite, Text } from "react-pixi-fiber"; 4 | import * as PIXI from "pixi.js"; 5 | import bunnysImage from "./bunnys.png"; 6 | 7 | const maxSize = 200000; 8 | const bunniesAddedPerFrame = 100; 9 | const gravity = 0.5; 10 | const maxX = 800; 11 | const maxY = 600; 12 | const minX = 0; 13 | const minY = 0; 14 | 15 | const bunnyAnchor = new PIXI.Point(0.5, 1); 16 | const particleContainerProperties = { 17 | scale: false, 18 | position: true, 19 | rotation: false, 20 | uvs: false, 21 | tint: false, 22 | }; 23 | 24 | const generateBunny = texture => ({ 25 | speedX: Math.random() * 10, 26 | speedY: Math.random() * 10 - 5, 27 | texture: texture, 28 | x: 0, 29 | y: 0, 30 | }); 31 | 32 | const moveBunny = bunny => { 33 | const movedBunny = { ...bunny }; 34 | 35 | movedBunny.x += movedBunny.speedX; 36 | movedBunny.y += movedBunny.speedY; 37 | movedBunny.speedY += gravity; 38 | 39 | if (movedBunny.x > maxX) { 40 | movedBunny.speedX *= -1; 41 | movedBunny.x = maxX; 42 | } else if (movedBunny.x < minX) { 43 | movedBunny.speedX *= -1; 44 | movedBunny.x = minX; 45 | } 46 | 47 | if (movedBunny.y > maxY) { 48 | movedBunny.speedY *= -0.85; 49 | movedBunny.y = maxY; 50 | if (Math.random() > 0.5) { 51 | movedBunny.speedY -= Math.random() * 6; 52 | } 53 | } else if (movedBunny.y < minY) { 54 | movedBunny.speedY = 0; 55 | movedBunny.y = minY; 56 | } 57 | 58 | return movedBunny; 59 | }; 60 | 61 | function Bunnymark() { 62 | const [bunnys, setBunnys] = useState([]); 63 | const [bunnyTextures, setBunnyTextures] = useState([]); 64 | const [currentTexture, setCurrentTexture] = useState(0); 65 | const [isAdding, setIsAdding] = useState(false); 66 | 67 | useLayoutEffect(() => { 68 | const bunnyTextures = new PIXI.Texture.from(bunnysImage); 69 | const bunny1 = new PIXI.Texture(bunnyTextures.baseTexture, new PIXI.Rectangle(2, 47, 26, 37)); 70 | const bunny2 = new PIXI.Texture(bunnyTextures.baseTexture, new PIXI.Rectangle(2, 86, 26, 37)); 71 | const bunny3 = new PIXI.Texture(bunnyTextures.baseTexture, new PIXI.Rectangle(2, 125, 26, 37)); 72 | const bunny4 = new PIXI.Texture(bunnyTextures.baseTexture, new PIXI.Rectangle(2, 164, 26, 37)); 73 | const bunny5 = new PIXI.Texture(bunnyTextures.baseTexture, new PIXI.Rectangle(2, 2, 26, 37)); 74 | 75 | setBunnyTextures([bunny1, bunny2, bunny3, bunny4, bunny5]); 76 | const currentTexture = 2; 77 | setBunnys([generateBunny(currentTexture), generateBunny(currentTexture)]); 78 | setCurrentTexture(currentTexture); 79 | }, []); 80 | 81 | const animate = useCallback(() => { 82 | const addedBunnys = []; 83 | 84 | if (isAdding) { 85 | if (bunnys.length < maxSize) { 86 | for (let i = 0; i < bunniesAddedPerFrame; i++) { 87 | addedBunnys.push(generateBunny(currentTexture)); 88 | } 89 | } 90 | } 91 | 92 | const newBunnys = bunnys.concat(addedBunnys).map(moveBunny); 93 | 94 | setBunnys(newBunnys); 95 | }, [bunnys, currentTexture, isAdding]); 96 | 97 | usePixiTicker(animate); 98 | 99 | const handlePointerDown = useCallback(() => { 100 | setIsAdding(true); 101 | }, []); 102 | 103 | const handlePointerUp = useCallback(() => { 104 | setCurrentTexture(currentTexture => (currentTexture + 1) % 5); 105 | setIsAdding(false); 106 | }, []); 107 | 108 | return ( 109 | 110 | 111 | {bunnys.map((bunny, i) => ( 112 | 113 | ))} 114 | 115 | 116 | {/* ParticleContainer and its children cannot be interactive 117 | so here's a clickable hit area */} 118 | 126 | 127 | ); 128 | } 129 | Bunnymark.propTypes = { 130 | app: PropTypes.object, 131 | }; 132 | 133 | export default withApp(Bunnymark); 134 | -------------------------------------------------------------------------------- /examples/src/BunnymarkExample/bunnys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michalochman/react-pixi-fiber/ef268a5e89e69a51306d7305fcd3c455521d3388/examples/src/BunnymarkExample/bunnys.png -------------------------------------------------------------------------------- /examples/src/BunnymarkExample/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Stage } from "react-pixi-fiber"; 3 | import Bunnymark from "./Bunnymark"; 4 | 5 | const OPTIONS = { 6 | backgroundColor: 0x1099bb, 7 | height: 600, 8 | width: 800, 9 | }; 10 | 11 | function BunnymarkExample() { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export default BunnymarkExample; 20 | -------------------------------------------------------------------------------- /examples/src/CanvasPropsExample/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Stage } from "react-pixi-fiber"; 3 | import Bunny from "../Bunny"; 4 | 5 | const OPTIONS = { 6 | backgroundColor: 0x1099bb, 7 | height: 600, 8 | width: 800, 9 | }; 10 | 11 | function CanvasPropsExample() { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export default CanvasPropsExample; 20 | -------------------------------------------------------------------------------- /examples/src/ClickExample/index.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from "react"; 2 | import { Stage } from "react-pixi-fiber"; 3 | import * as PIXI from "pixi.js"; 4 | import Bunny from "../Bunny"; 5 | 6 | // Scale mode for all textures, will retain pixelation 7 | PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; 8 | 9 | const OPTIONS = { 10 | backgroundColor: 0x1099bb, 11 | height: 600, 12 | width: 800, 13 | }; 14 | 15 | // http://pixijs.io/examples/#/basics/click.js 16 | function ClickExample() { 17 | const [scale, setScale] = useState(1); 18 | const handleClick = useCallback(() => { 19 | setScale(scale => scale * 1.25); 20 | }, []); 21 | 22 | return ( 23 | 24 | 35 | 36 | ); 37 | } 38 | 39 | export default ClickExample; 40 | -------------------------------------------------------------------------------- /examples/src/CustomApplicationExample/index.js: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, useRef, useState } from "react"; 2 | import { Stage } from "react-pixi-fiber"; 3 | import RotatingBunny from "../RotatingBunny"; 4 | import * as PIXI from "pixi.js"; 5 | 6 | function CustomApplicationExample() { 7 | const div = useRef(); 8 | const [app, setApp] = useState(null); 9 | 10 | useLayoutEffect(() => { 11 | if (!div.current) { 12 | return; 13 | } 14 | 15 | const canvas = document.createElement("canvas"); 16 | div.current.appendChild(canvas); 17 | 18 | const app = new PIXI.Application({ 19 | backgroundColor: 0xbb9910, 20 | height: 600, 21 | view: canvas, 22 | width: 800, 23 | }); 24 | setApp(app); 25 | 26 | return function cleanup() { 27 | setApp(null); 28 | app.destroy(true, true); 29 | }; 30 | }, []); 31 | 32 | return ( 33 |
34 | {app && ( 35 | 36 | 37 | 38 | )} 39 |
40 | ); 41 | } 42 | 43 | export default CustomApplicationExample; 44 | -------------------------------------------------------------------------------- /examples/src/CustomBunnymarkExample/CustomBunnymark.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useCallback, useLayoutEffect, useRef, useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { usePixiTicker, withApp, ParticleContainer, Sprite, Text } from "react-pixi-fiber"; 4 | import * as PIXI from "pixi.js"; 5 | import Particle from "./Particle"; 6 | import bunnysImage from "./bunnys.png"; 7 | 8 | const maxSize = 200000; 9 | const bunniesAddedPerFrame = 100; 10 | const gravity = 0.5; 11 | const maxX = 800; 12 | const maxY = 600; 13 | const minX = 0; 14 | const minY = 0; 15 | 16 | const bunnyAnchor = new PIXI.Point(0.5, 1); 17 | const particleContainerProperties = { 18 | scale: false, 19 | position: true, 20 | rotation: false, 21 | uvs: false, 22 | tint: false, 23 | }; 24 | 25 | const generateBunny = texture => ({ 26 | speedX: Math.random() * 10, 27 | speedY: Math.random() * 10 - 5, 28 | texture: texture, 29 | }); 30 | 31 | const moveBunny = function () { 32 | this.x += this.speedX; 33 | this.y += this.speedY; 34 | this.speedY += gravity; 35 | 36 | if (this.x > maxX) { 37 | this.speedX *= -1; 38 | this.x = maxX; 39 | } else if (this.x < minX) { 40 | this.speedX *= -1; 41 | this.x = minX; 42 | } 43 | 44 | if (this.y > maxY) { 45 | this.speedY *= -0.85; 46 | this.y = maxY; 47 | if (Math.random() > 0.5) { 48 | this.speedY -= Math.random() * 6; 49 | } 50 | } else if (this.y < minY) { 51 | this.speedY = 0; 52 | this.y = minY; 53 | } 54 | }; 55 | 56 | function CustomBunnymark() { 57 | const particleContainer = useRef(null); 58 | const [bunnys, setBunnys] = useState([]); 59 | const [bunnyTextures, setBunnyTextures] = useState([]); 60 | const [currentTexture, setCurrentTexture] = useState(0); 61 | const [isAdding, setIsAdding] = useState(false); 62 | 63 | useLayoutEffect(() => { 64 | const bunnyTextures = new PIXI.Texture.from(bunnysImage); 65 | const bunny1 = new PIXI.Texture(bunnyTextures.baseTexture, new PIXI.Rectangle(2, 47, 26, 37)); 66 | const bunny2 = new PIXI.Texture(bunnyTextures.baseTexture, new PIXI.Rectangle(2, 86, 26, 37)); 67 | const bunny3 = new PIXI.Texture(bunnyTextures.baseTexture, new PIXI.Rectangle(2, 125, 26, 37)); 68 | const bunny4 = new PIXI.Texture(bunnyTextures.baseTexture, new PIXI.Rectangle(2, 164, 26, 37)); 69 | const bunny5 = new PIXI.Texture(bunnyTextures.baseTexture, new PIXI.Rectangle(2, 2, 26, 37)); 70 | 71 | setBunnyTextures([bunny1, bunny2, bunny3, bunny4, bunny5]); 72 | const currentTexture = 2; 73 | setBunnys([generateBunny(currentTexture), generateBunny(currentTexture)]); 74 | setCurrentTexture(currentTexture); 75 | }, []); 76 | 77 | const animate = useCallback(() => { 78 | if (isAdding) { 79 | const addedBunnys = []; 80 | 81 | if (bunnys.length < maxSize) { 82 | for (let i = 0; i < bunniesAddedPerFrame; i++) { 83 | addedBunnys.push(generateBunny(currentTexture)); 84 | } 85 | } 86 | const newBunnys = bunnys.concat(addedBunnys); 87 | 88 | setBunnys(newBunnys); 89 | } 90 | 91 | if (particleContainer.current) { 92 | particleContainer.current.children.forEach(bunny => bunny.update(bunny)); 93 | } 94 | }, [bunnys, currentTexture, isAdding]); 95 | 96 | usePixiTicker(animate); 97 | 98 | const handlePointerDown = useCallback(() => { 99 | setIsAdding(true); 100 | }, []); 101 | 102 | const handlePointerUp = useCallback(() => { 103 | setCurrentTexture(currentTexture => (currentTexture + 1) % 5); 104 | setIsAdding(false); 105 | }, []); 106 | 107 | return ( 108 | 109 | 110 | {bunnys.map((bunny, i) => ( 111 | 119 | ))} 120 | 121 | 122 | {/* ParticleContainer and its children cannot be interactive 123 | so here's a clickable hit area */} 124 | 132 | 133 | ); 134 | } 135 | CustomBunnymark.propTypes = { 136 | app: PropTypes.object, 137 | }; 138 | 139 | export default withApp(CustomBunnymark); 140 | -------------------------------------------------------------------------------- /examples/src/CustomBunnymarkExample/Particle.js: -------------------------------------------------------------------------------- 1 | import { CustomPIXIComponent } from "react-pixi-fiber"; 2 | import * as PIXI from "pixi.js"; 3 | 4 | const PARTICLE = "Particle"; 5 | export const behavior = { 6 | customDisplayObject: props => new PIXI.Sprite(props.texture), 7 | customApplyProps: function (instance, oldProps, newProps) { 8 | if (typeof oldProps !== "undefined" && Object.keys(oldProps).length === 0) { 9 | return; 10 | } 11 | 12 | this.applyDisplayObjectProps(oldProps, newProps); 13 | }, 14 | }; 15 | export default CustomPIXIComponent(behavior, PARTICLE); 16 | -------------------------------------------------------------------------------- /examples/src/CustomBunnymarkExample/bunnys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michalochman/react-pixi-fiber/ef268a5e89e69a51306d7305fcd3c455521d3388/examples/src/CustomBunnymarkExample/bunnys.png -------------------------------------------------------------------------------- /examples/src/CustomBunnymarkExample/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Stage } from "react-pixi-fiber"; 3 | import CustomBunnymark from "./CustomBunnymark"; 4 | 5 | const OPTIONS = { 6 | backgroundColor: 0x1099bb, 7 | height: 600, 8 | width: 800, 9 | }; 10 | 11 | function CustomBunnymarkExample() { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export default CustomBunnymarkExample; 20 | -------------------------------------------------------------------------------- /examples/src/CustomPIXIComponentExample/Circle.js: -------------------------------------------------------------------------------- 1 | import { CustomPIXIComponent } from "react-pixi-fiber"; 2 | import * as PIXI from "pixi.js"; 3 | 4 | const TYPE = "Circle"; 5 | export const behavior = { 6 | customDisplayObject: props => new PIXI.Graphics(), 7 | customApplyProps: function (instance, oldProps, newProps) { 8 | const { fill, x, y, radius, ...newPropsRest } = newProps; 9 | const { fill: oldFill, radius: oldRadius, ...oldPropsRest } = oldProps || {}; 10 | if (typeof oldProps !== "undefined") { 11 | instance.clear(); 12 | } 13 | instance.beginFill(fill); 14 | instance.drawCircle(x, y, radius); 15 | instance.endFill(); 16 | 17 | this.applyDisplayObjectProps(oldPropsRest, newPropsRest); 18 | }, 19 | }; 20 | 21 | export default CustomPIXIComponent(behavior, TYPE); 22 | -------------------------------------------------------------------------------- /examples/src/CustomPIXIComponentExample/DraggableContainer.js: -------------------------------------------------------------------------------- 1 | import { CustomPIXIComponent } from "react-pixi-fiber"; 2 | import * as PIXI from "pixi.js"; 3 | 4 | const TYPE = "DraggableContainer"; 5 | const behavior = { 6 | customDisplayObject: () => new PIXI.Container(), 7 | customDidAttach: function (instance) { 8 | instance.interactive = true; 9 | instance.cursor = "pointer"; 10 | 11 | let draggedObject = null; 12 | this.dragStart = () => { 13 | draggedObject = instance; 14 | if (typeof instance.onDragStart === "function") instance.onDragStart(instance); 15 | }; 16 | this.dragEnd = () => { 17 | draggedObject = null; 18 | if (typeof instance.onDragEnd === "function") instance.onDragEnd(instance); 19 | }; 20 | this.dragMove = e => { 21 | if (draggedObject === null) { 22 | return; 23 | } 24 | draggedObject.position.x += e.data.originalEvent.movementX; 25 | draggedObject.position.y += e.data.originalEvent.movementY; 26 | if (typeof instance.onDragMove === "function") instance.onDragMove(instance); 27 | }; 28 | 29 | instance.on("mousedown", this.dragStart); 30 | instance.on("mouseup", this.dragEnd); 31 | instance.on("mousemove", this.dragMove); 32 | }, 33 | customWillDetach: function (instance) { 34 | instance.off("mousedown", this.dragStart); 35 | instance.off("mouseup", this.dragEnd); 36 | instance.off("mousemove", this.dragMove); 37 | }, 38 | }; 39 | 40 | export default CustomPIXIComponent(behavior, TYPE); 41 | -------------------------------------------------------------------------------- /examples/src/CustomPIXIComponentExample/Rect.js: -------------------------------------------------------------------------------- 1 | import { CustomPIXIComponent } from "react-pixi-fiber"; 2 | import * as PIXI from "pixi.js"; 3 | 4 | const TYPE = "Rect"; 5 | export const behavior = { 6 | customDisplayObject: props => new PIXI.Graphics(), 7 | customApplyProps: function (instance, oldProps, newProps) { 8 | const { fill, x, y, width, height, ...newPropsRest } = newProps; 9 | const { fill: oldFill, x: oldX, y: oldY, width: oldWidth, height: oldHeight, ...oldPropsRest } = oldProps || {}; 10 | if (typeof oldProps !== "undefined") { 11 | instance.clear(); 12 | } 13 | instance.beginFill(fill); 14 | instance.drawRect(x, y, width, height); 15 | instance.endFill(); 16 | 17 | this.applyDisplayObjectProps(oldPropsRest, newPropsRest); 18 | }, 19 | }; 20 | 21 | export default CustomPIXIComponent(behavior, TYPE); 22 | -------------------------------------------------------------------------------- /examples/src/CustomPIXIComponentExample/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Stage, Text } from "react-pixi-fiber"; 3 | import Circle from "./Circle"; 4 | import DraggableContainer from "./DraggableContainer"; 5 | import Rect from "./Rect"; 6 | import useInterval from "./useInterval"; 7 | 8 | const COLORS = [0xff00ff, 0x00ffff]; 9 | const POSITIONS = [ 10 | { x: 300, y: 200 }, 11 | { x: 400, y: 200 }, 12 | { x: 400, y: 300 }, 13 | { x: 300, y: 300 }, 14 | ]; 15 | const OPTIONS = { 16 | backgroundColor: 0x1099bb, 17 | height: 600, 18 | width: 800, 19 | }; 20 | 21 | function CustomComponentExample() { 22 | const [color, setColor] = useState(0); 23 | const [position, setPosition] = useState(0); 24 | 25 | useInterval(() => { 26 | setColor(color => color + 1) % COLORS.length; 27 | setPosition(position => position + 1) % POSITIONS.length; 28 | }, 2000); 29 | 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | } 40 | 41 | export default CustomComponentExample; 42 | -------------------------------------------------------------------------------- /examples/src/CustomPIXIComponentExample/useInterval.js: -------------------------------------------------------------------------------- 1 | // See: https://github.com/juliencrn/usehooks.ts/blob/3beb733044ed40994725cf891a10ebfcd3569795/src/hooks/useInterval/useInterval.ts 2 | import { useEffect, useRef } from "react"; 3 | 4 | function useInterval(callback, delay) { 5 | const savedCallback = useRef(callback); 6 | 7 | // Remember the latest callback if it changes. 8 | useEffect(() => { 9 | savedCallback.current = callback; 10 | }, [callback]); 11 | 12 | // Set up the interval. 13 | useEffect(() => { 14 | // Don't schedule if no delay is specified. 15 | if (delay === null) { 16 | return; 17 | } 18 | 19 | const id = setInterval(() => savedCallback.current(), delay); 20 | 21 | return () => clearInterval(id); 22 | }, [delay]); 23 | } 24 | 25 | export default useInterval; 26 | -------------------------------------------------------------------------------- /examples/src/CustomPIXIPropertyExample/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Container, CustomPIXIProperty, Sprite, Stage } from "react-pixi-fiber"; 3 | import RotatingBunny from "../RotatingBunny"; 4 | 5 | const OPTIONS = { 6 | backgroundColor: 0x1099bb, 7 | height: 600, 8 | width: 800, 9 | }; 10 | 11 | CustomPIXIProperty(Sprite, "id", value => typeof value === "number"); 12 | 13 | function CustomPIXIPropertyExample() { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | {/* id will be reported in this tree as invalid prop (string instead of number) */} 22 | 23 | {/* bunnyName will be reported in this tree as unknown prop */} 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | export default CustomPIXIPropertyExample; 32 | -------------------------------------------------------------------------------- /examples/src/ExampleList/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Link } from "react-router-dom"; 4 | 5 | const propTypes = { 6 | examples: PropTypes.arrayOf( 7 | PropTypes.shape({ 8 | component: PropTypes.func.isRequired, 9 | name: PropTypes.string.isRequired, 10 | slug: PropTypes.string.isRequired, 11 | }) 12 | ), 13 | }; 14 | 15 | function ExampleList({ examples }) { 16 | return ( 17 |
    18 | {examples.map(example => ( 19 |
  • 20 | {example.name} 21 |
  • 22 | ))} 23 |
24 | ); 25 | } 26 | 27 | ExampleList.propTypes = propTypes; 28 | 29 | export default ExampleList; 30 | -------------------------------------------------------------------------------- /examples/src/HooksExample/index.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useCallback, useContext, useEffect, useRef, useState } from "react"; 2 | import { Stage, Text, usePixiTicker } from "react-pixi-fiber"; 3 | import Circle from "../CustomPIXIComponentExample/Circle"; 4 | import Rect from "../CustomPIXIComponentExample/Rect"; 5 | 6 | // returns a base 10 translation of a gray scale hex string build from a single 7 | // number between 0 and 255. if num is > 255 or < 0 it's clamped to the limit. 8 | const grayFromNum = num => { 9 | const hex = ("00" + Math.max(0, Math.min(255, num)).toString(16)).substr(-2); 10 | return parseInt(`${hex.repeat(3)}`, 16); 11 | }; 12 | 13 | /** 14 | * Implements `react-pixi-fiber`'s `usePixiTicker` hook, and the `useState` hook. 15 | * Handles animation of the circle and square background. 16 | */ 17 | function useAnimatedValue({ direction, max, min, value }) { 18 | const [data, setData] = useState({ 19 | direction, 20 | value, 21 | }); 22 | 23 | const animate = useCallback(() => { 24 | // perform all the logic inside setData so useEffect's dependency array 25 | // can be empty so it will only trigger one on initial render and not 26 | // add and remove from ticker constantly. 27 | setData(current => { 28 | const data = { ...current }; 29 | 30 | // flip direction once min or max has been reached. 31 | if ( 32 | (current.value >= current.max && current.direction === 1) || 33 | (current.value <= current.min && current.direction === -1) 34 | ) { 35 | data.direction *= -1; 36 | } 37 | 38 | // increment or decrement ` 39 | data.value += data.direction; 40 | 41 | return data; 42 | }); 43 | }, []); 44 | 45 | usePixiTicker(animate); 46 | 47 | return data.value; 48 | } 49 | 50 | const AnimationContext = React.createContext(); 51 | 52 | /** 53 | * implements `useContext`, `useEffect` and `useRef`. 54 | */ 55 | const Title = () => { 56 | const { title } = useContext(AnimationContext); 57 | const pixiText = useRef(null); 58 | 59 | // horizontally center the title's pivot point. this also works fine with `useEffect`. 60 | useEffect(() => { 61 | pixiText.current.pivot.set(pixiText.current.width / 2, 0); 62 | }, [title]); 63 | 64 | return ; 65 | }; 66 | 67 | const Animation = () => { 68 | const number1 = useAnimatedValue({ direction: 1, max: 255, min: 0, value: 0 }); 69 | const number2 = useAnimatedValue({ direction: -1, max: 255, min: 0, value: 255 }); 70 | const [text1, setText1] = useState(`number1: ${number1}`); 71 | const [text2, setText2] = useState(`number2: ${number2}`); 72 | 73 | // update the contents of the `Text` instances. 74 | useEffect(() => { 75 | setText1(`number1: ${number1}`); 76 | setText2(`number2: ${number2}`); 77 | }, [number1, number2]); 78 | 79 | return ( 80 | 81 | 82 | <Rect x={275} y={175} width={250} height={250} fill={grayFromNum(number1)} /> 83 | <Circle x={400} y={300} radius={100} fill={grayFromNum(number2)} /> 84 | <Text text={text1} x={0} y={500} /> 85 | <Text text={text2} x={0} y={550} /> 86 | </Fragment> 87 | ); 88 | }; 89 | 90 | const HooksExample = () => { 91 | return ( 92 | <Stage options={{ backgroundColor: 0xff0000, height: 600, width: 800 }}> 93 | <AnimationContext.Provider value={{ title: "animation" }}> 94 | <Animation /> 95 | </AnimationContext.Provider> 96 | </Stage> 97 | ); 98 | }; 99 | 100 | export default HooksExample; 101 | -------------------------------------------------------------------------------- /examples/src/LayersExample/ColoredBunny.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Container } from "react-pixi-fiber"; 4 | import Bunny from "../Bunny"; 5 | import Rect from "../CustomPIXIComponentExample/Rect"; 6 | 7 | function ColoredBunny({ fill, ...rest }) { 8 | return ( 9 | <Container {...rest}> 10 | <Rect x={-21} y={-21} width={42} height={42} fill={0x0} /> 11 | <Rect x={-20} y={-20} width={40} height={40} fill={fill} /> 12 | <Bunny /> 13 | </Container> 14 | ); 15 | } 16 | 17 | ColoredBunny.propTypes = { 18 | fill: PropTypes.number.isRequired, 19 | }; 20 | 21 | export default ColoredBunny; 22 | -------------------------------------------------------------------------------- /examples/src/LayersExample/Layer.js: -------------------------------------------------------------------------------- 1 | /*global PIXI*/ 2 | import { CustomPIXIComponent } from "react-pixi-fiber"; 3 | 4 | const TYPE = "Layer"; 5 | const behavior = { 6 | customDisplayObject: ({ group }) => new PIXI.display.Layer(group), 7 | }; 8 | 9 | export default CustomPIXIComponent(behavior, TYPE); 10 | -------------------------------------------------------------------------------- /examples/src/LayersExample/LayeredStage.js: -------------------------------------------------------------------------------- 1 | /*global PIXI*/ 2 | import { CustomPIXIComponent } from "react-pixi-fiber"; 3 | 4 | const TYPE = "LayeredStage"; 5 | const behavior = { 6 | customDisplayObject: ({ enableSort = false }) => { 7 | const stage = new PIXI.display.Stage(); 8 | stage.sortableChildren = true; 9 | return stage; 10 | }, 11 | customDidAttach: instance => { 12 | const updateStage = () => { 13 | instance.updateStage(); 14 | instance._updateStageRafId = window.requestAnimationFrame(updateStage); 15 | }; 16 | updateStage(); 17 | }, 18 | customWillDetach: instance => { 19 | window.cancelAnimationFrame(instance._updateStageRafId); 20 | instance.destroy(); 21 | }, 22 | }; 23 | 24 | export default CustomPIXIComponent(behavior, TYPE); 25 | -------------------------------------------------------------------------------- /examples/src/LayersExample/index.js: -------------------------------------------------------------------------------- 1 | // This is demo of pixi-display.js, https://github.com/gameofbombs/pixi-display 2 | // Drag the rabbits to understand what's going on 3 | // https://pixijs.io/examples/#/layers/zorder.js 4 | import React from "react"; 5 | import { CustomPIXIProperty, Container, Stage } from "react-pixi-fiber"; 6 | import ColoredBunny from "./ColoredBunny"; 7 | import DraggableContainer from "../CustomPIXIComponentExample/DraggableContainer"; 8 | import Layer from "./Layer"; 9 | import LayeredStage from "./LayeredStage"; 10 | import Rect from "../CustomPIXIComponentExample/Rect"; 11 | const PIXI = require("pixi.js"); 12 | window.PIXI = PIXI; 13 | require("pixi-layers/dist/pixi-layers.js"); 14 | 15 | const OPTIONS = { 16 | backgroundColor: 0x1099bb, 17 | height: 600, 18 | width: 800, 19 | }; 20 | 21 | // Mark parentGroup prop as legal on Container as long as it's a valid display group 22 | CustomPIXIProperty(Container, "parentGroup", value => value instanceof PIXI.display.Group); 23 | 24 | //META STUFF, groups exist without stage just fine 25 | 26 | // z-index = 0, sorting = true; 27 | const greenGroup = new PIXI.display.Group(0, true); 28 | 29 | // green bunnies go down 30 | greenGroup.on("sort", bunny => { 31 | // we are dragging bunny parent, not the bunny itself 32 | bunny.zOrder = bunny.parent.y; 33 | }); 34 | 35 | // blue bunnies go up 36 | // z-index = 1, sorting = true, we can provide zOrder function directly in constructor 37 | const blueGroup = new PIXI.display.Group(1, bunny => { 38 | // we are dragging bunny parent, not the bunny itself 39 | bunny.zOrder = -bunny.parent.y; 40 | }); 41 | 42 | // Drag is the best layer, dragged element is above everything else 43 | const dragGroup = new PIXI.display.Group(2, false); 44 | 45 | // Shadows are the lowest 46 | const shadowGroup = new PIXI.display.Group(-1, false); 47 | 48 | const blurFilter = new PIXI.filters.BlurFilter(); 49 | blurFilter.blur = 0.5; 50 | 51 | const onBunnyDragEnd = instance => { 52 | // we are dragging bunny parent, not the bunny itself 53 | instance.children[1].parentGroup = instance.children[1].oldGroup; 54 | }; 55 | 56 | const onBunnyDragStart = instance => { 57 | // we are dragging bunny parent, not the bunny itself 58 | instance.children[1].oldGroup = instance.children[1].parentGroup; 59 | instance.children[1].parentGroup = dragGroup; 60 | }; 61 | 62 | const blueBunnies = Array(10) 63 | .fill(0) 64 | .map((i, index) => index); 65 | const greenBunnies = Array(15) 66 | .fill(0) 67 | .map((i, index) => index); 68 | const evenBunnies = greenBunnies.filter(index => index % 2 === 0); 69 | const oddBunnies = greenBunnies.filter(index => index % 2 !== 0); 70 | 71 | const shadowProps = { 72 | fill: 0x0, 73 | filters: [blurFilter], 74 | height: 44, 75 | width: 44, 76 | x: -22, 77 | y: -22, 78 | }; 79 | 80 | const stage = new PIXI.display.Stage(); 81 | stage.group.enableSort = true; 82 | 83 | function LayersExample() { 84 | return ( 85 | <Stage options={OPTIONS}> 86 | {/* specify display list component */} 87 | <LayeredStage enableSort> 88 | {/* sorry, group cant exist without layer yet :( */} 89 | <Layer group={blueGroup} /> 90 | <Layer group={greenGroup} /> 91 | <Layer group={dragGroup} /> 92 | <Layer group={shadowGroup} /> 93 | {/* make obsolete containers. Why do we need them? 94 | * Just to show that we can do everything without 95 | * caring of actual parent container */} 96 | <Container> 97 | {evenBunnies.map(index => ( 98 | <DraggableContainer 99 | key={index} 100 | x={100 + 20 * index} 101 | y={100 + 20 * index} 102 | onDragEnd={onBunnyDragEnd} 103 | onDragStart={onBunnyDragStart} 104 | > 105 | <Rect {...shadowProps} parentGroup={shadowGroup} /> 106 | <ColoredBunny fill={0x74fa9a} parentGroup={greenGroup} /> 107 | </DraggableContainer> 108 | ))} 109 | </Container> 110 | <Container> 111 | {oddBunnies.map(index => ( 112 | <DraggableContainer 113 | key={index} 114 | x={100 + 20 * index} 115 | y={100 + 20 * index} 116 | onDragEnd={onBunnyDragEnd} 117 | onDragStart={onBunnyDragStart} 118 | > 119 | <Rect {...shadowProps} parentGroup={shadowGroup} /> 120 | <ColoredBunny fill={0x74fa9a} parentGroup={greenGroup} /> 121 | </DraggableContainer> 122 | ))} 123 | </Container> 124 | <Container> 125 | {blueBunnies.map(index => ( 126 | <DraggableContainer 127 | key={index} 128 | x={400 + 20 * index} 129 | y={400 - 20 * index} 130 | onDragEnd={onBunnyDragEnd} 131 | onDragStart={onBunnyDragStart} 132 | > 133 | <Rect {...shadowProps} parentGroup={shadowGroup} /> 134 | <ColoredBunny fill={0xbcfdfe} parentGroup={blueGroup} /> 135 | </DraggableContainer> 136 | ))} 137 | </Container> 138 | </LayeredStage> 139 | </Stage> 140 | ); 141 | } 142 | 143 | export default LayersExample; 144 | -------------------------------------------------------------------------------- /examples/src/PointsExample/PointsExample.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Stage } from "react-pixi-fiber"; 3 | import * as PIXI from "pixi.js"; 4 | import Bunny from "../Bunny"; 5 | 6 | // Scale mode for all textures, will retain pixelation 7 | PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; 8 | 9 | const WIDTH = 800; 10 | const HEIGHT = 600; 11 | const OPTIONS = { 12 | backgroundColor: 0x1099bb, 13 | height: HEIGHT, 14 | width: WIDTH, 15 | }; 16 | 17 | function PointsExample() { 18 | return ( 19 | <Stage options={OPTIONS}> 20 | {/* Position via single value of X as string */} 21 | <Bunny position="75" /> 22 | {/* Position via single value of X as number */} 23 | <Bunny position={150} /> 24 | {/* Position via single value of X in array */} 25 | <Bunny position={[225]} /> 26 | {/* Position via list of X and Y as comma delimited string */} 27 | <Bunny position="300,300" /> 28 | {/* Position via single value of X as string */} 29 | <Bunny position={[375, 375]} /> 30 | {/* Position via list of X and Y in array */} 31 | <Bunny position={{ x: 450, y: 450 }} /> 32 | {/* Position via explicit PIXI.Point */} 33 | <Bunny position={new PIXI.Point(525, 525)} /> 34 | </Stage> 35 | ); 36 | } 37 | 38 | export default PointsExample; 39 | -------------------------------------------------------------------------------- /examples/src/RotatingBunny/index.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { usePixiTicker, withApp } from "react-pixi-fiber"; 4 | import Bunny from "../Bunny"; 5 | 6 | // http://pixijs.io/examples/#/basics/basic.js 7 | // we don't want to pass app prop further down, it will trigger dev warning 8 | function RotatingBunny({ app, step, ...passedProps }) { 9 | const [rotation, setRotation] = useState(0); 10 | 11 | const animate = useCallback( 12 | delta => { 13 | // just for fun, let's rotate mr rabbit a little 14 | // delta is 1 if running at 100% performance 15 | // creates frame-independent tranformation 16 | setRotation(rotation => rotation + step * delta); 17 | }, 18 | [step] 19 | ); 20 | 21 | usePixiTicker(animate); 22 | 23 | return <Bunny {...passedProps} rotation={rotation} />; 24 | } 25 | RotatingBunny.propTypes = { 26 | app: PropTypes.object.isRequired, 27 | step: PropTypes.number, 28 | }; 29 | RotatingBunny.defaultProps = { 30 | step: 0.1, 31 | }; 32 | 33 | export default withApp(RotatingBunny); 34 | -------------------------------------------------------------------------------- /examples/src/SmokeTest/index.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useCallback, useState } from "react"; 2 | import { createStageClass, Stage } from "react-pixi-fiber"; 3 | import RotatingBunny from "../RotatingBunny"; 4 | 5 | const COLORS = [0x1099bb, 0x10bb99]; 6 | const SIZES = [ 7 | { width: 400, height: 300 }, 8 | { width: 300, height: 400 }, 9 | ]; 10 | 11 | const StageClass = createStageClass(); 12 | 13 | function SmokeTestExample() { 14 | const [color, setColor] = useState(0); 15 | const [count, setCount] = useState(0); 16 | const [mount, setMount] = useState(false); 17 | const [size, setSize] = useState(0); 18 | const [useStageClass, setUseStageClass] = useState(false); 19 | 20 | const addBunny = useCallback(() => { 21 | setCount(count => Math.min(5, count + 1)); 22 | }, []); 23 | 24 | const removeBunny = useCallback(() => { 25 | setCount(count => Math.min(5, count - 1)); 26 | }, []); 27 | 28 | const toggleBackground = useCallback(() => { 29 | setColor(color => (color ? 0 : 1)); 30 | }, []); 31 | 32 | const toggleSize = useCallback(() => { 33 | setSize(size => (size ? 0 : 1)); 34 | }, []); 35 | 36 | const toggleStage = useCallback(() => { 37 | setMount(mount => !mount); 38 | }, []); 39 | 40 | const toggleUseStageClass = useCallback(() => { 41 | setUseStageClass(useStageClass => !useStageClass); 42 | }, []); 43 | 44 | const backgroundColor = COLORS[color]; 45 | const { width, height } = SIZES[size]; 46 | const StageComponent = useStageClass ? StageClass : Stage; 47 | 48 | return ( 49 | <Fragment> 50 | <table style={{ margin: "0 auto 1.5em", textAlign: "left" }}> 51 | <tbody> 52 | <tr> 53 | <td>Stage mounted: {mount ? "yes" : "no"}</td> 54 | <td> 55 | <button onClick={toggleStage}>toggle Stage</button> 56 | </td> 57 | </tr> 58 | <tr> 59 | <td>StageClass used: {useStageClass ? "yes" : "no"}</td> 60 | <td> 61 | <button onClick={toggleUseStageClass}>toggle Use Stage Class</button> 62 | </td> 63 | </tr> 64 | <tr> 65 | <td> 66 | Size: {SIZES[size].width}x{SIZES[size].height} 67 | </td> 68 | <td> 69 | <button onClick={toggleSize}>toggle size</button> 70 | </td> 71 | </tr> 72 | <tr> 73 | <td>Background: #{COLORS[color].toString(16)}</td> 74 | <td> 75 | <button onClick={toggleBackground}>toggle background</button> 76 | </td> 77 | </tr> 78 | <tr> 79 | <td>Bunnies: {count}</td> 80 | <td> 81 | <button disabled={count === 5} onClick={addBunny}> 82 | add bunny 83 | </button> 84 | <button disabled={count === 0} onClick={removeBunny}> 85 | remove bunny 86 | </button> 87 | </td> 88 | </tr> 89 | </tbody> 90 | </table> 91 | {mount && ( 92 | <StageComponent key={useStageClass ? "class" : "function"} options={{ backgroundColor, height, width }}> 93 | {count > 0 && <RotatingBunny x={width / 2} y={height / 2} texture={0} step={0.1} />} 94 | {count > 1 && <RotatingBunny x={width / 4} y={height / 4} texture={1} step={0.2} />} 95 | {count > 2 && <RotatingBunny x={width / 4} y={(3 * height) / 4} texture={2} step={-0.25} />} 96 | {count > 3 && <RotatingBunny x={(3 * width) / 4} y={height / 4} texture={3} step={-0.1} />} 97 | {count > 4 && <RotatingBunny x={(3 * width) / 4} y={(3 * height) / 4} texture={4} step={-0.02} />} 98 | </StageComponent> 99 | )} 100 | </Fragment> 101 | ); 102 | } 103 | 104 | export default SmokeTestExample; 105 | -------------------------------------------------------------------------------- /examples/src/Stats/index.js: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect } from "react"; 2 | import StatsJS from "stats.js/src/Stats"; 3 | 4 | function Stats() { 5 | useLayoutEffect(() => { 6 | const stats = new StatsJS(); 7 | document.body.appendChild(stats.domElement); 8 | 9 | let rafId; 10 | const update = () => { 11 | stats.update(); 12 | rafId = window.requestAnimationFrame(update); 13 | }; 14 | update(); 15 | 16 | return function cleanup() { 17 | window.cancelAnimationFrame(rafId); 18 | document.body.removeChild(stats.domElement); 19 | }; 20 | }, []); 21 | 22 | return null; 23 | } 24 | 25 | export default Stats; 26 | -------------------------------------------------------------------------------- /examples/src/SuspenseExample/TextureRenderer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | 4 | 5 | function TextureRenderer({ assetUrl, children }) { 6 | console.log('1') 7 | console.log('2') 8 | console.log(spriteSheet.read()) 9 | const sheet = spriteSheet.read().default; 10 | console.log('3') 11 | const texture = PIXI.Texture.from(sheet); 12 | console.log('4') 13 | 14 | console.log(sheet) 15 | console.log(texture) 16 | 17 | return children({ texture }); 18 | } 19 | 20 | export default TextureRenderer; 21 | -------------------------------------------------------------------------------- /examples/src/SuspenseExample/bunnys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michalochman/react-pixi-fiber/ef268a5e89e69a51306d7305fcd3c455521d3388/examples/src/SuspenseExample/bunnys.png -------------------------------------------------------------------------------- /examples/src/SuspenseExample/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Container, Sprite, Stage } from "react-pixi-fiber"; 3 | import { createAsset } from "use-asset"; 4 | import * as PIXI from "pixi.js"; 5 | 6 | const OPTIONS = { 7 | backgroundColor: 0x1099bb, 8 | height: 600, 9 | width: 800, 10 | }; 11 | 12 | const spriteSheet = createAsset(() => import("./bunnys.png"), Infinity); 13 | 14 | function SpriteRenderer() { 15 | const sheet = spriteSheet.read().default; 16 | 17 | return <Sprite texture={PIXI.Texture.from(sheet)} />; 18 | } 19 | 20 | function SuspenseExample() { 21 | return ( 22 | <Stage options={OPTIONS}> 23 | <Container> 24 | <React.Suspense fallback={null}> 25 | <SpriteRenderer /> 26 | </React.Suspense> 27 | </Container> 28 | </Stage> 29 | ); 30 | } 31 | 32 | export default SuspenseExample; 33 | -------------------------------------------------------------------------------- /examples/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /examples/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App/App"; 4 | import { BrowserRouter } from "react-router-dom"; 5 | import registerServiceWorker from "./registerServiceWorker"; 6 | import "./index.css"; 7 | 8 | const root = ReactDOM.createRoot(document.getElementById("root")); 9 | root.render( 10 | <BrowserRouter> 11 | <App /> 12 | </BrowserRouter> 13 | ); 14 | 15 | registerServiceWorker(); 16 | -------------------------------------------------------------------------------- /examples/src/logo.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"> 2 | <g fill="#61DAFB"> 3 | <path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/> 4 | <circle cx="420.9" cy="296.5" r="45.7"/> 5 | <path d="M520.5 78.1z"/> 6 | </g> 7 | </svg> 8 | -------------------------------------------------------------------------------- /examples/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === "localhost" || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === "[::1]" || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/) 17 | ); 18 | 19 | export default function register() { 20 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 21 | // The URL constructor is available in all browsers that support SW. 22 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 23 | if (publicUrl.origin !== window.location.origin) { 24 | // Our service worker won't work if PUBLIC_URL is on a different origin 25 | // from what our page is served on. This might happen if a CDN is used to 26 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 27 | return; 28 | } 29 | 30 | window.addEventListener("load", () => { 31 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 32 | 33 | if (isLocalhost) { 34 | // This is running on localhost. Lets check if a service worker still exists or not. 35 | checkValidServiceWorker(swUrl); 36 | } else { 37 | // Is not local host. Just register service worker 38 | registerValidSW(swUrl); 39 | } 40 | }); 41 | } 42 | } 43 | 44 | function registerValidSW(swUrl) { 45 | navigator.serviceWorker 46 | .register(swUrl) 47 | .then(registration => { 48 | registration.onupdatefound = () => { 49 | const installingWorker = registration.installing; 50 | installingWorker.onstatechange = () => { 51 | if (installingWorker.state === "installed") { 52 | if (navigator.serviceWorker.controller) { 53 | // At this point, the old content will have been purged and 54 | // the fresh content will have been added to the cache. 55 | // It's the perfect time to display a "New content is 56 | // available; please refresh." message in your web app. 57 | console.log("New content is available; please refresh."); 58 | } else { 59 | // At this point, everything has been precached. 60 | // It's the perfect time to display a 61 | // "Content is cached for offline use." message. 62 | console.log("Content is cached for offline use."); 63 | } 64 | } 65 | }; 66 | }; 67 | }) 68 | .catch(error => { 69 | console.error("Error during service worker registration:", error); 70 | }); 71 | } 72 | 73 | function checkValidServiceWorker(swUrl) { 74 | // Check if the service worker can be found. If it can't reload the page. 75 | fetch(swUrl) 76 | .then(response => { 77 | // Ensure service worker exists, and that we really are getting a JS file. 78 | if (response.status === 404 || response.headers.get("content-type").indexOf("javascript") === -1) { 79 | // No service worker found. Probably a different app. Reload the page. 80 | navigator.serviceWorker.ready.then(registration => { 81 | registration.unregister().then(() => { 82 | window.location.reload(); 83 | }); 84 | }); 85 | } else { 86 | // Service worker found. Proceed as normal. 87 | registerValidSW(swUrl); 88 | } 89 | }) 90 | .catch(() => { 91 | console.log("No internet connection found. App is running in offline mode."); 92 | }); 93 | } 94 | 95 | export function unregister() { 96 | if ("serviceWorker" in navigator) { 97 | navigator.serviceWorker.ready.then(registration => { 98 | registration.unregister(); 99 | }); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /index.es.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === "production") { 2 | module.exports = require("./es/react-pixi-fiber.production.min.js"); 3 | } else { 4 | module.exports = require("./es/react-pixi-fiber.development.js"); 5 | } 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === "production") { 2 | module.exports = require("./cjs/react-pixi-fiber.production.min.js"); 3 | } else { 4 | module.exports = require("./cjs/react-pixi-fiber.development.js"); 5 | } 6 | -------------------------------------------------------------------------------- /index.umd.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === "production") { 2 | module.exports = require("./umd/react-pixi-fiber.production.min.js"); 3 | } else { 4 | module.exports = require("./umd/react-pixi-fiber.development.js"); 5 | } 6 | -------------------------------------------------------------------------------- /mapCoverage.js: -------------------------------------------------------------------------------- 1 | // Based on: https://github.com/facebook/jest/issues/2418#issuecomment-276056758 2 | const createReporter = require("istanbul-api").createReporter; 3 | const istanbulCoverage = require("istanbul-lib-coverage"); 4 | 5 | const map = istanbulCoverage.createCoverageMap(); 6 | const reporter = createReporter(); 7 | 8 | const envs = ["dev", "prod"]; 9 | 10 | envs.forEach(env => { 11 | const coverage = require(`./coverage/${env}/coverage-final.json`); 12 | Object.keys(coverage).forEach(filename => map.addFileCoverage(coverage[filename])); 13 | }); 14 | 15 | reporter.addAll(["json", "lcov", "text-summary"]); 16 | reporter.write(map); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-pixi-fiber", 3 | "version": "2.0.0-alpha.1", 4 | "description": "React Fiber renderer for PixiJS", 5 | "main": "index.js", 6 | "umd:main": "index.umd.js", 7 | "module": "index.es.js", 8 | "jsnext:main": "index.es.js", 9 | "author": "Michal Ochman", 10 | "license": "MIT", 11 | "repository": "github:michalochman/react-pixi-fiber", 12 | "typings": "./index.d.ts", 13 | "files": [ 14 | "LICENSE", 15 | "README.md", 16 | "index.d.ts", 17 | "index.es.js", 18 | "index.js", 19 | "index.umd.js", 20 | "react-pixi-alias.js", 21 | "cjs/", 22 | "es/", 23 | "umd/", 24 | "src/" 25 | ], 26 | "sideEffects": false, 27 | "dependencies": { 28 | "fbjs": "^3.0.0", 29 | "react-reconciler": "^0.29.0" 30 | }, 31 | "peerDependencies": { 32 | "pixi.js": ">=4.4.0", 33 | "prop-types": ">=15.0.0", 34 | "react": ">=18.2.0", 35 | "react-dom": ">=18.2.0" 36 | }, 37 | "devDependencies": { 38 | "@babel/cli": "^7.21.5", 39 | "@babel/core": "^7.22.1", 40 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 41 | "@babel/preset-env": "^7.22.4", 42 | "@babel/preset-react": "^7.22.3", 43 | "@rollup/plugin-babel": "^6.0.3", 44 | "@rollup/plugin-commonjs": "^25.0.0", 45 | "@rollup/plugin-node-resolve": "^15.1.0", 46 | "@rollup/plugin-replace": "^5.0.2", 47 | "@types/react": "^18.2.8", 48 | "babel-eslint": "^10.1.0", 49 | "babel-plugin-rewire": "^1.2.0", 50 | "canvas": "^2.11.2", 51 | "cross-env": "^7.0.3", 52 | "eslint": "^7.32.0", 53 | "eslint-config-prettier": "^8.8.0", 54 | "eslint-plugin-babel": "^5.3.1", 55 | "eslint-plugin-import": "^2.27.5", 56 | "eslint-plugin-prettier": "^4.2.1", 57 | "eslint-plugin-promise": "^6.1.1", 58 | "eslint-plugin-react": "^7.32.2", 59 | "eslint-plugin-react-hooks": "^4.6.0", 60 | "jest": "^26.6.3", 61 | "jest-webgl-canvas-mock": "^2.5.0", 62 | "pixi.js": "^6.0.2", 63 | "prettier": "^2.8.8", 64 | "react": "^18.2.0", 65 | "react-dom": "^18.2.0", 66 | "react-test-renderer": "^18.2.0", 67 | "rollup": "^2.75.6", 68 | "rollup-plugin-peer-deps-external": "^2.2.4", 69 | "rollup-plugin-terser": "^7.0.0", 70 | "rollup-plugin-visualizer": "^5.9.0", 71 | "typescript": "^4.9.5" 72 | }, 73 | "scripts": { 74 | "build": "npm run build:prod && npm run build:dev", 75 | "build:dev": "cross-env NODE_ENV=development rollup -c config/rollup.config.js", 76 | "build:prod": "cross-env NODE_ENV=production rollup -c config/rollup.config.js", 77 | "build:alias:dev": "cross-env NODE_ENV=development rollup -c config/rollup.config.js --environment entry:alias", 78 | "build:alias:prod": "cross-env NODE_ENV=production rollup -c config/rollup.config.js --environment entry:alias", 79 | "build:index:dev": "cross-env NODE_ENV=development rollup -c config/rollup.config.js --environment entry:index", 80 | "build:index:prod": "cross-env NODE_ENV=production rollup -c config/rollup.config.js --environment entry:index", 81 | "eslint": "eslint src", 82 | "eslint-check": "eslint --print-config .eslintrc.json | eslint-config-prettier-check", 83 | "prepublishOnly": "npm run test && npm run build", 84 | "test": "npm run test:dev && npm run test:prod", 85 | "test:cov": "npm run test:dev -- --coverage && npm run test:prod -- --coverage && node mapCoverage.js", 86 | "test:dev": "jest -c config/jest.dev.json", 87 | "test:prod": "jest -c config/jest.prod.json", 88 | "test:ts": "tsc -p tsconfig.json" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /react-pixi-alias.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === "production") { 2 | module.exports = require("./cjs/react-pixi-alias.production.min.js"); 3 | } else { 4 | module.exports = require("./cjs/react-pixi-alias.development.js"); 5 | } 6 | -------------------------------------------------------------------------------- /react-pixi.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 841.9 595.3" y="0" x="0" id="Layer_2_1_" version="1.1"> 2 | <path fill="#61DAFB" d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9a487.8 487.8 0 0 0-41.6-50c32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2a520.32 520.32 0 0 1-90.2.1c-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2a520.32 520.32 0 0 1 90.2-.1c8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8a559.9 559.9 0 0 1-20.7 39.9zm32.3 19c12.33-2 13.8 7.8 13.8 7.8-13.1 3.2-34.37 20.2-41.2 8-3.47-6.2 7.39-12.57 14.4-13.7zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-6.73 12.18-27.9-4.7-41-7.9 0 0 .56-9.48 13.5-7.5l13.1 2c6.95 1.06 17.8 7.25 14.4 13.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9a552.45 552.45 0 0 0-27.4 47.7c-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9a487.8 487.8 0 0 0 41.6 50c-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6z" id="path79" style="opacity:1;stop-opacity:1" /> 3 | <path id="polygon81" style="opacity:1;fill:#61dafb;stop-opacity:1" d="M320.8 78.4Z" /> 4 | <path id="circle83" style="opacity:1;fill:#e61f64;stop-opacity:1;fill-opacity:1" d="M466.6 296.5a45.7 45.7 0 0 1-45.7 45.7 45.7 45.7 0 0 1-45.7-45.7 45.7 45.7 0 0 1 45.7-45.7 45.7 45.7 0 0 1 45.7 45.7" /> 5 | <path id="polygon85" style="opacity:1;fill:#61dafb;stop-opacity:1" d="M520.5 78.1Z" /> 6 | <g id="g87-3"> 7 | <path d="M421.15 374.17c-69.85.03-136.95-24.87-142.35-3.27-14.4 63.6-10 122.2 18.2 138.4 6.5 3.8 16.1 5.6 24.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 18-1.8 24.6-5.6 28.1-16.2 32.4-74.7 17.9-138.1-5.8-21.6-72.8 3.35-142.65 3.37ZM506.98 405h.02a10 10 0 1 1-.02 0zM335 408c3.88 0 7 3.12 7 7v13h13c3.88 0 7 3.12 7 7s-3.12 7-7 7h-13v13c0 3.88-3.12 7-7 7s-7-3.12-7-7v-13h-13c-3.88 0-7-3.12-7-7s3.12-7 7-7h13v-13c0-3.88 3.12-7 7-7zm151.98 17h.02a10 10 0 1 1-.02 0zm40 0h.02a10 10 0 1 1-.02 0zm-20 20h.02a10 10 0 1 1-.02 0z" style="fill:#e61f64;fill-opacity:1" id="path79-7" /> 8 | </g> 9 | <style type="text/css" id="style1163"> 10 | .st0 { 11 | fill: #ea1e63 12 | } 13 | </style> 14 | <g style="fill:#fff;fill-opacity:1" transform="matrix(.21304 0 0 .21304 384.86 284.34)" id="g1181"> 15 | <g style="fill:#fff;fill-opacity:1" id="g1167"> 16 | <path style="fill:#fff;fill-opacity:1" class="st0" d="M89.5 24.8c8.3 0 13.5-5.6 13.5-12.5C102.9 5 97.8-.4 89.7-.4 81.6-.4 76.3 5 76.5 12.3c-.2 6.9 5.1 12.5 13 12.5z" id="path1165" /> 17 | </g> 18 | <g style="fill:#fff;fill-opacity:1" id="g1171"> 19 | <path style="fill:#fff;fill-opacity:1" class="st0" d="M189.1-.4c-8.1 0-13.4 5.4-13.2 12.7-.2 6.9 5.1 12.5 13 12.5 8.3 0 13.5-5.6 13.5-12.5-.1-7.3-5.2-12.7-13.3-12.7Z" id="path1169" /> 20 | </g> 21 | <g style="fill:#fff;fill-opacity:1" id="g1175"> 22 | <path style="fill:#fff;fill-opacity:1" class="st0" d="M176.5 34.7h-22.6l-7.1 13c-2.1 4-4.1 8.1-6.3 12.4h-.3c-2.1-3.8-4.3-7.9-6.6-12l-7.9-13.4H79.9c-.9-9.4-5.4-17.4-11.7-22.4-7.4-5.9-18.5-8.9-34-8.9-15.3 0-26.2 1-34 2.3v109.7h24.9V75.7c2.3.3 5.3.5 8.6.5 14.9 0 27.6-3.6 36.1-11.7 3.2-3 5.6-6.7 7.3-10.9v61.9h47.4l7.6-14.5c2-4 4.1-7.9 6.1-12.2h.5c2 4.1 4 8.2 6.3 12.2l8.1 14.5h48.5V34.7h-20.4zm-143 22.1c-3.6 0-6.3-.2-8.4-.7V23.4c1.8-.5 5.3-1 10.4-1 12.5 0 19.6 6.1 19.6 16.3 0 11.4-8.2 18.1-21.6 18.1zm68.7 50.8V41.5l22.2 32.9zm52.4-34.4 21.9-31.7v66.2z" id="path1173" /> 23 | </g> 24 | <g style="fill:#fff;fill-opacity:1" id="g1179"> 25 | <path style="fill:#fff;fill-opacity:1" class="st0" d="M307.8 49.4c-14.4-5.4-20.6-8.6-20.6-15.7 0-5.8 5.3-10.7 16.2-10.7 10.9 0 18.8 3.1 23.3 5.3L332.2 8c-6.6-3-15.8-5.6-28.4-5.6-23.1 0-38.1 11.3-41.4 26.9V4.2h-25.1v70c0 17.3-6.6 22.1-17.2 22.1-5 0-9.4-.8-12.9-2l-2.8 20.3c5 1.6 12.5 2.6 18.3 2.6 24.4 0 39.6-11.1 39.6-42.7V42.2c2.8 12.6 13.9 20.9 29.9 26.5 13.4 4.8 18.6 8.7 18.6 15.7 0 7.3-6.1 12-17.7 12-10.7 0-21.1-3.5-27.9-6.9l-5.1 20.8c6.3 3.5 18.8 6.8 31.5 6.8 30.5 0 44.9-15.8 44.9-34.5.2-15.7-9.1-25.9-28.7-33.2z" id="path1177" /> 26 | </g> 27 | </g> 28 | </svg> 29 | -------------------------------------------------------------------------------- /src/AppProvider.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { isNewContextAvailable } from "./compat"; 4 | 5 | const propTypes = { 6 | app: PropTypes.object.isRequired, 7 | children: PropTypes.node, 8 | }; 9 | const childContextTypes = { 10 | app: PropTypes.object, 11 | }; 12 | 13 | let AppContext = null; 14 | 15 | function createAppProvider() { 16 | if (isNewContextAvailable()) { 17 | // New Context API 18 | if (AppContext === null) { 19 | AppContext = React.createContext(null); 20 | } 21 | 22 | class AppProvider extends React.Component { 23 | render() { 24 | const { app, children } = this.props; 25 | return <AppContext.Provider value={app}>{children}</AppContext.Provider>; 26 | } 27 | } 28 | 29 | AppProvider.propTypes = propTypes; 30 | 31 | const withApp = WrappedComponent => { 32 | function WithApp(props) { 33 | return <AppContext.Consumer>{app => <WrappedComponent {...props} app={app} />}</AppContext.Consumer>; 34 | } 35 | WithApp.displayName = `withApp(${WrappedComponent})`; 36 | 37 | return WithApp; 38 | }; 39 | 40 | return { AppProvider, withApp }; 41 | } else { 42 | // Legacy Context API 43 | class AppProvider extends React.Component { 44 | getChildContext() { 45 | return { 46 | app: this.props.app, 47 | }; 48 | } 49 | 50 | render() { 51 | return this.props.children; 52 | } 53 | } 54 | 55 | AppProvider.propTypes = propTypes; 56 | AppProvider.childContextTypes = childContextTypes; 57 | 58 | const withApp = WrappedComponent => { 59 | function WithApp(props, context) { 60 | return <WrappedComponent {...props} app={context.app} />; 61 | } 62 | WithApp.displayName = `withApp(${WrappedComponent})`; 63 | WithApp.contextTypes = childContextTypes; 64 | 65 | return WithApp; 66 | }; 67 | 68 | return { AppProvider, withApp }; 69 | } 70 | } 71 | 72 | const { AppProvider, withApp } = createAppProvider(); 73 | 74 | export { AppContext, AppProvider, withApp }; 75 | -------------------------------------------------------------------------------- /src/CustomPIXIComponent.js: -------------------------------------------------------------------------------- 1 | import emptyFunction from "fbjs/lib/emptyFunction"; 2 | import invariant from "fbjs/lib/invariant"; 3 | import { injectType } from "./inject"; 4 | import possibleStandardNames from "./possibleStandardNames"; 5 | import { customProperties, PropertyInfoRecord } from "./PixiProperty"; 6 | 7 | function CustomPIXIComponent(behavior, type) { 8 | invariant( 9 | typeof type === "string", 10 | "Invalid argument `type` of type `%s` supplied to `CustomPIXIComponent`, expected `string`.", 11 | typeof type 12 | ); 13 | 14 | return injectType(type, behavior); 15 | } 16 | 17 | let CustomPIXIProperty = emptyFunction; 18 | 19 | if (__DEV__) { 20 | /** 21 | * @param maybeComponentType string|string[]|null|undefined 22 | * @param propertyName string 23 | * @param validator function<T>(value: T): boolean 24 | */ 25 | CustomPIXIProperty = function CustomPIXIProperty(maybeComponentType, propertyName, validator) { 26 | const lowerCasedPropertyName = propertyName.toLowerCase(); 27 | 28 | // Define custom property for all component types if none was provided 29 | if (maybeComponentType == null) { 30 | maybeComponentType = Object.keys(possibleStandardNames); 31 | } 32 | 33 | // Allow providing just one component as value instead of an array with 1 element 34 | const componentTypeList = [].concat(maybeComponentType); 35 | 36 | componentTypeList.forEach(componentType => { 37 | invariant( 38 | Object.keys(possibleStandardNames).includes(componentType), 39 | "`%s` is not a valid component type", 40 | componentType 41 | ); 42 | invariant( 43 | !( 44 | Object.keys(customProperties).includes(componentType) && 45 | Object.keys(customProperties[componentType]).includes(propertyName) 46 | ) && !Object.keys(possibleStandardNames[componentType]).includes(lowerCasedPropertyName), 47 | "Property `%s` is already registered on `%s`", 48 | propertyName, 49 | componentType 50 | ); 51 | invariant( 52 | typeof validator === "undefined" || typeof validator === "function", 53 | "Validator type for property `%s` is invalid. Expected `function`, got `%s`", 54 | propertyName, 55 | typeof validator 56 | ); 57 | 58 | if (typeof validator === "undefined") { 59 | validator = () => true; 60 | } 61 | 62 | // inject standard name 63 | possibleStandardNames[componentType][lowerCasedPropertyName] = propertyName; 64 | // inject custom property info 65 | if (!Object.keys(customProperties).includes(componentType)) { 66 | customProperties[componentType] = {}; 67 | } 68 | customProperties[componentType][propertyName] = new PropertyInfoRecord(propertyName, validator); 69 | }); 70 | }; 71 | } 72 | 73 | export { CustomPIXIProperty }; 74 | 75 | export default CustomPIXIComponent; 76 | -------------------------------------------------------------------------------- /src/PixiProperty.js: -------------------------------------------------------------------------------- 1 | // Based on: https://github.com/facebook/react/blob/9c77ffb444598c32c8f92c8d79e406959a10445b/packages/react-dom/src/shared/DOMProperty.js 2 | import { isInjectedType } from "./inject"; 3 | import { parsePoint } from "./utils"; 4 | 5 | // A reserved attribute. 6 | // It is handled by React separately and shouldn't be written to PIXI tree. 7 | export const RESERVED = 0; 8 | 9 | // A simple string attribute. 10 | // Attributes that aren't in the whitelist are presumed to have this type. 11 | export const STRING = 1; 12 | 13 | // A real boolean attribute. 14 | // When true, it should be present (set either to an empty string or its name). 15 | export const BOOLEAN = 2; 16 | 17 | // An attribute that must be numeric or parse as a numeric. 18 | // When falsy, it should be removed. 19 | export const NUMERIC = 3; 20 | 21 | // An attribute that must be positive numeric or parse as a positive numeric. 22 | // When falsy, it should be removed. 23 | export const POSITIVE_NUMERIC = 4; 24 | 25 | // An attribute that must be vector or parse as a vector. 26 | // When falsy, it should be removed. 27 | export const VECTOR = 5; 28 | 29 | // An attribute that must be function 30 | // When falsy, it should be removed. 31 | export const CALLBACK = 6; 32 | 33 | export function shouldIgnoreAttribute(type, name, propertyInfo) { 34 | if (propertyInfo !== null) { 35 | return propertyInfo.type === RESERVED; 36 | } 37 | if (isInjectedType(type)) { 38 | return false; 39 | } 40 | return false; 41 | } 42 | 43 | export function shouldRemoveAttributeWithWarning(type, name, value, propertyInfo) { 44 | if (propertyInfo !== null && propertyInfo.type === RESERVED) { 45 | return false; 46 | } 47 | switch (typeof value) { 48 | case "boolean": 49 | if (isInjectedType(type)) { 50 | return false; 51 | } 52 | return propertyInfo === null || !propertyInfo.acceptsBooleans; 53 | case "function": 54 | if (isInjectedType(type)) { 55 | return false; 56 | } 57 | return propertyInfo === null || propertyInfo.type !== CALLBACK; 58 | case "symbol": 59 | return true; 60 | default: 61 | return false; 62 | } 63 | } 64 | 65 | export function shouldRemoveAttribute(type, name, value, propertyInfo) { 66 | if (typeof value === "undefined") { 67 | return true; 68 | } 69 | if (shouldRemoveAttributeWithWarning(type, name, value, propertyInfo)) { 70 | return true; 71 | } 72 | if (propertyInfo !== null) { 73 | switch (propertyInfo.type) { 74 | case CALLBACK: 75 | return typeof value !== "function"; 76 | case NUMERIC: 77 | return isNaN(value); 78 | case POSITIVE_NUMERIC: 79 | return isNaN(value) || value < 0; 80 | case VECTOR: 81 | const vector = parsePoint(value); 82 | return vector.length === 0 || vector.findIndex(x => isNaN(x)) !== -1; 83 | } 84 | } 85 | return false; 86 | } 87 | 88 | export function getPropertyInfo(name) { 89 | return properties.hasOwnProperty(name) ? properties[name] : null; 90 | } 91 | 92 | export function getCustomPropertyInfo(name, type) { 93 | return customProperties.hasOwnProperty(type) && customProperties[type].hasOwnProperty(name) 94 | ? customProperties[type][name] 95 | : null; 96 | } 97 | 98 | export function PropertyInfoRecord(name, type) { 99 | this.acceptsBooleans = type === BOOLEAN; 100 | this.propertyName = name; 101 | this.type = type; 102 | } 103 | 104 | // When adding attributes to this list, be sure to also add them to 105 | // the `possibleStandardNames` module to ensure casing and incorrect 106 | // name warnings. 107 | const properties = {}; 108 | export const customProperties = {}; 109 | 110 | // These props are reserved by React. They shouldn't be written to the DOM. 111 | ["children", "parent"].forEach(name => { 112 | properties[name] = new PropertyInfoRecord(name, RESERVED); 113 | }); 114 | 115 | // let otherProps = [ 116 | // "align", 117 | // "blendMode", 118 | // "canvas", 119 | // "context", 120 | // "cursor", 121 | // "filterArea", 122 | // "filters", 123 | // "font", 124 | // "hitArea", 125 | // "lineWidth", 126 | // "mask", 127 | // "name", 128 | // "pluginName", 129 | // "shader", 130 | // "style", 131 | // "text", 132 | // "texture", 133 | // "tileTransform", 134 | // "transform", 135 | // "uvTransform", 136 | // ]; 137 | 138 | // These are PIXI boolean attributes. 139 | [ 140 | "autoResize", 141 | "buttonMode", 142 | "cacheAsBitmap", 143 | "interactive", 144 | "interactiveChildren", 145 | "isMask", 146 | "nativeLines", 147 | "renderable", 148 | "roundPixels", 149 | "uvRespectAnchor", 150 | "visible", 151 | ].forEach(name => { 152 | properties[name] = new PropertyInfoRecord(name, BOOLEAN); 153 | }); 154 | 155 | // These are PIXI attributes that must be positive numbers. 156 | ["alpha", "fillAlpha", "height", "lineColor", "maxWidth", "resolution", "tint", "width"].forEach(name => { 157 | properties[name] = new PropertyInfoRecord(name, POSITIVE_NUMERIC); 158 | }); 159 | 160 | // These are PIXI attributes that must be numbers. 161 | ["boundsPadding", "clampMargin", "rotation", "x", "y"].forEach(name => { 162 | properties[name] = new PropertyInfoRecord(name, NUMERIC); 163 | }); 164 | 165 | ["anchor", "pivot", "position", "scale", "skew", "tilePosition", "tileScale"].forEach(name => { 166 | properties[name] = new PropertyInfoRecord(name, VECTOR); 167 | }); 168 | 169 | [ 170 | // pixi.js < 7.0 171 | "added", 172 | "click", 173 | "mousedown", 174 | "mousemove", 175 | "mouseout", 176 | "mouseover", 177 | "mouseup", 178 | "mouseupoutside", 179 | "pointercancel", 180 | "pointerdown", 181 | "pointermove", 182 | "pointerout", 183 | "pointerover", 184 | "pointertap", 185 | "pointerup", 186 | "pointerupoutside", 187 | "removed", 188 | "rightclick", 189 | "rightdown", 190 | "rightup", 191 | "rightupoutside", 192 | "tap", 193 | "touchcancel", 194 | "touchend", 195 | "touchendoutside", 196 | "touchmove", 197 | "touchstart", 198 | // pixi.js >= 7.1 199 | "onclick", 200 | "onmousedown", 201 | "onmouseenter", 202 | "onmouseleave", 203 | "onmousemove", 204 | "onmouseout", 205 | "onmouseover", 206 | "onmouseup", 207 | "onmouseupoutside", 208 | "onpointercancel", 209 | "onpointerdown", 210 | "onpointerenter", 211 | "onpointerleave", 212 | "onpointermove", 213 | "onpointerout", 214 | "onpointerover", 215 | "onpointertap", 216 | "onpointerup", 217 | "onpointerupoutside", 218 | "onrightclick", 219 | "onrightdown", 220 | "onrightup", 221 | "onrightupoutside", 222 | "ontap", 223 | "ontouchcancel", 224 | "ontouchend", 225 | "ontouchendoutside", 226 | "ontouchmove", 227 | "ontouchstart", 228 | "onwheel", 229 | ].forEach(name => { 230 | properties[name] = new PropertyInfoRecord(name, CALLBACK); 231 | }); 232 | -------------------------------------------------------------------------------- /src/PixiPropertyOperations.js: -------------------------------------------------------------------------------- 1 | // Based on: https://github.com/facebook/react/blob/27535e7bfcb63e8a4d65f273311e380b4ca12eff/packages/react-dom/src/client/DOMPropertyOperations.js 2 | import warning from "fbjs/lib/warning"; 3 | import { getPropertyInfo, shouldIgnoreAttribute, shouldRemoveAttribute } from "./PixiProperty"; 4 | import { defaultProps } from "./props"; 5 | import { getStackAddendum } from "./ReactGlobalSharedState"; 6 | import { findStrictRoot, setPixiValue } from "./utils"; 7 | 8 | export function getDefaultValue(type, propName) { 9 | const defaultValues = defaultProps[type]; 10 | if (typeof defaultValues !== "undefined") { 11 | return defaultValues[propName]; 12 | } 13 | } 14 | 15 | /** 16 | * Sets the value for a property on a PIXI.DisplayObject instance. 17 | * 18 | * @param {string} type 19 | * @param {PIXI.DisplayObject} instance 20 | * @param {string} propName 21 | * @param {*} value 22 | * @param {*} internalHandle 23 | */ 24 | export function setValueForProperty(type, instance, propName, value, internalHandle) { 25 | const propertyInfo = getPropertyInfo(propName); 26 | let strictRoot = null; 27 | if (__DEV__) { 28 | strictRoot = findStrictRoot(internalHandle); 29 | } 30 | 31 | if (shouldIgnoreAttribute(type, propName, propertyInfo)) { 32 | return; 33 | } 34 | let shouldIgnoreValue = false; 35 | if (shouldRemoveAttribute(type, propName, value, propertyInfo)) { 36 | // Try to reset to property to default value (if it is defined) otherwise ignore provided value. 37 | // This is the original behaviour of react-pixi@0.9.19 (on which this is based) and react-pixi-fiber@0.14.3, 38 | // left here for backwards compatibility. 39 | // TODO This is not the best solution as it makes it impossible to remove props that were once set. 40 | // Setting value to null or undefined makes behaviour of props used by PIXI unstable/undefined. 41 | // Deleting properties i another idea, however with many getters/setters defined by PIXI it is not trivial. 42 | const defaultValue = getDefaultValue(type, propName); 43 | if (typeof defaultValue !== "undefined") { 44 | value = defaultValue; 45 | if (strictRoot != null) { 46 | warning( 47 | false, 48 | "Received undefined for prop `%s` on `<%s />`. Setting default value to `%s`.%s", 49 | propName, 50 | type, 51 | value, 52 | getStackAddendum() 53 | ); 54 | } 55 | } else { 56 | shouldIgnoreValue = true; 57 | if (strictRoot != null) { 58 | warning( 59 | false, 60 | "Received undefined for prop `%s` on `<%s />`. Cannot determine default value. Ignoring.%s", 61 | propName, 62 | type, 63 | getStackAddendum() 64 | ); 65 | } 66 | } 67 | } 68 | 69 | if (!shouldIgnoreValue) { 70 | setPixiValue(instance, propName, value); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/ReactGlobalSharedState.js: -------------------------------------------------------------------------------- 1 | // Based on: https://github.com/facebook/react/blob/6c1fba539adbc3088c425a4d832bb8c4132e7b31/packages/shared/ReactGlobalSharedState.js 2 | import React from "react"; 3 | 4 | const ReactInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; 5 | 6 | export const ReactDebugCurrentFrame = __DEV__ ? ReactInternals.ReactDebugCurrentFrame : null; 7 | 8 | export function getStackAddendum() { 9 | if (ReactDebugCurrentFrame == null) { 10 | return ""; 11 | } 12 | 13 | const stack = ReactDebugCurrentFrame.getStackAddendum(); 14 | return stack != null ? stack : ""; 15 | } 16 | -------------------------------------------------------------------------------- /src/ReactPixiFiber.js: -------------------------------------------------------------------------------- 1 | import ReactFiberReconciler from "react-reconciler"; 2 | import { DefaultEventPriority } from "react-reconciler/constants"; 3 | import emptyObject from "fbjs/lib/emptyObject"; 4 | import invariant from "fbjs/lib/invariant"; 5 | import { createInstance, setInitialProperties, diffProperties, updateProperties } from "./ReactPixiFiberComponent"; 6 | import { validateProperties as validateUnknownProperties } from "./ReactPixiFiberUnknownPropertyHook"; 7 | import { findStrictRoot } from "./utils"; 8 | 9 | let validatePropertiesInDevelopment; 10 | 11 | if (__DEV__) { 12 | validatePropertiesInDevelopment = function (type, props, internalHandle) { 13 | const strictRoot = findStrictRoot(internalHandle); 14 | if (strictRoot != null) { 15 | validateUnknownProperties(type, props); 16 | } 17 | }; 18 | } 19 | 20 | /* PixiJS Renderer */ 21 | 22 | const noTimeout = -1; 23 | 24 | export function afterActiveInstanceBlur() { 25 | // Noop 26 | } 27 | 28 | export function appendChild(parentInstance, child) { 29 | if (parentInstance == null) return; 30 | 31 | // TODO do we need to remove the child first if it's already added? 32 | parentInstance.removeChild(child); 33 | 34 | parentInstance.addChild(child); 35 | if (typeof child._customDidAttach === "function") { 36 | child._customDidAttach(child); 37 | } 38 | } 39 | 40 | export function removeChild(parentInstance, child) { 41 | if (typeof child._customWillDetach === "function") { 42 | child._customWillDetach(child); 43 | } 44 | 45 | parentInstance.removeChild(child); 46 | 47 | child.destroy({ children: true }); 48 | } 49 | 50 | export function insertBefore(parentInstance, child, beforeChild) { 51 | invariant(child !== beforeChild, "ReactPixiFiber cannot insert node before itself"); 52 | 53 | const childExists = parentInstance.children.indexOf(child) !== -1; 54 | 55 | if (childExists) { 56 | parentInstance.removeChild(child); 57 | } 58 | 59 | const index = parentInstance.getChildIndex(beforeChild); 60 | parentInstance.addChildAt(child, index); 61 | } 62 | 63 | export function commitUpdate(instance, updatePayload, type, prevProps, nextProps, internalHandle) { 64 | updateProperties(type, instance, updatePayload, prevProps, nextProps, internalHandle); 65 | 66 | if (__DEV__) { 67 | validatePropertiesInDevelopment(type, nextProps, internalHandle); 68 | } 69 | } 70 | 71 | export function createTextInstance(text, rootContainer, hostContext, internalHandle) { 72 | invariant(false, "ReactPixiFiber does not support text instances. Use `Text` component instead."); 73 | } 74 | 75 | export function detachDeletedInstance(node) { 76 | // Noop 77 | } 78 | 79 | export function finalizeInitialChildren(instance, type, props, rootContainer, hostContext) { 80 | setInitialProperties(type, instance, props, rootContainer, hostContext); 81 | return true; 82 | } 83 | 84 | export function getChildHostContext(parentHostContext, type, rootContainer) { 85 | return parentHostContext; 86 | } 87 | 88 | export function getCurrentEventPriority() { 89 | return DefaultEventPriority; 90 | } 91 | 92 | export function getInstanceFromNode() { 93 | invariant(false, "Not yet implemented."); 94 | } 95 | 96 | export function getInstanceFromScope() { 97 | invariant(false, "Not yet implemented."); 98 | } 99 | 100 | export function getRootHostContext(rootContainer) { 101 | return emptyObject; 102 | } 103 | 104 | export function getPublicInstance(instance) { 105 | return instance; 106 | } 107 | 108 | export function prepareForCommit(containerInfo) { 109 | return null; 110 | } 111 | 112 | export function preparePortalMount(containerInfo) { 113 | // Noop 114 | } 115 | 116 | export function prepareUpdate(instance, type, oldProps, newProps, rootContainer, hostContext) { 117 | return diffProperties(type, instance, oldProps, newProps); 118 | } 119 | 120 | export function prepareScopeUpdate() { 121 | // Noop 122 | } 123 | 124 | export function resetAfterCommit(containerInfo) { 125 | // Noop 126 | } 127 | 128 | export function resetTextContent(instance) { 129 | // Noop 130 | } 131 | 132 | export function scheduleTimeout(fn, delay) { 133 | setTimeout(fn, delay); 134 | } 135 | 136 | export function shouldSetTextContent(type, props) { 137 | return false; 138 | } 139 | 140 | export function beforeActiveInstanceBlur() { 141 | // Noop 142 | } 143 | 144 | export function commitTextUpdate(textInstance, prevText, nextText) { 145 | // Noop 146 | } 147 | 148 | export function cancelTimeout(id) { 149 | clearTimeout(id); 150 | } 151 | 152 | export function clearContainer(container) { 153 | if (container) { 154 | container.removeChildren(); 155 | } 156 | } 157 | 158 | export function commitMount(instance, type, props, internalHandle) { 159 | if (__DEV__) { 160 | validatePropertiesInDevelopment(type, props, internalHandle); 161 | } 162 | } 163 | 164 | export function hideInstance(instance) { 165 | instance.visible = false; 166 | } 167 | 168 | export function unhideInstance(instance, props) { 169 | instance.visible = typeof props.visible !== "undefined" ? props.visible : true; 170 | } 171 | 172 | export function hideTextInstance(instance) { 173 | // Noop 174 | } 175 | 176 | export function unhideTextInstance(instance, props) { 177 | // Noop 178 | } 179 | 180 | export function now() { 181 | return typeof performance === "object" && typeof performance.now === "function" 182 | ? () => performance.now() 183 | : () => Date.now(); 184 | } 185 | 186 | export const supportsMutation = true; 187 | export const supportsPersistence = false; 188 | export const supportsHydration = false; 189 | export const supportsMicrotasks = true; 190 | 191 | const hostConfig = { 192 | afterActiveInstanceBlur: afterActiveInstanceBlur, 193 | appendChild: appendChild, 194 | appendChildToContainer: appendChild, 195 | appendInitialChild: appendChild, 196 | beforeActiveInstanceBlur: beforeActiveInstanceBlur, 197 | cancelTimeout: cancelTimeout, 198 | clearContainer: clearContainer, 199 | commitMount: commitMount, 200 | commitTextUpdate: commitTextUpdate, 201 | commitUpdate: commitUpdate, 202 | createInstance: createInstance, 203 | createTextInstance: createTextInstance, 204 | detachDeletedInstance: detachDeletedInstance, 205 | finalizeInitialChildren: finalizeInitialChildren, 206 | getChildHostContext: getChildHostContext, 207 | getCurrentEventPriority: getCurrentEventPriority, 208 | getInstanceFromNode: getInstanceFromNode, 209 | getInstanceFromScope: getInstanceFromScope, 210 | getRootHostContext: getRootHostContext, 211 | getPublicInstance: getPublicInstance, 212 | hideInstance: hideInstance, 213 | hideTextInstance: hideTextInstance, 214 | insertBefore: insertBefore, 215 | insertInContainerBefore: insertBefore, 216 | noTimeout: noTimeout, 217 | now: now, 218 | prepareForCommit: prepareForCommit, 219 | preparePortalMount: preparePortalMount, 220 | prepareUpdate: prepareUpdate, 221 | prepareScopeUpdate: prepareScopeUpdate, 222 | removeChild: removeChild, 223 | removeChildFromContainer: removeChild, 224 | resetAfterCommit: resetAfterCommit, 225 | resetTextContent: resetTextContent, 226 | scheduleTimeout: scheduleTimeout, 227 | shouldSetTextContent: shouldSetTextContent, 228 | supportsHydration: supportsHydration, 229 | scheduleMicrotask: 230 | typeof queueMicrotask === "function" 231 | ? queueMicrotask 232 | : typeof Promise !== "undefined" 233 | ? callback => 234 | Promise.resolve(null) 235 | .then(callback) 236 | .catch(error => { 237 | setTimeout(() => { 238 | throw error; 239 | }); 240 | }) 241 | : setTimeout, 242 | supportsMicrotasks: true, 243 | supportsMutation: supportsMutation, 244 | supportsPersistence: supportsPersistence, 245 | 246 | unhideInstance: unhideInstance, 247 | unhideTextInstance: unhideTextInstance, 248 | }; 249 | 250 | // React Pixi Fiber renderer is primary if used without React DOM 251 | export const ReactPixiFiberAsPrimaryRenderer = ReactFiberReconciler({ ...hostConfig, isPrimaryRenderer: true }); 252 | 253 | // React Pixi Fiber renderer is secondary to React DOM renderer if used together 254 | export const ReactPixiFiberAsSecondaryRenderer = ReactFiberReconciler({ ...hostConfig, isPrimaryRenderer: false }); 255 | 256 | export default ReactPixiFiberAsSecondaryRenderer; 257 | -------------------------------------------------------------------------------- /src/ReactPixiFiberComponent.js: -------------------------------------------------------------------------------- 1 | // Based on: https://github.com/facebook/react/blob/27535e7bfcb63e8a4d65f273311e380b4ca12eff/packages/react-dom/src/client/ReactDOMFiberComponent.js 2 | import invariant from "fbjs/lib/invariant"; 3 | import * as PIXI from "pixi.js"; 4 | import { CHILDREN } from "./props"; 5 | import { TYPES } from "./types"; 6 | import { createInjectedTypeInstance, isInjectedType } from "./inject"; 7 | import { setValueForProperty } from "./PixiPropertyOperations"; 8 | 9 | export function createInstance(type, props, rootContainer, hostContext, internalHandle) { 10 | let instance; 11 | 12 | switch (type) { 13 | case TYPES.BITMAP_TEXT: 14 | const style = 15 | typeof props.style !== "undefined" 16 | ? props.style 17 | : { 18 | align: props.align, 19 | font: props.font, 20 | tint: props.tint, 21 | }; 22 | try { 23 | instance = new PIXI.extras.BitmapText(props.text, style); 24 | } catch (e) { 25 | instance = new PIXI.BitmapText(props.text, style); 26 | } 27 | break; 28 | case TYPES.CONTAINER: 29 | instance = new PIXI.Container(); 30 | break; 31 | case TYPES.GRAPHICS: 32 | instance = new PIXI.Graphics(); 33 | break; 34 | case TYPES.NINE_SLICE_PLANE: 35 | try { 36 | instance = new PIXI.mesh.NineSlicePlane( 37 | props.texture, 38 | props.leftWidth, 39 | props.topHeight, 40 | props.rightWidth, 41 | props.bottomHeight 42 | ); 43 | } catch (e) { 44 | instance = new PIXI.NineSlicePlane( 45 | props.texture, 46 | props.leftWidth, 47 | props.topHeight, 48 | props.rightWidth, 49 | props.bottomHeight 50 | ); 51 | } 52 | break; 53 | case TYPES.PARTICLE_CONTAINER: 54 | try { 55 | instance = new PIXI.particles.ParticleContainer( 56 | props.maxSize, 57 | props.properties, 58 | props.batchSize, 59 | props.autoResize 60 | ); 61 | } catch (e) { 62 | instance = new PIXI.ParticleContainer(props.maxSize, props.properties, props.batchSize, props.autoResize); 63 | } 64 | break; 65 | case TYPES.SPRITE: 66 | instance = new PIXI.Sprite(props.texture); 67 | break; 68 | case TYPES.TEXT: 69 | instance = new PIXI.Text(props.text, props.style, props.canvas); 70 | break; 71 | case TYPES.TILING_SPRITE: 72 | try { 73 | instance = new PIXI.extras.TilingSprite(props.texture, props.width, props.height); 74 | } catch (e) { 75 | instance = new PIXI.TilingSprite(props.texture, props.width, props.height); 76 | } 77 | break; 78 | default: 79 | instance = createInjectedTypeInstance( 80 | type, 81 | props, 82 | rootContainer, 83 | hostContext, 84 | internalHandle, 85 | applyDisplayObjectProps 86 | ); 87 | break; 88 | } 89 | 90 | invariant(instance, "ReactPixiFiber does not support the type: `%s`.", type); 91 | 92 | return instance; 93 | } 94 | 95 | export function setInitialCustomComponentProperties(type, instance, rawProps, rootContainer, hostContext) { 96 | instance._customApplyProps(instance, undefined, rawProps); 97 | } 98 | 99 | export function setInitialPixiProperties(type, instance, rawProps, rootContainer, hostContext) { 100 | for (const propKey in rawProps) { 101 | if (!rawProps.hasOwnProperty(propKey)) { 102 | continue; 103 | } 104 | const nextProp = rawProps[propKey]; 105 | if (propKey === CHILDREN) { 106 | // Noop. Text children not supported 107 | } else { 108 | setValueForProperty(type, instance, propKey, nextProp); 109 | } 110 | } 111 | } 112 | 113 | export function setInitialProperties(type, instance, rawProps, rootContainer, hostContext) { 114 | // injected types with customApplyProps need to have full control over passed props 115 | if (isInjectedType(type) && typeof instance._customApplyProps === "function") { 116 | setInitialCustomComponentProperties(type, instance, rawProps, rootContainer, hostContext); 117 | return; 118 | } 119 | 120 | setInitialPixiProperties(type, instance, rawProps, rootContainer, hostContext); 121 | } 122 | 123 | // Calculate the diff between the two objects. 124 | // See: https://github.com/facebook/react/blob/97e2911/packages/react-dom/src/client/ReactDOMFiberComponent.js#L546 125 | export function diffProperties(type, instance, lastRawProps, nextRawProps) { 126 | let updatePayload = null; 127 | 128 | let lastProps = lastRawProps; 129 | let nextProps = nextRawProps; 130 | let propKey; 131 | 132 | for (propKey in lastProps) { 133 | if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey)) { 134 | continue; 135 | } 136 | if (propKey === CHILDREN) { 137 | // Noop. Text children not supported 138 | } else { 139 | // For all other deleted properties we add it to the queue. We use 140 | // the whitelist in the commit phase instead. 141 | (updatePayload = updatePayload || []).push(propKey, null); 142 | } 143 | } 144 | for (propKey in nextProps) { 145 | const nextProp = nextProps[propKey]; 146 | const lastProp = lastProps != null ? lastProps[propKey] : undefined; 147 | if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) { 148 | continue; 149 | } 150 | if (propKey === CHILDREN) { 151 | // Noop. Text children not supported 152 | } else { 153 | // For any other property we always add it to the queue and then we 154 | // filter it out using the whitelist during the commit. 155 | (updatePayload = updatePayload || []).push(propKey, nextProp); 156 | } 157 | } 158 | return updatePayload; 159 | } 160 | 161 | export function applyDisplayObjectProps(type, instance, oldProps, newProps) { 162 | const updatePayload = diffProperties(type, instance, oldProps, newProps); 163 | if (updatePayload !== null) { 164 | updatePixiProperties(type, instance, updatePayload); 165 | } 166 | } 167 | 168 | export function updateCustomComponentProperties(type, instance, updatePayload, prevProps, nextProps, internalHandle) { 169 | instance._customApplyProps(instance, prevProps, nextProps); 170 | } 171 | 172 | export function updatePixiProperties(type, instance, updatePayload, prevProps, nextProps, internalHandle) { 173 | for (let i = 0; i < updatePayload.length; i += 2) { 174 | const propKey = updatePayload[i]; 175 | const propValue = updatePayload[i + 1]; 176 | if (propKey === CHILDREN) { 177 | // Noop. Text children not supported 178 | } else { 179 | setValueForProperty(type, instance, propKey, propValue, internalHandle); 180 | } 181 | } 182 | } 183 | 184 | // Apply the diff. 185 | export function updateProperties(type, instance, updatePayload, prevProps, nextProps, internalHandle) { 186 | // injected types with customApplyProps need to have full control over passed props 187 | if (isInjectedType(type) && typeof instance._customApplyProps === "function") { 188 | updateCustomComponentProperties(type, instance, updatePayload, prevProps, nextProps, internalHandle); 189 | return; 190 | } 191 | 192 | updatePixiProperties(type, instance, updatePayload, prevProps, nextProps, internalHandle); 193 | } 194 | -------------------------------------------------------------------------------- /src/ReactPixiFiberUnknownPropertyHook.js: -------------------------------------------------------------------------------- 1 | // Based on: https://github.com/facebook/react/blob/27535e7bfcb63e8a4d65f273311e380b4ca12eff/packages/react-dom/src/shared/ReactDOMUnknownPropertyHook.js 2 | import emptyFunction from "fbjs/lib/emptyFunction"; 3 | import warning from "fbjs/lib/warning"; 4 | import { RESERVED, getPropertyInfo, getCustomPropertyInfo, shouldRemoveAttributeWithWarning } from "./PixiProperty"; 5 | import { getStackAddendum } from "./ReactGlobalSharedState"; 6 | import { isInjectedType } from "./inject"; 7 | import possibleStandardNames from "./possibleStandardNames"; 8 | 9 | let validateProperty = emptyFunction; 10 | 11 | if (__DEV__) { 12 | const warnedProperties = {}; 13 | const hasOwnProperty = Object.prototype.hasOwnProperty; 14 | const EVENT_NAME_REGEX = /^on./; 15 | 16 | validateProperty = function (type, name, value) { 17 | if (hasOwnProperty.call(warnedProperties, name) && warnedProperties[name]) { 18 | return true; 19 | } 20 | 21 | const lowerCasedName = name.toLowerCase(); 22 | 23 | if (EVENT_NAME_REGEX.test(name)) { 24 | warning( 25 | false, 26 | "Invalid event handler prop `%s` on `<%s />`. PIXI events use other naming convention, for example `click`.%s", 27 | name, 28 | type, 29 | getStackAddendum() 30 | ); 31 | warnedProperties[name] = true; 32 | return true; 33 | } 34 | 35 | if (typeof value === "number" && isNaN(value)) { 36 | warning( 37 | false, 38 | "Received NaN for prop `%s` on `<%s />`. If this is expected, cast the value to a string.%s", 39 | name, 40 | type, 41 | getStackAddendum() 42 | ); 43 | warnedProperties[name] = true; 44 | return true; 45 | } 46 | 47 | const propertyInfo = getPropertyInfo(name); 48 | const customPropertyInfo = getCustomPropertyInfo(name, type); 49 | const isReserved = propertyInfo !== null && propertyInfo.type === RESERVED; 50 | 51 | // Known attributes should match the casing specified in the property config. 52 | if ( 53 | typeof possibleStandardNames[type] !== "undefined" && 54 | possibleStandardNames[type].hasOwnProperty(lowerCasedName) 55 | ) { 56 | const standardName = possibleStandardNames[type][lowerCasedName]; 57 | if (standardName !== name) { 58 | warning( 59 | false, 60 | "Invalid prop `%s` on `<%s />`. Did you mean `%s`?%s", 61 | name, 62 | type, 63 | standardName, 64 | getStackAddendum() 65 | ); 66 | warnedProperties[name] = true; 67 | return true; 68 | } 69 | } else if (!isReserved && typeof value !== "undefined") { 70 | warning( 71 | false, 72 | "React does not recognize prop `%s` on `<%s />`. If you accidentally passed it from a parent component, remove it from `<%s />`.%s", 73 | name, 74 | type, 75 | type, 76 | getStackAddendum() 77 | ); 78 | warnedProperties[name] = true; 79 | return true; 80 | } 81 | 82 | // Now that we've validated casing, do not validate 83 | // data types for reserved props 84 | if (isReserved) { 85 | return true; 86 | } 87 | 88 | // Warn when a known attribute is a bad type 89 | if (shouldRemoveAttributeWithWarning(type, name, value, propertyInfo)) { 90 | warnedProperties[name] = true; 91 | return false; 92 | } 93 | 94 | // Warn when custom property does not pass custom validation 95 | if (customPropertyInfo != null && !customPropertyInfo.type(value)) { 96 | warnedProperties[name] = true; 97 | return false; 98 | } 99 | 100 | return true; 101 | }; 102 | } 103 | 104 | export { validateProperty }; 105 | 106 | export const warnUnknownProperties = function (type, props) { 107 | const unknownProps = []; 108 | for (const key in props) { 109 | const isValid = validateProperty(type, key, props[key]); 110 | if (!isValid) { 111 | unknownProps.push(key); 112 | } 113 | } 114 | 115 | const unknownPropString = unknownProps.map(prop => "`" + prop + "`").join(", "); 116 | if (unknownProps.length === 1) { 117 | warning(false, "Invalid value for prop %s on `<%s />`.%s", unknownPropString, type, getStackAddendum()); 118 | } else if (unknownProps.length > 1) { 119 | warning(false, "Invalid values for props %s on `<%s />`.%s", unknownPropString, type, getStackAddendum()); 120 | } 121 | }; 122 | 123 | export function validateProperties(type, props) { 124 | if (isInjectedType(type)) { 125 | return; 126 | } 127 | warnUnknownProperties(type, props); 128 | } 129 | -------------------------------------------------------------------------------- /src/Stage/common.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { AppProvider } from "../AppProvider"; 3 | import { ReactPixiFiberAsSecondaryRenderer } from "../ReactPixiFiber"; 4 | import { diffProperties, setInitialProperties, updateProperties } from "../ReactPixiFiberComponent"; 5 | import { createRender, createUnmount } from "../render"; 6 | import { getContainerProps } from "./propTypes"; 7 | import { TYPES } from "../types"; 8 | 9 | export const render = createRender(ReactPixiFiberAsSecondaryRenderer); 10 | export const unmount = createUnmount(ReactPixiFiberAsSecondaryRenderer); 11 | 12 | export const STAGE_OPTIONS_RECREATE = false; 13 | export const STAGE_OPTIONS_UNMOUNT = true; 14 | 15 | export function cleanupStage(app, stageOptions = STAGE_OPTIONS_RECREATE) { 16 | // Do not remove canvas from DOM, there are two ways canvas made it's way to PIXI.Application: 17 | // 1) canvas was rendered by Stage component - it will be removed by React when Stage is unmounted 18 | // 2) canvas was passed in options as `view` - removing canvas created externally may have unexpected consequences 19 | const removeView = false; 20 | 21 | // Unmount stage tree 22 | unmount(app.stage); 23 | 24 | // Give components a chance to finish unmounting before destroying PIXI.Application 25 | setTimeout(() => { 26 | // Destroy PIXI.Application and what it rendered if necessary 27 | app.destroy(removeView, stageOptions); 28 | }, 0); 29 | } 30 | 31 | export function getDimensions(props) { 32 | const { 33 | options: { height, width }, 34 | } = props; 35 | 36 | return [width, height]; 37 | } 38 | 39 | export function renderApp(app, props, instance) { 40 | const provider = <AppProvider app={app}>{props.children}</AppProvider>; 41 | 42 | if (typeof instance === "object") { 43 | render(provider, app.stage, undefined, instance); 44 | } else { 45 | render(provider, app.stage); 46 | } 47 | } 48 | 49 | export function renderStage(app, props, instance) { 50 | // Determine what props to apply 51 | const stageProps = getContainerProps(props); 52 | 53 | setInitialProperties(TYPES.CONTAINER, app.stage, stageProps); 54 | renderApp(app, props, instance); 55 | } 56 | 57 | export function rerenderStage(app, oldProps, newProps, instance) { 58 | // Determine what has changed 59 | const oldStageProps = getContainerProps(oldProps); 60 | const newStageProps = getContainerProps(newProps); 61 | const updatePayload = diffProperties(TYPES.CONTAINER, app.stage, oldStageProps, newStageProps); 62 | 63 | if (updatePayload !== null) { 64 | updateProperties(TYPES.CONTAINER, app.stage, updatePayload); 65 | } 66 | 67 | renderApp(app, newProps, instance); 68 | } 69 | 70 | export function resizeRenderer(app, oldProps, newProps) { 71 | const [oldWidth, oldHeight] = getDimensions(oldProps); 72 | const [newWidth, newHeight] = getDimensions(newProps); 73 | 74 | if (newHeight !== oldHeight || newWidth !== oldWidth) { 75 | app.renderer.resize(newWidth, newHeight); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Stage/hooks.js: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useRef } from "react"; 2 | import emptyObject from "fbjs/lib/emptyObject"; 3 | import invariant from "fbjs/lib/invariant"; 4 | import shallowEqual from "fbjs/lib/shallowEqual"; 5 | import { createPixiApplication } from "../utils"; 6 | import { 7 | cleanupStage, 8 | renderStage, 9 | rerenderStage, 10 | resizeRenderer, 11 | STAGE_OPTIONS_RECREATE, 12 | STAGE_OPTIONS_UNMOUNT, 13 | } from "./common"; 14 | import { defaultProps, getCanvasProps, propTypes } from "./propTypes"; 15 | import * as PIXI from "pixi.js"; 16 | 17 | export function usePreviousProps(value) { 18 | const props = useRef(emptyObject); 19 | 20 | useEffect(() => { 21 | props.current = value; 22 | }); 23 | 24 | return props.current; 25 | } 26 | 27 | export function useStageRenderer(props, appRef, canvasRef) { 28 | // create app on mount 29 | useLayoutEffect(() => { 30 | const { app, options } = props; 31 | 32 | // Return PIXI.Application if it was provided in props 33 | if (app != null) { 34 | invariant(app instanceof PIXI.Application, "Provided `app` has to be an instance of PIXI.Application"); 35 | appRef.current = app; 36 | 37 | renderStage(appRef.current, props); 38 | 39 | // Not destroying provided PIXI.Application when unmounting 40 | return; 41 | } 42 | 43 | const view = canvasRef.current; 44 | 45 | // Create new PIXI.Application 46 | // Canvas passed in options as `view` will be used if provided 47 | appRef.current = createPixiApplication({ view, ...options }); 48 | 49 | renderStage(appRef.current, props); 50 | 51 | // Cleanup current PIXI.Application when unmounting 52 | return function cleanup() { 53 | cleanupStage(appRef.current, STAGE_OPTIONS_UNMOUNT); 54 | }; 55 | // eslint-disable-next-line react-hooks/exhaustive-deps 56 | }, []); 57 | } 58 | 59 | export function useStageRerenderer(props, appRef, canvasRef) { 60 | const prevProps = usePreviousProps(props); 61 | 62 | // eslint-disable-next-line react-hooks/exhaustive-deps 63 | useLayoutEffect(() => { 64 | // This is first render, no need to do anything 65 | if (!appRef.current || prevProps === emptyObject) return; 66 | 67 | const { app } = props; 68 | 69 | if (app instanceof PIXI.Application) { 70 | // Update stage tree 71 | rerenderStage(appRef.current, prevProps, props); 72 | 73 | return; 74 | } 75 | 76 | const { 77 | options, 78 | options: { height, width, ...otherOptions }, 79 | } = props; 80 | const { 81 | options: { height: prevHeight, width: prevWidth, ...prevOtherOptions }, 82 | } = prevProps; 83 | const view = canvasRef.current; 84 | 85 | // We need to create new PIXI.Application when options other than dimensions 86 | // are changed because some renderer settings are immutable. 87 | if (!shallowEqual(otherOptions, prevOtherOptions)) { 88 | // Destroy PIXI.Application 89 | cleanupStage(appRef.current, STAGE_OPTIONS_RECREATE); 90 | 91 | // Create new PIXI.Application 92 | // Canvas passed in options as `view` will be used if provided 93 | appRef.current = createPixiApplication({ view, ...options }); 94 | 95 | // Set initial properties 96 | renderStage(appRef.current, props); 97 | } else { 98 | // Update stage tree 99 | rerenderStage(appRef.current, prevProps, props); 100 | // Update canvas and renderer dimestions 101 | resizeRenderer(appRef.current, prevProps, props); 102 | } 103 | }); 104 | } 105 | 106 | export default function createStageFunction() { 107 | const Stage = forwardRef(function Stage(props, ref) { 108 | const { app, options } = props; 109 | 110 | // Store PIXI.Application instance 111 | const appRef = useRef(); 112 | // Store canvas if it was rendered 113 | const canvasRef = useRef(); 114 | 115 | useImperativeHandle(ref, () => ({ 116 | _app: appRef, 117 | _canvas: canvasRef, 118 | props: props, 119 | })); 120 | 121 | // The order is important here to avoid unnecessary renders or extra state: 122 | // - useStageRerenderer: 123 | // - is no-op first time it is called, because PIXI.Application is not created yet 124 | // - is responsible for applying changes to existing PIXI.Application 125 | // - useStageRenderer: 126 | // - is only called once 127 | // - is responsible for creating first PIXI.Application and destroying it when Stage is finally unmounted 128 | useStageRerenderer(props, appRef, canvasRef); 129 | useStageRenderer(props, appRef, canvasRef); 130 | 131 | // Do not render anything if PIXI.Application was provided in props 132 | if (app instanceof PIXI.Application) { 133 | return null; 134 | } 135 | 136 | // Do not render anything if canvas is passed in options as `view` 137 | if (typeof options !== "undefined" && options.view) { 138 | return null; 139 | } 140 | 141 | const canvasProps = getCanvasProps(props); 142 | 143 | return <canvas ref={canvasRef} {...canvasProps} />; 144 | }); 145 | 146 | Stage.propTypes = propTypes; 147 | Stage.defaultProps = defaultProps; 148 | 149 | return Stage; 150 | } 151 | -------------------------------------------------------------------------------- /src/Stage/index.js: -------------------------------------------------------------------------------- 1 | import { areReactHooksAvailable } from "../compat"; 2 | import createStageFunction from "./hooks"; 3 | import createStageClass from "./legacy"; 4 | 5 | export default areReactHooksAvailable() ? createStageFunction() : createStageClass(); 6 | 7 | export { createStageFunction, createStageClass }; 8 | -------------------------------------------------------------------------------- /src/Stage/legacy.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import invariant from "fbjs/lib/invariant"; 3 | import shallowEqual from "fbjs/lib/shallowEqual"; 4 | import { createRef } from "../compat"; 5 | import { createPixiApplication } from "../utils"; 6 | import { 7 | cleanupStage, 8 | renderStage, 9 | rerenderStage, 10 | resizeRenderer, 11 | STAGE_OPTIONS_RECREATE, 12 | STAGE_OPTIONS_UNMOUNT, 13 | } from "./common"; 14 | import { defaultProps, getCanvasProps, propTypes } from "./propTypes"; 15 | import * as PIXI from "pixi.js"; 16 | 17 | export default function createStageClass() { 18 | class Stage extends React.Component { 19 | constructor(props) { 20 | super(props); 21 | 22 | // Store PIXI.Application instance 23 | this._app = createRef(); 24 | // Store canvas if it was rendered 25 | this._canvas = createRef(); 26 | } 27 | 28 | componentDidMount() { 29 | const app = this.getPixiApplication(this.props); 30 | 31 | this.renderStage(app, this.props); 32 | 33 | // Store app instance 34 | this._app.current = app; 35 | } 36 | 37 | // Re-render and resize stage on component update 38 | componentDidUpdate(prevProps) { 39 | const app = this.getPixiApplication(this.props, prevProps); 40 | 41 | this.renderStage(app, this.props); 42 | this.rerenderStage(app, this.props, prevProps); 43 | 44 | // Update stored app instance 45 | this._app.current = app; 46 | } 47 | 48 | componentWillUnmount() { 49 | const { app } = this.props; 50 | 51 | // Destroy PIXI.Application if it was not provided in props 52 | if (!(app instanceof PIXI.Application)) { 53 | cleanupStage(this._app.current, STAGE_OPTIONS_UNMOUNT); 54 | } 55 | } 56 | 57 | render() { 58 | const { app, options } = this.props; 59 | 60 | // Do not render anything if PIXI.Application was provided in props 61 | if (app instanceof PIXI.Application) { 62 | return null; 63 | } 64 | 65 | // Do not render anything if canvas is passed in options as `view` 66 | if (typeof options !== "undefined" && options.view) { 67 | return null; 68 | } 69 | 70 | const canvasProps = getCanvasProps(this.props); 71 | 72 | return <canvas ref={this._canvas} {...canvasProps} />; 73 | } 74 | 75 | getPixiApplication(props, prevProps) { 76 | const { app, options } = props; 77 | 78 | // Return PIXI.Application if it was provided in props 79 | if (app != null) { 80 | invariant(app instanceof PIXI.Application, "Provided `app` has to be an instance of PIXI.Application"); 81 | return app; 82 | } 83 | 84 | const view = this._canvas.current; 85 | 86 | // Render stage for the first time 87 | if (this._app.current == null) { 88 | // Create new PIXI.Application 89 | // Canvas passed in options as `view` will be used if provided 90 | return createPixiApplication({ view, ...options }); 91 | } 92 | 93 | const { 94 | options: { height, width, ...otherOptions }, 95 | } = props; 96 | const { 97 | options: { height: prevHeight, width: prevWidth, ...prevOtherOptions }, 98 | } = prevProps; 99 | 100 | // We need to create new Application when options other than dimensions 101 | // are changed because some of the renderer settings are immutable 102 | if (!shallowEqual(otherOptions, prevOtherOptions)) { 103 | // Destroy PIXI.Application 104 | cleanupStage(this._app.current, STAGE_OPTIONS_RECREATE); 105 | 106 | // Create new PIXI.Application 107 | // Canvas passed in options as `view` will be used if provided 108 | return createPixiApplication({ view, ...options }); 109 | } 110 | 111 | // Return already existing Application otherwise 112 | return this._app.current; 113 | } 114 | 115 | renderStage(app, props) { 116 | // Only act if the app was created 117 | if (app === this._app.current) return; 118 | 119 | // Set initial properties 120 | renderStage(app, props, this); 121 | } 122 | 123 | rerenderStage(app, props, prevProps) { 124 | // Only act if new app was not created 125 | if (app !== this._app.current) return; 126 | 127 | // Update stage tree 128 | rerenderStage(app, prevProps, props, this); 129 | 130 | // Update canvas and renderer dimestions 131 | if (!(props.app instanceof PIXI.Application)) { 132 | resizeRenderer(app, prevProps, props); 133 | } 134 | } 135 | } 136 | 137 | Stage.propTypes = propTypes; 138 | Stage.defaultProps = defaultProps; 139 | 140 | return Stage; 141 | } 142 | -------------------------------------------------------------------------------- /src/Stage/propTypes.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import possibleStandardNames from "../possibleStandardNames"; 3 | import { deprecated, validateApp, validateCanvas } from "../propTypes"; 4 | import { TYPES } from "../types"; 5 | import { filterByKey, including } from "../utils"; 6 | 7 | export const propTypes = { 8 | app: validateApp, 9 | options: PropTypes.shape({ 10 | antialias: PropTypes.bool, 11 | autoStart: PropTypes.bool, 12 | backgroundColor: PropTypes.number, 13 | clearBeforeRender: PropTypes.bool, 14 | forceCanvas: PropTypes.bool, 15 | forceFXAA: PropTypes.bool, 16 | height: PropTypes.number, 17 | legacy: PropTypes.bool, 18 | powerPreference: PropTypes.string, 19 | preserveDrawingBuffer: PropTypes.bool, 20 | resolution: PropTypes.number, 21 | roundPixels: PropTypes.bool, 22 | sharedLoader: PropTypes.bool, 23 | sharedTicker: PropTypes.bool, 24 | transparent: PropTypes.bool, 25 | view: validateCanvas, 26 | width: PropTypes.number, 27 | }), 28 | children: PropTypes.node, 29 | height: deprecated(PropTypes.number, "Pass `height` in `options` prop instead."), 30 | width: deprecated(PropTypes.number, "Pass `height` in `options` prop instead."), 31 | }; 32 | 33 | export const defaultProps = { 34 | options: {}, 35 | }; 36 | 37 | export const includingContainerProps = including(Object.keys(possibleStandardNames[TYPES.CONTAINER])); 38 | export const includingStageProps = including(Object.keys(propTypes)); 39 | export const includingCanvasProps = key => !includingContainerProps(key) && !includingStageProps(key); 40 | 41 | export const getCanvasProps = props => filterByKey(props, includingCanvasProps); 42 | export const getContainerProps = props => filterByKey(props, includingContainerProps); 43 | -------------------------------------------------------------------------------- /src/compat.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function areReactHooksAvailable() { 4 | return typeof React.useEffect === "function"; 5 | } 6 | 7 | export function createRef() { 8 | return typeof React.createRef === "function" 9 | ? React.createRef() 10 | : { 11 | current: null, 12 | }; 13 | } 14 | 15 | export function isNewContextAvailable() { 16 | return typeof React.createContext === "function"; 17 | } 18 | -------------------------------------------------------------------------------- /src/hooks.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useContext, useEffect } from "react"; 3 | import { AppContext } from "./AppProvider"; 4 | 5 | export function usePixiApp() { 6 | const app = useContext(AppContext); 7 | 8 | if (app === null) { 9 | throw new Error("No PIXI.Application is available here. You can only access it in children of <Stage />"); 10 | } 11 | 12 | return app; 13 | } 14 | 15 | export function usePixiTicker(fn) { 16 | const { ticker } = usePixiApp(); 17 | 18 | useEffect(() => { 19 | ticker.add(fn); 20 | 21 | return () => { 22 | ticker.remove(fn); 23 | }; 24 | }, [fn, ticker]); 25 | } 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import CustomPIXIComponent, { CustomPIXIProperty } from "./CustomPIXIComponent"; 2 | import { AppContext, AppProvider, withApp } from "./AppProvider"; 3 | import Stage, { createStageClass } from "./Stage"; 4 | import { TYPES } from "./types"; 5 | import { usePixiApp, usePixiTicker } from "./hooks"; 6 | import { createRender, createUnmount } from "./render"; 7 | import { ReactPixiFiberAsPrimaryRenderer } from "./ReactPixiFiber"; 8 | import { applyDisplayObjectProps } from "./ReactPixiFiberComponent"; 9 | 10 | const render = createRender(ReactPixiFiberAsPrimaryRenderer); 11 | const unmount = createUnmount(ReactPixiFiberAsPrimaryRenderer); 12 | 13 | /* Public API */ 14 | 15 | export { 16 | AppContext, 17 | AppProvider, 18 | CustomPIXIComponent, 19 | CustomPIXIProperty, 20 | Stage, 21 | applyDisplayObjectProps, 22 | createStageClass, 23 | render, 24 | unmount, 25 | withApp, 26 | usePixiApp, 27 | usePixiTicker, 28 | }; 29 | 30 | export const BitmapText = TYPES.BITMAP_TEXT; 31 | export const Container = TYPES.CONTAINER; 32 | export const Graphics = TYPES.GRAPHICS; 33 | export const NineSlicePlane = TYPES.NINE_SLICE_PLANE; 34 | export const ParticleContainer = TYPES.PARTICLE_CONTAINER; 35 | export const Sprite = TYPES.SPRITE; 36 | export const Text = TYPES.TEXT; 37 | export const TilingSprite = TYPES.TILING_SPRITE; 38 | -------------------------------------------------------------------------------- /src/inject.js: -------------------------------------------------------------------------------- 1 | import invariant from "fbjs/lib/invariant"; 2 | 3 | export const INJECTED_TYPES = {}; 4 | 5 | export function injectType(type, behavior) { 6 | INJECTED_TYPES[type] = behavior; 7 | return type; 8 | } 9 | 10 | export function createInjectedTypeInstance( 11 | type, 12 | props, 13 | rootContainer, 14 | hostContext, 15 | internalHandle, 16 | applyDisplayObjectProps 17 | ) { 18 | let instance; 19 | 20 | if (type in INJECTED_TYPES) { 21 | const injectedType = INJECTED_TYPES[type]; 22 | let customDisplayObject; 23 | if (typeof injectedType === "function") { 24 | customDisplayObject = injectedType; 25 | } else if (typeof injectedType.customDisplayObject === "function") { 26 | customDisplayObject = injectedType.customDisplayObject; 27 | } 28 | 29 | invariant(customDisplayObject, "Invalid Component injected to ReactPixiFiber: `%s`.", type); 30 | 31 | instance = customDisplayObject(props); 32 | 33 | if (typeof injectedType.customApplyProps === "function") { 34 | instance._customApplyProps = injectedType.customApplyProps.bind({ 35 | // See: https://github.com/Izzimach/react-pixi/blob/a25196251a13ed9bb116a8576d93e9fceac2a14c/src/ReactPIXI.js#L953 36 | applyDisplayObjectProps: applyDisplayObjectProps.bind(null, type, instance), 37 | }); 38 | } 39 | if (typeof injectedType.customDidAttach === "function") { 40 | instance._customDidAttach = injectedType.customDidAttach; 41 | } 42 | if (typeof injectedType.customWillDetach === "function") { 43 | instance._customWillDetach = injectedType.customWillDetach; 44 | } 45 | } 46 | 47 | return instance; 48 | } 49 | 50 | export function isInjectedType(type) { 51 | return typeof INJECTED_TYPES[type] !== "undefined"; 52 | } 53 | -------------------------------------------------------------------------------- /src/possibleStandardNames.js: -------------------------------------------------------------------------------- 1 | // Based on: https://github.com/facebook/react/blob/27535e7bfcb63e8a4d65f273311e380b4ca12eff/packages/react-dom/src/shared/possibleStandardNames.js 2 | import { TYPES } from "./types"; 3 | 4 | // http://pixijs.download/release/docs/PIXI.DisplayObject.html 5 | const eventsStandardNames = { 6 | // pixi.js < 7.0 7 | added: "added", 8 | click: "click", 9 | mousedown: "mousedown", 10 | mousemove: "mousemove", 11 | mouseout: "mouseout", 12 | mouseover: "mouseover", 13 | mouseup: "mouseup", 14 | mouseupoutside: "mouseupoutside", 15 | pointercancel: "pointercancel", 16 | pointerdown: "pointerdown", 17 | pointermove: "pointermove", 18 | pointerout: "pointerout", 19 | pointerover: "pointerover", 20 | pointertap: "pointertap", 21 | pointerup: "pointerup", 22 | pointerupoutside: "pointerupoutside", 23 | removed: "removed", 24 | rightclick: "rightclick", 25 | rightdown: "rightdown", 26 | rightup: "rightup", 27 | rightupoutside: "rightupoutside", 28 | tap: "tap", 29 | touchcancel: "touchcancel", 30 | touchend: "touchend", 31 | touchendoutside: "touchendoutside", 32 | touchmove: "touchmove", 33 | touchstart: "touchstart", 34 | // pixi.js >= 7.1 35 | onclick: "onclick", 36 | onmousedown: "onmousedown", 37 | onmouseenter: "onmouseenter", 38 | onmouseleave: "onmouseleave", 39 | onmousemove: "onmousemove", 40 | onmouseout: "onmouseout", 41 | onmouseover: "onmouseover", 42 | onmouseup: "onmouseup", 43 | onmouseupoutside: "onmouseupoutside", 44 | onpointercancel: "onpointercancel", 45 | onpointerdown: "onpointerdown", 46 | onpointerenter: "onpointerenter", 47 | onpointerleave: "onpointerleave", 48 | onpointermove: "onpointermove", 49 | onpointerout: "onpointerout", 50 | onpointerover: "onpointerover", 51 | onpointertap: "onpointertap", 52 | onpointerup: "onpointerup", 53 | onpointerupoutside: "onpointerupoutside", 54 | onrightclick: "onrightclick", 55 | onrightdown: "onrightdown", 56 | onrightup: "onrightup", 57 | onrightupoutside: "onrightupoutside", 58 | ontap: "ontap", 59 | ontouchcancel: "ontouchcancel", 60 | ontouchend: "ontouchend", 61 | ontouchendoutside: "ontouchendoutside", 62 | ontouchmove: "ontouchmove", 63 | ontouchstart: "ontouchstart", 64 | onwheel: "onwheel", 65 | }; 66 | 67 | // http://pixijs.download/release/docs/PIXI.DisplayObject.html 68 | const displayObjectStandardNames = { 69 | ...eventsStandardNames, 70 | alpha: "alpha", 71 | angle: "angle", 72 | buttonmode: "buttonMode", 73 | cacheasbitmap: "cacheAsBitmap", 74 | cursor: "cursor", 75 | filterarea: "filterArea", 76 | filters: "filters", 77 | hitarea: "hitArea", 78 | interactive: "interactive", 79 | mask: "mask", 80 | name: "name", 81 | pivot: "pivot", 82 | position: "position", 83 | renderable: "renderable", 84 | rotation: "rotation", 85 | scale: "scale", 86 | skew: "skew", 87 | transform: "transform", 88 | visible: "visible", 89 | x: "x", 90 | y: "y", 91 | }; 92 | 93 | // http://pixijs.download/release/docs/PIXI.Container.html 94 | const containerStandardNames = { 95 | ...displayObjectStandardNames, 96 | height: "height", 97 | interactivechildren: "interactiveChildren", 98 | sortablechildren: "sortableChildren", 99 | sortdirty: "sortDirty", 100 | width: "width", 101 | }; 102 | 103 | // http://pixijs.download/release/docs/PIXI.Sprite.html 104 | const spriteStandardNames = { 105 | ...containerStandardNames, 106 | anchor: "anchor", 107 | blendmode: "blendMode", 108 | pluginname: "pluginName", 109 | roundpixels: "roundPixels", 110 | shader: "shader", 111 | texture: "texture", 112 | tint: "tint", 113 | }; 114 | 115 | // http://pixijs.download/release/docs/PIXI.extras.BitmapText.html 116 | const bitmapTextStandardNames = { 117 | ...containerStandardNames, 118 | align: "align", 119 | anchor: "anchor", 120 | font: "font", 121 | letterspacing: "letterSpacing", 122 | maxwidth: "maxWidth", 123 | roundpixels: "roundPixels", 124 | style: "style", 125 | text: "text", 126 | tint: "tint", 127 | }; 128 | 129 | // http://pixijs.download/release/docs/PIXI.Graphics.html 130 | const graphicsStandardNames = { 131 | ...containerStandardNames, 132 | blendmode: "blendMode", 133 | geometry: "geometry", 134 | pluginname: "pluginName", 135 | tint: "tint", 136 | }; 137 | 138 | // http://pixijs.download/release/docs/PIXI.particles.ParticleContainer.html 139 | const particleContainerStandardNames = { 140 | ...containerStandardNames, 141 | autoresize: "autoResize", 142 | batchsize: "batchSize", 143 | blendmode: "blendMode", 144 | interactivechildren: "interactiveChildren", 145 | maxsize: "maxSize", 146 | properties: "properties", 147 | roundpixels: "roundPixels", 148 | tint: "tint", 149 | }; 150 | 151 | // http://pixijs.download/release/docs/PIXI.Text.html 152 | const textStandardNames = { 153 | ...spriteStandardNames, 154 | canvas: "canvas", 155 | context: "context", 156 | resolution: "resolution", 157 | style: "style", 158 | text: "text", 159 | }; 160 | 161 | // http://pixijs.download/release/docs/PIXI.extras.TilingSprite.html 162 | const tilingSpriteStandardNames = { 163 | ...spriteStandardNames, 164 | clampmargin: "clampMargin", 165 | tileposition: "tilePosition", 166 | tilescale: "tileScale", 167 | tiletransform: "tileTransform", 168 | uvrespectanchor: "uvRespectAnchor", 169 | uvmatrix: "uvMatrix", 170 | }; 171 | 172 | const possibleStandardNames = { 173 | [TYPES.BITMAP_TEXT]: bitmapTextStandardNames, 174 | [TYPES.CONTAINER]: containerStandardNames, 175 | [TYPES.GRAPHICS]: graphicsStandardNames, 176 | [TYPES.PARTICLE_CONTAINER]: particleContainerStandardNames, 177 | [TYPES.SPRITE]: spriteStandardNames, 178 | [TYPES.TEXT]: textStandardNames, 179 | [TYPES.TILING_SPRITE]: tilingSpriteStandardNames, 180 | }; 181 | 182 | export default possibleStandardNames; 183 | -------------------------------------------------------------------------------- /src/propTypes.js: -------------------------------------------------------------------------------- 1 | import warning from "fbjs/lib/warning"; 2 | import * as PIXI from "pixi.js"; 3 | 4 | // Copied from https://reactjs.org/warnings/dont-call-proptypes.html#fixing-the-false-positive-in-third-party-proptypes 5 | const deprecatedWarned = {}; 6 | 7 | export function deprecated(propType, explanation) { 8 | return function validate(props, propName, componentName, ...rest) { 9 | if (props[propName] != null) { 10 | const message = `"${propName}" property of "${componentName}" has been deprecated.\n${explanation}`; 11 | if (!deprecatedWarned[message]) { 12 | warning(false, message); 13 | deprecatedWarned[message] = true; 14 | } 15 | } 16 | 17 | return propType(props, propName, componentName, ...rest); 18 | }; 19 | } 20 | 21 | export function validateApp(props, propName, componentName) { 22 | const app = props[propName]; 23 | if (typeof app === "undefined") { 24 | return; 25 | } 26 | 27 | const { options } = props; 28 | 29 | warning( 30 | Object.keys(options || {}).length === 0, 31 | `'options' property of '${componentName}' has no effect when 'app' property is provided. Only use 'app' or 'options', never both.` 32 | ); 33 | 34 | const isPixiApplication = app instanceof PIXI.Application; 35 | if (!isPixiApplication) { 36 | const propType = typeof app; 37 | return new Error( 38 | `Invalid prop '${propName}' of type '${propType}' supplied to '${componentName}', expected 'PIXI.Application'.` 39 | ); 40 | } 41 | } 42 | 43 | export function validateCanvas(props, propName, componentName) { 44 | // Let's assume that element is canvas if the element is Element and implements getContext 45 | const element = props[propName]; 46 | if (typeof element === "undefined") { 47 | return; 48 | } 49 | 50 | const isCanvas = element instanceof Element && typeof element.getContext === "function"; 51 | if (!isCanvas) { 52 | const propType = typeof element; 53 | return new Error( 54 | `Invalid prop '${propName}' of type '${propType}' supplied to '${componentName}', expected '<canvas> Element'.` 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/props.js: -------------------------------------------------------------------------------- 1 | import { TYPES } from "./types"; 2 | import * as PIXI from "pixi.js"; 3 | 4 | export const CHILDREN = "children"; 5 | 6 | // http://pixijs.download/release/docs/PIXI.DisplayObject.html 7 | const displayObjectDefaultProps = { 8 | alpha: 1, 9 | angle: 0, 10 | buttonMode: false, 11 | cacheAsBitmap: false, 12 | cursor: "auto", 13 | interactive: false, 14 | pivot: 0, 15 | position: 0, 16 | renderable: true, 17 | rotation: 0, 18 | scale: 1, 19 | skew: 0, 20 | visible: true, 21 | x: 0, 22 | y: 0, 23 | }; 24 | 25 | // http://pixijs.download/release/docs/PIXI.Container.html 26 | const containerDefaultProps = { 27 | ...displayObjectDefaultProps, 28 | interactiveChildren: true, 29 | }; 30 | 31 | // http://pixijs.download/release/docs/PIXI.Sprite.html 32 | const spriteDefaultProps = { 33 | ...containerDefaultProps, 34 | anchor: 0, 35 | blendMode: PIXI.BLEND_MODES.NORMAL, 36 | pluginName: "batch", 37 | roundPixels: false, 38 | tint: 0xffffff, 39 | }; 40 | 41 | // http://pixijs.download/release/docs/PIXI.extras.BitmapText.html 42 | const bitmapTextDefaultProps = { 43 | ...containerDefaultProps, 44 | align: "left", 45 | anchor: 0, 46 | letterSpacing: 0, 47 | maxWidth: 0, 48 | roundPixels: false, 49 | text: "", 50 | tint: 0xffffff, 51 | }; 52 | 53 | // http://pixijs.download/release/docs/PIXI.Graphics.html 54 | const graphicsDefaultProps = { 55 | ...containerDefaultProps, 56 | blendMode: PIXI.BLEND_MODES.NORMAL, 57 | pluginName: "batch", 58 | tint: 0xffffff, 59 | }; 60 | 61 | // http://pixijs.download/release/docs/PIXI.particles.ParticleContainer.html 62 | const particleContainerDefaultProps = { 63 | ...containerDefaultProps, 64 | autoResize: false, 65 | batchSize: 16384, 66 | blendMode: PIXI.BLEND_MODES.NORMAL, 67 | interactiveChildren: true, 68 | maxSize: 1500, 69 | roundPixels: true, 70 | tint: 0xffffff, 71 | }; 72 | 73 | // http://pixijs.download/release/docs/PIXI.Text.html 74 | const textDefaultProps = { 75 | ...spriteDefaultProps, 76 | resolution: 1, 77 | text: "", 78 | }; 79 | 80 | // http://pixijs.download/release/docs/PIXI.extras.TilingSprite.html 81 | const tilingSpriteDefaultProps = { 82 | ...spriteDefaultProps, 83 | clampMargin: 0.5, 84 | uvRespectAnchor: false, 85 | }; 86 | 87 | export const defaultProps = { 88 | [TYPES.BITMAP_TEXT]: bitmapTextDefaultProps, 89 | [TYPES.CONTAINER]: containerDefaultProps, 90 | [TYPES.GRAPHICS]: graphicsDefaultProps, 91 | [TYPES.PARTICLE_CONTAINER]: particleContainerDefaultProps, 92 | [TYPES.SPRITE]: spriteDefaultProps, 93 | [TYPES.TEXT]: textDefaultProps, 94 | [TYPES.TILING_SPRITE]: tilingSpriteDefaultProps, 95 | }; 96 | -------------------------------------------------------------------------------- /src/react-pixi-alias/index.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom"; 2 | import * as ReactPixiFiber from "react-pixi-fiber"; 3 | 4 | // react-pixi like API 5 | // Note: ReactPIXI.factories is not supported 6 | 7 | export const CustomPIXIComponent = ReactPixiFiber.CustomPIXIComponent; 8 | export const render = ReactDOM.render; 9 | export const unmountComponentAtNode = ReactDOM.unmountComponentAtNode; 10 | export const BitmapText = ReactPixiFiber.BitmapText; 11 | export const DisplayObjectContainer = ReactPixiFiber.Container; 12 | export const Graphics = ReactPixiFiber.Graphics; 13 | export const ParticleContainer = ReactPixiFiber.ParticleContainer; 14 | export const Sprite = ReactPixiFiber.Sprite; 15 | export const Stage = ReactPixiFiber.Stage; 16 | export const Text = ReactPixiFiber.Text; 17 | export const TilingSprite = ReactPixiFiber.TilingSprite; 18 | 19 | const ReactPIXI = { 20 | // Render methods 21 | CustomPIXIComponent, 22 | render, 23 | unmountComponentAtNode, 24 | // Components 25 | BitmapText, 26 | DisplayObjectContainer, 27 | Graphics, 28 | ParticleContainer, 29 | Sprite, 30 | Stage, 31 | Text, 32 | TilingSprite, 33 | }; 34 | 35 | export default ReactPIXI; 36 | -------------------------------------------------------------------------------- /src/render.js: -------------------------------------------------------------------------------- 1 | import invariant from "fbjs/lib/invariant"; 2 | import { version } from "react"; 3 | 4 | export function getDevToolsVersion() { 5 | return version; 6 | } 7 | 8 | export const roots = new Map(); 9 | 10 | /* 11 | * element should be any instance of PIXI DisplayObject 12 | * containerTag should be an instance of PIXI root Container (i.e. the Stage) 13 | */ 14 | export function createRender(ReactPixiFiber) { 15 | return function render(element, containerTag, callback, parentComponent) { 16 | let root = roots.get(containerTag); 17 | if (!root) { 18 | root = ReactPixiFiber.createContainer(containerTag); 19 | roots.set(containerTag, root); 20 | 21 | ReactPixiFiber.injectIntoDevTools({ 22 | findFiberByHostInstance: ReactPixiFiber.findFiberByHostInstance, 23 | bundleType: __DEV__ ? 1 : 0, 24 | version: getDevToolsVersion(), 25 | rendererPackageName: __PACKAGE_NAME__, 26 | }); 27 | } 28 | 29 | ReactPixiFiber.updateContainer(element, root, parentComponent, callback); 30 | 31 | return ReactPixiFiber.getPublicRootInstance(root); 32 | }; 33 | } 34 | 35 | export function createUnmount(ReactPixiFiber) { 36 | return function unmount(containerTag) { 37 | const root = roots.get(containerTag); 38 | 39 | invariant(root, "ReactPixiFiber did not render into container provided"); 40 | 41 | ReactPixiFiber.updateContainer(null, root); 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | // List of types supported by ReactPixiFiber 2 | export const TYPES = { 3 | BITMAP_TEXT: "BitmapText", 4 | CONTAINER: "Container", 5 | GRAPHICS: "Graphics", 6 | NINE_SLICE_PLANE: "NineSlicePlane", 7 | PARTICLE_CONTAINER: "ParticleContainer", 8 | SPRITE: "Sprite", 9 | TEXT: "Text", 10 | TILING_SPRITE: "TilingSprite", 11 | }; 12 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import invariant from "fbjs/lib/invariant"; 2 | import * as PIXI from "pixi.js"; 3 | import { getStackAddendum } from "./ReactGlobalSharedState"; 4 | 5 | /* Helper Methods */ 6 | 7 | export const not = fn => (...args) => !fn(...args); 8 | 9 | export const including = props => key => props.indexOf(key) !== -1; 10 | 11 | export const unique = (element, index, array) => array.indexOf(element) === index; 12 | 13 | export function filterByKey(inputObject, filter) { 14 | const exportObject = {}; 15 | 16 | Object.keys(inputObject) 17 | .filter(filter) 18 | .forEach(key => { 19 | exportObject[key] = inputObject[key]; 20 | }); 21 | 22 | return exportObject; 23 | } 24 | 25 | /* react-reconciler related Methods */ 26 | 27 | // See https://github.com/facebook/react/blob/702fad4b1b48ac8f626ed3f35e8f86f5ea728084/packages/react-reconciler/src/ReactTypeOfMode.js#L13 28 | const StrictMode = 1; 29 | 30 | // Would be better if this was just exported from react-reconciler 31 | // Additional try/catch added in case the internal API changes 32 | // See: https://github.com/facebook/react/blob/702fad4b1b48ac8f626ed3f35e8f86f5ea728084/packages/react-reconciler/src/ReactStrictModeWarnings.new.js#L31 33 | export function findStrictRoot(fiber) { 34 | try { 35 | let maybeStrictRoot = null; 36 | 37 | let node = fiber; 38 | while (node !== null) { 39 | if (node.mode & StrictMode) { 40 | maybeStrictRoot = node; 41 | } 42 | node = node.return; 43 | } 44 | 45 | return maybeStrictRoot; 46 | } catch (e) { 47 | return null; 48 | } 49 | } 50 | 51 | /* PIXI related Methods */ 52 | 53 | export function createPixiApplication(options) { 54 | return new PIXI.Application(options); 55 | } 56 | 57 | // Converts value to an array of coordinates 58 | export function parsePoint(value) { 59 | let arr = []; 60 | if (value == null) { 61 | return arr; 62 | } else if (typeof value === "string") { 63 | arr = value.split(","); 64 | } else if (typeof value === "number") { 65 | arr = [value]; 66 | } else if (Array.isArray(value)) { 67 | // shallow copy the array 68 | arr = value.slice(); 69 | } else if (typeof value.x !== "undefined" && typeof value.y !== "undefined") { 70 | arr = [value.x, value.y]; 71 | } 72 | 73 | return arr.map(Number); 74 | } 75 | 76 | export function isPointType(value) { 77 | return value instanceof PIXI.Point || value instanceof PIXI.ObservablePoint; 78 | } 79 | 80 | // Use Point.copyFrom if available because Point.copy was deprecated in PIXI 5.0 81 | export function copyPoint(instance, propName, value) { 82 | if (typeof instance[propName].copyFrom === "function") { 83 | instance[propName].copyFrom(value); 84 | } else { 85 | instance[propName].copy(value); 86 | } 87 | } 88 | 89 | // Set props on a DisplayObject by checking the type. If a PIXI.Point or 90 | // a PIXI.ObservablePoint is having its value set, then either a comma-separated 91 | // string with in the form of "x,y" or a size 2 array with index 0 being the x 92 | // coordinate and index 1 being the y coordinate. 93 | // See: https://github.com/Izzimach/react-pixi/blob/a25196251a13ed9bb116a8576d93e9fceac2a14c/src/ReactPIXI.js#L114 94 | export function setPixiValue(instance, propName, value) { 95 | if (isPointType(instance[propName]) && isPointType(value)) { 96 | // Just copy the data if a Point type is being assigned to a Point type 97 | copyPoint(instance, propName, value); 98 | } else if (isPointType(instance[propName])) { 99 | // Parse value if a non-Point type is being assigned to a Point type 100 | const coordinateData = parsePoint(value); 101 | 102 | invariant( 103 | typeof coordinateData !== "undefined" && coordinateData.length > 0 && coordinateData.length < 3, 104 | "The property `%s` is a PIXI.Point or PIXI.ObservablePoint and must be set to a comma-separated string of " + 105 | "either 1 or 2 coordinates, a 1 or 2 element array containing coordinates, or a PIXI Point/ObservablePoint. " + 106 | "If only one coordinate is given then X and Y will be set to the provided value.%s", 107 | propName, 108 | getStackAddendum() 109 | ); 110 | 111 | instance[propName].set(coordinateData.shift(), coordinateData.shift()); 112 | } else { 113 | // Just assign the value directly if a non-Point type is being assigned to a non-Point type 114 | instance[propName] = value; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /test/AppProvider.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import renderer from "react-test-renderer"; 4 | import { AppContext, AppProvider, Container, withApp } from "../src"; 5 | import { isNewContextAvailable } from "../src/compat"; 6 | import { createStageClass, createStageFunction } from "../src/Stage"; 7 | import { createRender } from "../src/render"; 8 | import { ReactPixiFiberAsPrimaryRenderer } from "../src/ReactPixiFiber"; 9 | import { __RewireAPI__ as HooksRewireAPI } from "../src/Stage/hooks"; 10 | import { __RewireAPI__ as StageRewireAPI } from "../src/Stage/legacy"; 11 | import * as PIXI from "pixi.js"; 12 | 13 | const render = createRender(ReactPixiFiberAsPrimaryRenderer); 14 | 15 | if (isNewContextAvailable()) { 16 | // New Context API 17 | describe("AppProvider using New Context API (React >=16.3.0)", () => { 18 | it("exports AppContext with Provider and Consumer", () => { 19 | expect(AppContext.Provider).not.toEqual(null); 20 | expect(AppContext.Consumer).not.toEqual(null); 21 | }); 22 | 23 | it("passes app prop to wrapped component", () => { 24 | const app = new PIXI.Application(); 25 | const TestComponent = jest.fn(() => null); 26 | 27 | renderer.act(() => { 28 | render( 29 | <AppContext.Provider value={app}> 30 | <AppContext.Consumer>{app => <TestComponent app={app} foo="bar" />}</AppContext.Consumer> 31 | </AppContext.Provider>, 32 | app.stage 33 | ); 34 | }); 35 | 36 | expect(TestComponent).toHaveBeenCalledWith({ app, foo: "bar" }, {}); 37 | }); 38 | }); 39 | } else { 40 | // Legacy Context API 41 | describe("AppProvider using Legacy Context API (React <16.3.0)", () => { 42 | let app; 43 | const createPixiApplication = jest.fn(options => { 44 | app = new PIXI.Application(options); 45 | return app; 46 | }); 47 | 48 | beforeEach(() => { 49 | HooksRewireAPI.__Rewire__("createPixiApplication", createPixiApplication); 50 | StageRewireAPI.__Rewire__("createPixiApplication", createPixiApplication); 51 | createPixiApplication.mockClear(); 52 | }); 53 | 54 | afterEach(() => { 55 | HooksRewireAPI.__ResetDependency__("createPixiApplication"); 56 | StageRewireAPI.__ResetDependency__("createPixiApplication"); 57 | }); 58 | 59 | it("exports null AppContext", () => { 60 | expect(AppContext).toEqual(null); 61 | }); 62 | 63 | it("passes app context to component rendered inside AppProvider", () => { 64 | const app = new PIXI.Application(); 65 | const TestComponent = jest.fn(() => null); 66 | TestComponent.contextTypes = { 67 | app: PropTypes.object, 68 | }; 69 | 70 | render( 71 | <AppProvider app={app}> 72 | <Container> 73 | <TestComponent foo="bar" /> 74 | </Container> 75 | </AppProvider>, 76 | app.stage 77 | ); 78 | 79 | expect(TestComponent).toHaveBeenCalledWith({ foo: "bar" }, { app }); 80 | }); 81 | 82 | it("passes app context to component rendered inside Stage (class)", () => { 83 | const Stage = createStageClass(); 84 | const TestComponent = jest.fn(() => null); 85 | TestComponent.contextTypes = { 86 | app: PropTypes.object, 87 | }; 88 | 89 | renderer.create( 90 | <Stage> 91 | <Container> 92 | <TestComponent foo="bar" /> 93 | </Container> 94 | </Stage> 95 | ); 96 | 97 | expect(TestComponent).toHaveBeenCalledWith({ foo: "bar" }, { app }); 98 | }); 99 | 100 | it("passes app context to component rendered inside Stage (function)", () => { 101 | const Stage = createStageFunction(); 102 | const TestComponent = jest.fn(() => null); 103 | TestComponent.contextTypes = { 104 | app: PropTypes.object, 105 | }; 106 | 107 | renderer.create( 108 | <Stage> 109 | <Container> 110 | <TestComponent foo="bar" /> 111 | </Container> 112 | </Stage> 113 | ); 114 | 115 | expect(TestComponent).toHaveBeenCalledWith({ foo: "bar" }, { app }); 116 | }); 117 | }); 118 | } 119 | 120 | describe("withApp", () => { 121 | let app; 122 | const createPixiApplication = jest.fn(options => { 123 | app = new PIXI.Application(options); 124 | return app; 125 | }); 126 | 127 | beforeEach(() => { 128 | HooksRewireAPI.__Rewire__("createPixiApplication", createPixiApplication); 129 | StageRewireAPI.__Rewire__("createPixiApplication", createPixiApplication); 130 | createPixiApplication.mockClear(); 131 | }); 132 | 133 | afterEach(() => { 134 | HooksRewireAPI.__ResetDependency__("createPixiApplication"); 135 | StageRewireAPI.__ResetDependency__("createPixiApplication"); 136 | }); 137 | 138 | it("passes app prop to component rendered inside AppProvider", () => { 139 | const app = new PIXI.Application(); 140 | const TestComponent = jest.fn(() => null); 141 | const TestComponentWithApp = withApp(TestComponent); 142 | 143 | renderer.act(() => { 144 | render( 145 | <AppProvider app={app}> 146 | <Container> 147 | <TestComponentWithApp foo="bar" /> 148 | </Container> 149 | </AppProvider>, 150 | app.stage 151 | ); 152 | }); 153 | 154 | expect(TestComponent).toHaveBeenCalledWith({ app, foo: "bar" }, {}); 155 | }); 156 | 157 | it("passes app prop to component rendered inside Stage (class)", () => { 158 | const Stage = createStageClass(); 159 | const TestComponent = jest.fn(() => null); 160 | const TestComponentWithApp = withApp(TestComponent); 161 | 162 | renderer.act(() => { 163 | renderer.create( 164 | <Stage> 165 | <TestComponentWithApp foo="bar" /> 166 | </Stage> 167 | ); 168 | }); 169 | 170 | expect(TestComponent).toHaveBeenCalledWith({ app, foo: "bar" }, {}); 171 | }); 172 | 173 | it("passes app prop to component rendered inside Stage (function)", () => { 174 | const Stage = createStageFunction(); 175 | const TestComponent = jest.fn(() => null); 176 | const TestComponentWithApp = withApp(TestComponent); 177 | 178 | renderer.act(() => { 179 | renderer.create( 180 | <Stage> 181 | <TestComponentWithApp foo="bar" /> 182 | </Stage> 183 | ); 184 | }); 185 | 186 | expect(TestComponent).toHaveBeenCalledWith({ app, foo: "bar" }, {}); 187 | }); 188 | }); 189 | -------------------------------------------------------------------------------- /test/CustomPIXIComponent.test.js: -------------------------------------------------------------------------------- 1 | import CustomPIXIComponent from "../src/CustomPIXIComponent"; 2 | import { injectType } from "../src/inject"; 3 | 4 | jest.mock("../src/inject"); 5 | 6 | describe("CustomPIXIComponent", () => { 7 | it("calls injectType", () => { 8 | const type = "INJECTED_TYPE"; 9 | const customDisplayObject = jest.fn(); 10 | CustomPIXIComponent(customDisplayObject, type); 11 | expect(injectType).toHaveBeenCalledTimes(1); 12 | expect(injectType).toHaveBeenCalledWith(type, customDisplayObject); 13 | }); 14 | it("throws if type is not provided", () => { 15 | expect(() => CustomPIXIComponent(jest.fn())).toThrow(); 16 | }); 17 | it("throws if type is not a string", () => { 18 | const type = jest.fn(); 19 | expect(() => CustomPIXIComponent(jest.fn(), type)).toThrow(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/PixiPropertyOperations.test.js: -------------------------------------------------------------------------------- 1 | import * as PixiPropertyOperations from "../src/PixiPropertyOperations"; 2 | import { __RewireAPI__ as PixiPropertyOperationsRewireAPI } from "../src/PixiPropertyOperations"; 3 | 4 | describe("PixiPropertyOperations", () => { 5 | describe("setValueForProperty", () => { 6 | const instance = {}; 7 | const setPixiValue = jest.fn(); 8 | const shouldIgnoreAttribute = jest.fn(() => false); 9 | const shouldRemoveAttribute = jest.fn(() => false); 10 | 11 | beforeAll(() => { 12 | PixiPropertyOperationsRewireAPI.__Rewire__("setPixiValue", setPixiValue); 13 | PixiPropertyOperationsRewireAPI.__Rewire__("shouldIgnoreAttribute", shouldIgnoreAttribute); 14 | PixiPropertyOperationsRewireAPI.__Rewire__("shouldRemoveAttribute", shouldRemoveAttribute); 15 | }); 16 | 17 | afterAll(() => { 18 | PixiPropertyOperationsRewireAPI.__ResetDependency__("setPixiValue"); 19 | PixiPropertyOperationsRewireAPI.__ResetDependency__("shouldIgnoreAttribute"); 20 | PixiPropertyOperationsRewireAPI.__ResetDependency__("shouldRemoveAttribute"); 21 | }); 22 | 23 | afterEach(() => { 24 | setPixiValue.mockReset(); 25 | shouldIgnoreAttribute.mockReset(); 26 | shouldRemoveAttribute.mockReset(); 27 | }); 28 | 29 | it("should not call setPixiValue if property should be ignored", () => { 30 | shouldIgnoreAttribute.mockImplementation(() => true); 31 | PixiPropertyOperations.setValueForProperty("Sprite", instance, "ignoredProp", "unsetValue"); 32 | expect(setPixiValue).toHaveBeenCalledTimes(0); 33 | }); 34 | 35 | it("should call setPixiValue with default value if property should be removed and default is available", () => { 36 | const type = "Sprite"; 37 | const propName = "roundPixels"; 38 | shouldRemoveAttribute.mockImplementation(() => true); 39 | PixiPropertyOperations.setValueForProperty(type, instance, propName, undefined); 40 | expect(setPixiValue).toHaveBeenCalledTimes(1); 41 | expect(setPixiValue).toHaveBeenCalledWith(instance, propName, false); 42 | }); 43 | 44 | it("should not call setPixiValue if property should be removed and default is not available", () => { 45 | const type = "Sprite"; 46 | const propName = "unknownProp"; 47 | shouldRemoveAttribute.mockImplementation(() => true); 48 | PixiPropertyOperations.setValueForProperty(type, instance, propName, undefined); 49 | expect(setPixiValue).toHaveBeenCalledTimes(0); 50 | }); 51 | 52 | it("should not call setPixiValue if property should be removed and defaults are not available", () => { 53 | const type = "UnknownType"; 54 | const propName = "unknownProp"; 55 | shouldRemoveAttribute.mockImplementation(() => true); 56 | PixiPropertyOperations.setValueForProperty(type, instance, propName, undefined); 57 | expect(setPixiValue).toHaveBeenCalledTimes(0); 58 | }); 59 | 60 | it("should call setPixiValue with provided value if property should not be removed", () => { 61 | const type = "Sprite"; 62 | const propName = "roundPixels"; 63 | PixiPropertyOperations.setValueForProperty(type, instance, propName, true); 64 | expect(setPixiValue).toHaveBeenCalledTimes(1); 65 | expect(setPixiValue).toHaveBeenCalledWith(instance, propName, true); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/ReactPixiFiberUnknownPropertyHook.test.js: -------------------------------------------------------------------------------- 1 | import emptyFunction from "fbjs/lib/emptyFunction"; 2 | import * as ReactPixiFiberUnknownPropertyHook from "../src/ReactPixiFiberUnknownPropertyHook"; 3 | import { __RewireAPI__ as ReactPixiFiberUnknownPropertyHookRewireAPI } from "../src/ReactPixiFiberUnknownPropertyHook"; 4 | 5 | describe("ReactPixiFiberUnknownPropertyHook", () => { 6 | describe("validateProperty", () => { 7 | const type = "type"; 8 | const stack = "stack"; 9 | const getPropertyInfo = jest.fn(); 10 | const validateProperty = jest.fn(); 11 | const shouldRemoveAttributeWithWarning = jest.fn(); 12 | const warning = jest.fn(); 13 | 14 | beforeAll(() => { 15 | ReactPixiFiberUnknownPropertyHookRewireAPI.__Rewire__("getPropertyInfo", () => getPropertyInfo); 16 | ReactPixiFiberUnknownPropertyHookRewireAPI.__Rewire__("getStackAddendum", () => stack); 17 | ReactPixiFiberUnknownPropertyHookRewireAPI.__Rewire__( 18 | "shouldRemoveAttributeWithWarning", 19 | shouldRemoveAttributeWithWarning 20 | ); 21 | ReactPixiFiberUnknownPropertyHookRewireAPI.__Rewire__("warning", warning); 22 | }); 23 | 24 | afterAll(() => { 25 | ReactPixiFiberUnknownPropertyHookRewireAPI.__ResetDependency__("getPropertyInfo"); 26 | ReactPixiFiberUnknownPropertyHookRewireAPI.__ResetDependency__("getStackAddendum"); 27 | ReactPixiFiberUnknownPropertyHookRewireAPI.__ResetDependency__("shouldRemoveAttributeWithWarning"); 28 | ReactPixiFiberUnknownPropertyHookRewireAPI.__ResetDependency__("warning"); 29 | }); 30 | 31 | afterEach(() => { 32 | validateProperty.mockReset(); 33 | warning.mockReset(); 34 | }); 35 | 36 | it("should be defined in development", () => { 37 | if (__DEV__) { 38 | expect(ReactPixiFiberUnknownPropertyHook.validateProperty).not.toEqual(emptyFunction); 39 | } else { 40 | expect(ReactPixiFiberUnknownPropertyHook.validateProperty).toEqual(emptyFunction); 41 | } 42 | }); 43 | 44 | it("should warn about properties starting with `on` in development", () => { 45 | const name = "onTest"; 46 | ReactPixiFiberUnknownPropertyHook.validateProperty(type, name, () => {}); 47 | 48 | if (__DEV__) { 49 | expect(warning).toHaveBeenCalledTimes(1); 50 | expect(warning).toHaveBeenCalledWith( 51 | false, 52 | "Invalid event handler prop `%s` on `<%s />`. PIXI events use other naming convention, for example `click`.%s", 53 | name, 54 | type, 55 | stack 56 | ); 57 | } else { 58 | expect(warning).toHaveBeenCalledTimes(0); 59 | } 60 | }); 61 | 62 | it("should warn about NaNs in development", () => { 63 | const name = "someValue"; 64 | ReactPixiFiberUnknownPropertyHook.validateProperty(type, name, NaN); 65 | 66 | if (__DEV__) { 67 | expect(warning).toHaveBeenCalledTimes(1); 68 | expect(warning).toHaveBeenCalledWith( 69 | false, 70 | "Received NaN for prop `%s` on `<%s />`. If this is expected, cast the value to a string.%s", 71 | name, 72 | type, 73 | stack 74 | ); 75 | } else { 76 | expect(warning).toHaveBeenCalledTimes(0); 77 | } 78 | }); 79 | 80 | it.skip("should warn about invalid prop casing", () => {}); 81 | 82 | it.skip("should warn about unknown properties if they are not reserved", () => {}); 83 | 84 | it.skip("should assume that values for reserved properties are valid", () => {}); 85 | 86 | it.skip("should not warn again if shouldRemoveAttributeWithWarning returns true", () => {}); 87 | 88 | it.skip("should assume property value is valid otherwise", () => {}); 89 | }); 90 | 91 | describe("validateProperties", () => { 92 | const type = "type"; 93 | const props = { position: "0,0" }; 94 | const isInjectedType = jest.fn(); 95 | const warnUnknownProperties = jest.fn(); 96 | 97 | beforeAll(() => { 98 | ReactPixiFiberUnknownPropertyHookRewireAPI.__Rewire__("isInjectedType", isInjectedType); 99 | ReactPixiFiberUnknownPropertyHookRewireAPI.__Rewire__("warnUnknownProperties", warnUnknownProperties); 100 | }); 101 | 102 | afterAll(() => { 103 | ReactPixiFiberUnknownPropertyHookRewireAPI.__ResetDependency__("isInjectedType"); 104 | ReactPixiFiberUnknownPropertyHookRewireAPI.__ResetDependency__("warnUnknownProperties"); 105 | }); 106 | 107 | afterEach(() => { 108 | isInjectedType.mockReset(); 109 | warnUnknownProperties.mockReset(); 110 | }); 111 | 112 | it("should not call warnUnknownProperties for injected types", () => { 113 | const strictRoot = null; 114 | isInjectedType.mockImplementation(() => true); 115 | ReactPixiFiberUnknownPropertyHook.validateProperties(type, props, strictRoot); 116 | 117 | expect(warnUnknownProperties).toHaveBeenCalledTimes(0); 118 | }); 119 | }); 120 | 121 | describe("warnUnknownProperties", () => { 122 | const type = "type"; 123 | const props = { position: "0,0", scale: 2 }; 124 | const stack = "stack"; 125 | const validateProperty = jest.fn(); 126 | const warning = jest.fn(); 127 | 128 | beforeAll(() => { 129 | ReactPixiFiberUnknownPropertyHookRewireAPI.__Rewire__("getStackAddendum", () => stack); 130 | ReactPixiFiberUnknownPropertyHookRewireAPI.__Rewire__("validateProperty", validateProperty); 131 | ReactPixiFiberUnknownPropertyHookRewireAPI.__Rewire__("warning", warning); 132 | }); 133 | 134 | afterAll(() => { 135 | ReactPixiFiberUnknownPropertyHookRewireAPI.__ResetDependency__("getStackAddendum"); 136 | ReactPixiFiberUnknownPropertyHookRewireAPI.__ResetDependency__("validateProperty"); 137 | ReactPixiFiberUnknownPropertyHookRewireAPI.__ResetDependency__("warning"); 138 | }); 139 | 140 | afterEach(() => { 141 | validateProperty.mockReset(); 142 | warning.mockReset(); 143 | }); 144 | 145 | it("should not warn is props are valid", () => { 146 | validateProperty.mockImplementation(() => true); 147 | ReactPixiFiberUnknownPropertyHook.warnUnknownProperties(type, props); 148 | 149 | expect(warning).toHaveBeenCalledTimes(0); 150 | }); 151 | 152 | it("should warn if one prop is not valid", () => { 153 | validateProperty.mockImplementationOnce(() => true).mockImplementationOnce(() => false); 154 | ReactPixiFiberUnknownPropertyHook.warnUnknownProperties(type, props); 155 | 156 | expect(warning).toHaveBeenCalledTimes(1); 157 | expect(warning).toHaveBeenCalledWith(false, "Invalid value for prop %s on `<%s />`.%s", "`scale`", type, stack); 158 | }); 159 | 160 | it("should warn if more than one prop is not valid", () => { 161 | validateProperty.mockImplementation(() => false); 162 | ReactPixiFiberUnknownPropertyHook.warnUnknownProperties(type, props); 163 | 164 | expect(warning).toHaveBeenCalledTimes(1); 165 | expect(warning).toHaveBeenCalledWith( 166 | false, 167 | "Invalid values for props %s on `<%s />`.%s", 168 | "`position`, `scale`", 169 | type, 170 | stack 171 | ); 172 | }); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /test/Stage/index.test.js: -------------------------------------------------------------------------------- 1 | import { createStageFunction, createStageClass } from "../../src/Stage"; 2 | 3 | describe("Stage", () => { 4 | it("should export Stage class creator", () => { 5 | expect(typeof createStageClass).toEqual("function"); 6 | }); 7 | 8 | it("should export Stage function creator", () => { 9 | expect(typeof createStageFunction).toEqual("function"); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/Stage/propTypes.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createStageFunction, createStageClass } from "../../src/Stage"; 3 | import { 4 | getCanvasProps, 5 | getContainerProps, 6 | includingCanvasProps, 7 | includingContainerProps, 8 | includingStageProps, 9 | } from "../../src/Stage/propTypes"; 10 | import possibleStandardNames from "../../src/possibleStandardNames"; 11 | import { TYPES } from "../../src/types"; 12 | 13 | describe("includingContainerProps", () => { 14 | it("returns true if prop is one of Container members", () => { 15 | Object.keys(possibleStandardNames[TYPES.CONTAINER]).forEach(propName => { 16 | expect(includingContainerProps(propName)).toBeTruthy(); 17 | }); 18 | }); 19 | 20 | it("returns false if prop is not one of Container members", () => { 21 | expect(includingContainerProps("className")).toBeFalsy(); 22 | expect(includingContainerProps("style")).toBeFalsy(); 23 | expect(includingContainerProps("options")).toBeFalsy(); 24 | }); 25 | }); 26 | 27 | describe("includingStageProps", () => { 28 | const StageClass = createStageClass(); 29 | const StageFunction = createStageFunction(); 30 | 31 | it("returns true if prop is one of Stage props", () => { 32 | Object.keys(StageFunction.propTypes).forEach(propName => { 33 | expect(includingStageProps(propName)).toBeTruthy(); 34 | }); 35 | 36 | Object.keys(StageClass.propTypes).forEach(propName => { 37 | expect(includingStageProps(propName)).toBeTruthy(); 38 | }); 39 | }); 40 | 41 | it("returns false if prop is not one of Stage props", () => { 42 | expect(includingStageProps("className")).toBeFalsy(); 43 | expect(includingStageProps("position")).toBeFalsy(); 44 | expect(includingStageProps("style")).toBeFalsy(); 45 | }); 46 | }); 47 | 48 | describe("includingCanvasProps", () => { 49 | const StageClass = createStageClass(); 50 | const StageFunction = createStageFunction(); 51 | 52 | it("returns true if prop is not one of Container members", () => { 53 | expect(includingCanvasProps("className")).toBeTruthy(); 54 | expect(includingCanvasProps("id")).toBeTruthy(); 55 | expect(includingCanvasProps("style")).toBeTruthy(); 56 | }); 57 | 58 | it("returns false if prop is one of Container members or Stage props", () => { 59 | Object.keys(possibleStandardNames[TYPES.CONTAINER]) 60 | .concat(Object.keys(StageFunction.propTypes)) 61 | .forEach(propName => { 62 | expect(includingCanvasProps(propName)).toBeFalsy(); 63 | }); 64 | 65 | Object.keys(possibleStandardNames[TYPES.CONTAINER]) 66 | .concat(Object.keys(StageClass.propTypes)) 67 | .forEach(propName => { 68 | expect(includingCanvasProps(propName)).toBeFalsy(); 69 | }); 70 | }); 71 | }); 72 | 73 | describe("getCanvasProps", () => { 74 | it("extracts <canvas /> related props from all props", () => { 75 | const allProps = { 76 | className: "canvas--responsive", 77 | options: { 78 | height: 200, 79 | width: 200, 80 | }, 81 | position: "100,0", 82 | scale: 2, 83 | style: { 84 | position: "relative", 85 | }, 86 | }; 87 | const canvasProps = { 88 | className: allProps.className, 89 | style: allProps.style, 90 | }; 91 | expect(getCanvasProps(allProps)).toEqual(canvasProps); 92 | }); 93 | }); 94 | 95 | describe("getContainerProps", () => { 96 | it("extracts Container related props from all props", () => { 97 | const allProps = { 98 | className: "canvas--responsive", 99 | options: { 100 | height: 200, 101 | width: 200, 102 | }, 103 | position: "100,0", 104 | scale: 2, 105 | style: { 106 | position: "relative", 107 | }, 108 | }; 109 | const containerProps = { 110 | position: allProps.position, 111 | scale: allProps.scale, 112 | }; 113 | expect(getContainerProps(allProps)).toEqual(containerProps); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /test/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ReactPixiFiber public API should match snapshot 1`] = ` 4 | Object { 5 | "AppContext": Object { 6 | "$$typeof": Symbol(react.context), 7 | "Consumer": Object { 8 | "$$typeof": Symbol(react.context), 9 | "_context": [Circular], 10 | }, 11 | "Provider": Object { 12 | "$$typeof": Symbol(react.provider), 13 | "_context": [Circular], 14 | }, 15 | "_currentRenderer": null, 16 | "_currentRenderer2": null, 17 | "_currentValue": null, 18 | "_currentValue2": null, 19 | "_defaultValue": null, 20 | "_globalName": null, 21 | "_threadCount": 0, 22 | }, 23 | "AppProvider": [Function], 24 | "BitmapText": "BitmapText", 25 | "Container": "Container", 26 | "CustomPIXIComponent": [Function], 27 | "CustomPIXIProperty": [Function], 28 | "Graphics": "Graphics", 29 | "NineSlicePlane": "NineSlicePlane", 30 | "ParticleContainer": "ParticleContainer", 31 | "Sprite": "Sprite", 32 | "Stage": Object { 33 | "$$typeof": Symbol(react.forward_ref), 34 | "defaultProps": Object { 35 | "options": Object {}, 36 | }, 37 | "propTypes": Object { 38 | "app": [Function], 39 | "children": [Function], 40 | "height": [Function], 41 | "options": [Function], 42 | "width": [Function], 43 | }, 44 | "render": [Function], 45 | }, 46 | "Text": "Text", 47 | "TilingSprite": "TilingSprite", 48 | "applyDisplayObjectProps": [Function], 49 | "createStageClass": [Function], 50 | "default": Object {}, 51 | "render": [Function], 52 | "unmount": [Function], 53 | "usePixiApp": [Function], 54 | "usePixiTicker": [Function], 55 | "withApp": [Function], 56 | } 57 | `; 58 | -------------------------------------------------------------------------------- /test/__snapshots__/props.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`props defaultProps should match 1`] = ` 4 | Object { 5 | "BitmapText": Object { 6 | "align": "left", 7 | "alpha": 1, 8 | "anchor": 0, 9 | "angle": 0, 10 | "buttonMode": false, 11 | "cacheAsBitmap": false, 12 | "cursor": "auto", 13 | "interactive": false, 14 | "interactiveChildren": true, 15 | "letterSpacing": 0, 16 | "maxWidth": 0, 17 | "pivot": 0, 18 | "position": 0, 19 | "renderable": true, 20 | "rotation": 0, 21 | "roundPixels": false, 22 | "scale": 1, 23 | "skew": 0, 24 | "text": "", 25 | "tint": 16777215, 26 | "visible": true, 27 | "x": 0, 28 | "y": 0, 29 | }, 30 | "Container": Object { 31 | "alpha": 1, 32 | "angle": 0, 33 | "buttonMode": false, 34 | "cacheAsBitmap": false, 35 | "cursor": "auto", 36 | "interactive": false, 37 | "interactiveChildren": true, 38 | "pivot": 0, 39 | "position": 0, 40 | "renderable": true, 41 | "rotation": 0, 42 | "scale": 1, 43 | "skew": 0, 44 | "visible": true, 45 | "x": 0, 46 | "y": 0, 47 | }, 48 | "Graphics": Object { 49 | "alpha": 1, 50 | "angle": 0, 51 | "blendMode": 0, 52 | "buttonMode": false, 53 | "cacheAsBitmap": false, 54 | "cursor": "auto", 55 | "interactive": false, 56 | "interactiveChildren": true, 57 | "pivot": 0, 58 | "pluginName": "batch", 59 | "position": 0, 60 | "renderable": true, 61 | "rotation": 0, 62 | "scale": 1, 63 | "skew": 0, 64 | "tint": 16777215, 65 | "visible": true, 66 | "x": 0, 67 | "y": 0, 68 | }, 69 | "ParticleContainer": Object { 70 | "alpha": 1, 71 | "angle": 0, 72 | "autoResize": false, 73 | "batchSize": 16384, 74 | "blendMode": 0, 75 | "buttonMode": false, 76 | "cacheAsBitmap": false, 77 | "cursor": "auto", 78 | "interactive": false, 79 | "interactiveChildren": true, 80 | "maxSize": 1500, 81 | "pivot": 0, 82 | "position": 0, 83 | "renderable": true, 84 | "rotation": 0, 85 | "roundPixels": true, 86 | "scale": 1, 87 | "skew": 0, 88 | "tint": 16777215, 89 | "visible": true, 90 | "x": 0, 91 | "y": 0, 92 | }, 93 | "Sprite": Object { 94 | "alpha": 1, 95 | "anchor": 0, 96 | "angle": 0, 97 | "blendMode": 0, 98 | "buttonMode": false, 99 | "cacheAsBitmap": false, 100 | "cursor": "auto", 101 | "interactive": false, 102 | "interactiveChildren": true, 103 | "pivot": 0, 104 | "pluginName": "batch", 105 | "position": 0, 106 | "renderable": true, 107 | "rotation": 0, 108 | "roundPixels": false, 109 | "scale": 1, 110 | "skew": 0, 111 | "tint": 16777215, 112 | "visible": true, 113 | "x": 0, 114 | "y": 0, 115 | }, 116 | "Text": Object { 117 | "alpha": 1, 118 | "anchor": 0, 119 | "angle": 0, 120 | "blendMode": 0, 121 | "buttonMode": false, 122 | "cacheAsBitmap": false, 123 | "cursor": "auto", 124 | "interactive": false, 125 | "interactiveChildren": true, 126 | "pivot": 0, 127 | "pluginName": "batch", 128 | "position": 0, 129 | "renderable": true, 130 | "resolution": 1, 131 | "rotation": 0, 132 | "roundPixels": false, 133 | "scale": 1, 134 | "skew": 0, 135 | "text": "", 136 | "tint": 16777215, 137 | "visible": true, 138 | "x": 0, 139 | "y": 0, 140 | }, 141 | "TilingSprite": Object { 142 | "alpha": 1, 143 | "anchor": 0, 144 | "angle": 0, 145 | "blendMode": 0, 146 | "buttonMode": false, 147 | "cacheAsBitmap": false, 148 | "clampMargin": 0.5, 149 | "cursor": "auto", 150 | "interactive": false, 151 | "interactiveChildren": true, 152 | "pivot": 0, 153 | "pluginName": "batch", 154 | "position": 0, 155 | "renderable": true, 156 | "rotation": 0, 157 | "roundPixels": false, 158 | "scale": 1, 159 | "skew": 0, 160 | "tint": 16777215, 161 | "uvRespectAnchor": false, 162 | "visible": true, 163 | "x": 0, 164 | "y": 0, 165 | }, 166 | } 167 | `; 168 | -------------------------------------------------------------------------------- /test/hooks.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import renderer from "react-test-renderer"; 3 | import * as PIXI from "pixi.js"; 4 | import { Container } from "../src/index"; 5 | import { usePixiApp, usePixiTicker } from "../src/hooks"; 6 | import { AppContext } from "../src/AppProvider"; 7 | import { createRender } from "../src/render"; 8 | import { ReactPixiFiberAsSecondaryRenderer } from "../src/ReactPixiFiber"; 9 | 10 | const render = createRender(ReactPixiFiberAsSecondaryRenderer); 11 | 12 | describe("usePixiApp", () => { 13 | it("will provide app from the context above", () => { 14 | const app = new PIXI.Application(); 15 | 16 | const TestComponent = jest.fn(() => null); 17 | 18 | const HookContainer = () => { 19 | const app = usePixiApp(); 20 | 21 | return ( 22 | <Container> 23 | <TestComponent app={app} /> 24 | </Container> 25 | ); 26 | }; 27 | 28 | renderer.act(() => { 29 | render( 30 | <AppContext.Provider value={app}> 31 | <HookContainer /> 32 | </AppContext.Provider>, 33 | app.stage 34 | ); 35 | }); 36 | 37 | expect(TestComponent).toHaveBeenCalledWith({ app }, {}); 38 | }); 39 | }); 40 | 41 | describe("usePixiTicker", () => { 42 | it("will add the callback to the app.ticker", () => { 43 | const app = new PIXI.Application(); 44 | const fn = jest.fn(); 45 | const add = jest.spyOn(app.ticker, "add"); 46 | 47 | const TestComponent = () => { 48 | usePixiTicker(fn); 49 | 50 | return <Container />; 51 | }; 52 | 53 | const tree = renderer.create( 54 | <AppContext.Provider value={app}> 55 | <TestComponent /> 56 | </AppContext.Provider> 57 | ); 58 | 59 | // trigger useEffect because that's when usePixiTicker 60 | // calls `app.ticker.add` 61 | tree.update(); 62 | 63 | expect(add).toHaveBeenCalledTimes(1); 64 | expect(add).toHaveBeenCalledWith(fn); 65 | }); 66 | 67 | it("will remove the callback from the app.ticker as a cleanup", () => { 68 | const app = new PIXI.Application(); 69 | const fn = jest.fn(); 70 | const remove = jest.spyOn(app.ticker, "remove"); 71 | 72 | const TestComponent = () => { 73 | usePixiTicker(fn); 74 | 75 | return <Container />; 76 | }; 77 | 78 | const tree = renderer.create( 79 | <AppContext.Provider value={app}> 80 | <TestComponent /> 81 | </AppContext.Provider> 82 | ); 83 | 84 | // trigger useEffect cleanup because that's when usePixiTicker 85 | // calls `app.ticker.remove` 86 | renderer.act(() => { 87 | tree.unmount(); 88 | }); 89 | 90 | expect(remove).toHaveBeenCalledWith(fn); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import * as ReactPixiFiber from "../src/index"; 2 | import CustomPIXIComponent from "../src/CustomPIXIComponent"; 3 | import { AppContext, AppProvider, withApp } from "../src/AppProvider"; 4 | import Stage, { createStageClass } from "../src/Stage"; 5 | import { TYPES } from "../src/types"; 6 | import { usePixiApp, usePixiTicker } from "../src/hooks"; 7 | import { applyDisplayObjectProps } from "../src/ReactPixiFiberComponent"; 8 | 9 | describe("ReactPixiFiber public API", () => { 10 | it("should match snapshot", () => { 11 | expect(ReactPixiFiber).toMatchSnapshot(); 12 | }); 13 | 14 | it("provides expected utils", () => { 15 | expect(ReactPixiFiber.CustomPIXIComponent).toEqual(CustomPIXIComponent); 16 | expect(ReactPixiFiber.applyDisplayObjectProps).toEqual(applyDisplayObjectProps); 17 | expect(ReactPixiFiber.createStageClass).toEqual(createStageClass); 18 | expect(typeof ReactPixiFiber.render).toEqual("function"); 19 | expect(typeof ReactPixiFiber.unmount).toEqual("function"); 20 | }); 21 | 22 | it("provides expected context utils", () => { 23 | expect(ReactPixiFiber.AppContext).toEqual(AppContext); 24 | expect(ReactPixiFiber.AppProvider).toEqual(AppProvider); 25 | expect(ReactPixiFiber.withApp).toEqual(withApp); 26 | }); 27 | 28 | it("provides expected hooks", () => { 29 | expect(ReactPixiFiber.usePixiApp).toEqual(usePixiApp); 30 | expect(ReactPixiFiber.usePixiTicker).toEqual(usePixiTicker); 31 | }); 32 | 33 | it("provides expected components", () => { 34 | expect(ReactPixiFiber.BitmapText).toEqual(TYPES.BITMAP_TEXT); 35 | expect(ReactPixiFiber.Container).toEqual(TYPES.CONTAINER); 36 | expect(ReactPixiFiber.Graphics).toEqual(TYPES.GRAPHICS); 37 | expect(ReactPixiFiber.NineSlicePlane).toEqual(TYPES.NINE_SLICE_PLANE); 38 | expect(ReactPixiFiber.ParticleContainer).toEqual(TYPES.PARTICLE_CONTAINER); 39 | expect(ReactPixiFiber.Sprite).toEqual(TYPES.SPRITE); 40 | expect(ReactPixiFiber.Stage).toEqual(Stage); 41 | expect(ReactPixiFiber.Text).toEqual(TYPES.TEXT); 42 | expect(ReactPixiFiber.TilingSprite).toEqual(TYPES.TILING_SPRITE); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/inject.test.js: -------------------------------------------------------------------------------- 1 | import { INJECTED_TYPES, createInjectedTypeInstance, isInjectedType } from "../src/inject"; 2 | 3 | jest.mock("../src/ReactPixiFiber"); 4 | 5 | describe("inject", () => { 6 | beforeEach(() => { 7 | // injectType is mutating INJECTED_TYPES 8 | jest.resetModules(); 9 | }); 10 | 11 | describe("injectType", () => { 12 | it("should add type to INJECTED_TYPES", () => { 13 | const inject = require("../src/inject"); 14 | const type = "INJECTED_TYPE"; 15 | const behavior = {}; 16 | expect(inject.INJECTED_TYPES).not.toHaveProperty(type); 17 | expect(inject.injectType(type, behavior)); 18 | expect(inject.INJECTED_TYPES).toHaveProperty(type, behavior); 19 | }); 20 | }); 21 | 22 | describe("INJECTED_TYPES", () => { 23 | it("is empty object", () => { 24 | expect(Object.keys(INJECTED_TYPES)).toHaveLength(0); 25 | }); 26 | }); 27 | 28 | describe("createInjectedTypeInstance", () => { 29 | it("returns undefined if type is not in INJECTED_TYPES", () => { 30 | expect(createInjectedTypeInstance("NON_EXISTENT_TYPE", {})).toBeUndefined(); 31 | }); 32 | it("throws when passed incompatible behavior", () => { 33 | const inject = require("../src/inject"); 34 | const type = "INJECTED_TYPE"; 35 | // incompatible behavior 36 | const behavior = {}; 37 | inject.injectType(type, behavior); 38 | expect(() => inject.createInjectedTypeInstance(type)).toThrow(); 39 | }); 40 | it("returns an instance of type if type is in INJECTED_TYPES (with simple behavior)", () => { 41 | const inject = require("../src/inject"); 42 | const type = "INJECTED_TYPE"; 43 | // just return the type name 44 | const customDisplayObject = () => type; 45 | inject.injectType(type, customDisplayObject); 46 | expect(inject.createInjectedTypeInstance(type)).toBeDefined(); 47 | expect(inject.createInjectedTypeInstance(type)).toEqual(customDisplayObject()); 48 | }); 49 | it("returns an instance of type if type is in INJECTED_TYPES (with full behavior)", () => { 50 | const inject = require("../src/inject"); 51 | const type = "INJECTED_TYPE"; 52 | const behavior = { 53 | // customDisplayObject will just return the type name 54 | customDisplayObject: () => type, 55 | }; 56 | inject.injectType(type, behavior); 57 | expect(inject.createInjectedTypeInstance(type)).toBeDefined(); 58 | expect(inject.createInjectedTypeInstance(type)).toEqual(behavior.customDisplayObject()); 59 | }); 60 | it("calls type constructor with props", () => { 61 | const inject = require("../src/inject"); 62 | const type = "INJECTED_TYPE"; 63 | const customDisplayObject = jest.fn(); 64 | inject.injectType(type, customDisplayObject); 65 | const props = { prop: "value" }; 66 | inject.createInjectedTypeInstance(type, props); 67 | expect(customDisplayObject).toHaveBeenCalledTimes(1); 68 | expect(customDisplayObject).toHaveBeenCalledWith(props); 69 | }); 70 | it("attaches custom behavior to created instance", () => { 71 | const inject = require("../src/inject"); 72 | const type = "INJECTED_TYPE"; 73 | const behavior = { 74 | customDisplayObject: () => ({}), 75 | customApplyProps: function(instance, oldProps, newProps) { 76 | // returning `this` so we can test bound `this` value 77 | return this; 78 | }, 79 | customDidAttach: jest.fn(), 80 | customWillDetach: jest.fn(), 81 | }; 82 | inject.injectType(type, behavior); 83 | 84 | const applyDisplayObjectProps = jest.fn(); 85 | const oldProps = { value: 1 }; 86 | const newProps = { value: 2 }; 87 | const instance = inject.createInjectedTypeInstance(type, {}, null, {}, {}, applyDisplayObjectProps); 88 | 89 | const context = instance._customApplyProps(instance, oldProps, newProps); 90 | expect(context).toHaveProperty("applyDisplayObjectProps"); 91 | 92 | context.applyDisplayObjectProps(oldProps, newProps); 93 | expect(applyDisplayObjectProps).toHaveBeenCalledTimes(1); 94 | expect(applyDisplayObjectProps).toHaveBeenCalledWith(type, instance, oldProps, newProps); 95 | 96 | expect(instance._customDidAttach).toEqual(behavior.customDidAttach); 97 | expect(instance._customWillDetach).toEqual(behavior.customWillDetach); 98 | }); 99 | }); 100 | 101 | describe("isInjectedType", () => { 102 | it("returns true if type is injected", () => { 103 | const inject = require("../src/inject"); 104 | const type = "INJECTED_TYPE"; 105 | const behavior = () => ({}); 106 | inject.injectType(type, behavior); 107 | 108 | expect(inject.isInjectedType(type)).toBeTruthy(); 109 | }); 110 | it("returns false if type is not injected", () => { 111 | expect(isInjectedType("NOT_INJECTED_TYPE")).toBeFalsy(); 112 | }); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /test/propTypes.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { validateApp, validateCanvas } from "../src/propTypes"; 3 | import { __RewireAPI__ as PropTypesRewireAPI } from "../src/propTypes"; 4 | import * as PIXI from "pixi.js"; 5 | 6 | describe("validateApp", () => { 7 | const warning = jest.fn(); 8 | 9 | beforeAll(() => { 10 | PropTypesRewireAPI.__Rewire__("warning", warning); 11 | }); 12 | 13 | afterAll(() => { 14 | PropTypesRewireAPI.__ResetDependency__("warning"); 15 | }); 16 | 17 | afterEach(() => { 18 | warning.mockReset(); 19 | }); 20 | 21 | it("passes validation if prop is not defined", () => { 22 | const props = {}; 23 | const propName = "app"; 24 | const componentName = "Component"; 25 | expect(validateApp(props, propName, componentName)).toBeUndefined(); 26 | }); 27 | 28 | it("passes validation if propName is PIXI.Application instance", () => { 29 | const props = { app: new PIXI.Application() }; 30 | const propName = "app"; 31 | const componentName = "Component"; 32 | expect(validateApp(props, propName, componentName)).toBeUndefined(); 33 | }); 34 | 35 | it("should warn about options prop if both options and app are defined", () => { 36 | const props = { app: new PIXI.Application(), options: { width: 800 } }; 37 | const propName = "app"; 38 | const componentName = "Component"; 39 | expect(validateApp(props, propName, componentName)).toBeUndefined(); 40 | 41 | expect(warning).toHaveBeenCalledTimes(1); 42 | expect(warning).toHaveBeenCalledWith( 43 | false, 44 | "'options' property of 'Component' has no effect when 'app' property is provided. Only use 'app' or 'options', never both." 45 | ); 46 | }); 47 | 48 | it("does not pass validation if propName is defined and is not PIXI.Application instance", () => { 49 | const props = { app: {} }; 50 | const propName = "app"; 51 | const propType = typeof props.app; 52 | const componentName = "Component"; 53 | const error = `Invalid prop '${propName}' of type '${propType}' supplied to '${componentName}', expected 'PIXI.Application'.`; 54 | expect(() => { 55 | throw validateApp(props, propName, componentName); 56 | }).toThrow(error); 57 | }); 58 | }); 59 | 60 | describe("validateCanvas", () => { 61 | it("passes validation if prop is not defined", () => { 62 | const props = {}; 63 | const propName = "view"; 64 | const componentName = "Component"; 65 | expect(validateCanvas(props, propName, componentName)).toBeUndefined(); 66 | }); 67 | 68 | it("passes validation if propName is <canvas /> element", () => { 69 | const props = { view: document.createElement("canvas") }; 70 | const propName = "view"; 71 | const componentName = "Component"; 72 | expect(validateCanvas(props, propName, componentName)).toBeUndefined(); 73 | }); 74 | 75 | it("does not pass validation if propName is defined and is not <canvas /> element", () => { 76 | const props = { view: document.createElement("p") }; 77 | const propName = "view"; 78 | const propType = typeof props.view; 79 | const componentName = "Component"; 80 | const error = `Invalid prop '${propName}' of type '${propType}' supplied to '${componentName}', expected '<canvas> Element'.`; 81 | expect(() => { 82 | throw validateCanvas(props, propName, componentName); 83 | }).toThrow(error); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /test/props.test.js: -------------------------------------------------------------------------------- 1 | import { defaultProps } from "../src/props"; 2 | 3 | describe("props", () => { 4 | describe("defaultProps", () => { 5 | it("should match", () => { 6 | expect(defaultProps).toMatchSnapshot(); 7 | }); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/react-pixi-alias/index.test.js: -------------------------------------------------------------------------------- 1 | import * as ReactPIXI from "../../src/react-pixi-alias/index"; 2 | import * as ReactPixiFiber from "react-pixi-fiber"; 3 | import ReactDOM from "react-dom"; 4 | 5 | describe("react-pixi-alias", () => { 6 | it("provides expected methods", () => { 7 | expect(ReactPIXI.CustomPIXIComponent).toEqual(ReactPixiFiber.CustomPIXIComponent); 8 | expect(ReactPIXI.render).toEqual(ReactDOM.render); 9 | expect(ReactPIXI.unmountComponentAtNode).toEqual(ReactDOM.unmountComponentAtNode); 10 | }); 11 | 12 | it("provides expected components", () => { 13 | expect(ReactPIXI.BitmapText).toEqual(ReactPixiFiber.BitmapText); 14 | expect(ReactPIXI.DisplayObjectContainer).toEqual(ReactPixiFiber.Container); 15 | expect(ReactPIXI.Graphics).toEqual(ReactPixiFiber.Graphics); 16 | expect(ReactPIXI.ParticleContainer).toEqual(ReactPixiFiber.ParticleContainer); 17 | expect(ReactPIXI.Sprite).toEqual(ReactPixiFiber.Sprite); 18 | expect(ReactPIXI.Stage).toEqual(ReactPixiFiber.Stage); 19 | expect(ReactPIXI.Text).toEqual(ReactPixiFiber.Text); 20 | expect(ReactPIXI.TilingSprite).toEqual(ReactPixiFiber.TilingSprite); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/render.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Container, Text } from "../src/index"; 3 | import { ReactPixiFiberAsPrimaryRenderer as ReactPixiFiber } from "../src/ReactPixiFiber"; 4 | import { createRender, createUnmount, getDevToolsVersion, roots } from "../src/render"; 5 | import * as PIXI from "pixi.js"; 6 | 7 | jest.mock("../src/ReactPixiFiber", () => { 8 | const actual = jest.requireActual("../src/ReactPixiFiber"); 9 | return Object.assign({}, actual, { 10 | ReactPixiFiberAsPrimaryRenderer: Object.assign({}, actual.ReactPixiFiberAsPrimaryRenderer, { 11 | createContainer: jest.fn(), 12 | getPublicRootInstance: jest.fn(), 13 | injectIntoDevTools: jest.fn(), 14 | updateContainer: jest.fn(), 15 | }), 16 | }); 17 | }); 18 | 19 | describe("getDevToolsVersion", () => { 20 | it("should return React version", () => { 21 | expect(getDevToolsVersion()).toEqual(require("react").version); 22 | }); 23 | }); 24 | 25 | describe("render", () => { 26 | beforeEach(() => { 27 | jest.resetAllMocks(); 28 | }); 29 | 30 | const render = createRender(ReactPixiFiber); 31 | const app = new PIXI.Application(); 32 | const callback = jest.fn(); 33 | const root = app.stage; 34 | const element = ( 35 | <Container> 36 | <Text text="Hello World!" /> 37 | </Container> 38 | ); 39 | 40 | it("calls ReactPixiFiber.createContainer", () => { 41 | render(element, root, callback); 42 | 43 | expect(ReactPixiFiber.createContainer).toHaveBeenCalledTimes(1); 44 | expect(ReactPixiFiber.createContainer).toHaveBeenCalledWith(app.stage); 45 | }); 46 | 47 | it("calls ReactPixiFiber.updateContainer", () => { 48 | render(element, root, callback); 49 | 50 | expect(ReactPixiFiber.updateContainer).toHaveBeenCalledTimes(1); 51 | expect(ReactPixiFiber.updateContainer).toHaveBeenCalledWith(element, roots.get(root), undefined, callback); 52 | }); 53 | 54 | it("calls ReactPixiFiber.injectIntoDevTools", () => { 55 | render(element, root, callback); 56 | 57 | expect(ReactPixiFiber.injectIntoDevTools).toHaveBeenCalledTimes(1); 58 | 59 | expect(ReactPixiFiber.injectIntoDevTools).toHaveBeenCalledWith( 60 | expect.objectContaining({ 61 | findFiberByHostInstance: ReactPixiFiber.findFiberByHostInstance, 62 | bundleType: __DEV__ ? 1 : 0, 63 | version: getDevToolsVersion(), 64 | rendererPackageName: __PACKAGE_NAME__, 65 | }) 66 | ); 67 | }); 68 | 69 | it("does not create root if it is already present", () => { 70 | roots.set(root, app.stage); 71 | render(element, root, callback); 72 | 73 | expect(ReactPixiFiber.createContainer).toHaveBeenCalledTimes(0); 74 | }); 75 | }); 76 | 77 | describe("unmount", () => { 78 | beforeEach(() => { 79 | roots.clear(); 80 | jest.resetAllMocks(); 81 | }); 82 | 83 | const unmount = createUnmount(ReactPixiFiber); 84 | const app = new PIXI.Application(); 85 | const root = app.stage; 86 | 87 | it("calls ReactPixiFiber.updateContainer if it was mounted", () => { 88 | roots.set(root, app.stage); 89 | unmount(root); 90 | 91 | expect(ReactPixiFiber.updateContainer).toHaveBeenCalledTimes(1); 92 | expect(ReactPixiFiber.updateContainer).toHaveBeenCalledWith(null, roots.get(root)); 93 | }); 94 | 95 | it("does not update root if it is not present", () => { 96 | expect(() => unmount(root)).toThrow("ReactPixiFiber did not render into container provided"); 97 | expect(ReactPixiFiber.updateContainer).toHaveBeenCalledTimes(0); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /test/types.test.js: -------------------------------------------------------------------------------- 1 | import { TYPES } from "../src/types"; 2 | 3 | describe("TYPES", () => { 4 | it("is an object mapping of supported types", () => { 5 | expect(TYPES).toEqual({ 6 | BITMAP_TEXT: "BitmapText", 7 | CONTAINER: "Container", 8 | GRAPHICS: "Graphics", 9 | NINE_SLICE_PLANE: "NineSlicePlane", 10 | PARTICLE_CONTAINER: "ParticleContainer", 11 | SPRITE: "Sprite", 12 | TEXT: "Text", 13 | TILING_SPRITE: "TilingSprite", 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from "pixi.js"; 2 | import { filterByKey, including, isPointType, not, parsePoint, setPixiValue, copyPoint } from "../src/utils"; 3 | 4 | describe("not", () => { 5 | it("returns a function", () => { 6 | expect(typeof not()).toEqual("function"); 7 | }); 8 | 9 | it("calls wrapped function when called", () => { 10 | const fn = jest.fn(); 11 | not(fn)(); 12 | 13 | expect(fn).toHaveBeenCalledTimes(1); 14 | }); 15 | 16 | it("negates wrapped function return value", () => { 17 | const returnTrue = jest.fn(() => true); 18 | const returnFalse = jest.fn(() => false); 19 | 20 | expect(not(returnTrue)()).toBeFalsy(); 21 | expect(not(returnFalse)()).toBeTruthy(); 22 | }); 23 | }); 24 | 25 | describe("filterByKey", () => { 26 | it("should return an object when called with object and function", () => { 27 | expect(typeof filterByKey({}, jest.fn())).toEqual("object"); 28 | }); 29 | 30 | it("should return an object with keys matching filter only", () => { 31 | const obj = { 32 | foo: 1, 33 | bar: 2, 34 | baz: 3, 35 | }; 36 | const fooOnly = jest.fn(key => key === "foo"); 37 | const withoutFoo = jest.fn(key => key !== "foo"); 38 | 39 | expect(filterByKey(obj, fooOnly)).toEqual({ 40 | foo: 1, 41 | }); 42 | expect(fooOnly).toHaveBeenCalledTimes(Object.keys(obj).length); 43 | 44 | expect(filterByKey(obj, withoutFoo)).toEqual({ 45 | bar: 2, 46 | baz: 3, 47 | }); 48 | expect(withoutFoo).toHaveBeenCalledTimes(Object.keys(obj).length); 49 | }); 50 | }); 51 | 52 | describe("including", () => { 53 | it("returns a function", () => { 54 | expect(typeof including()).toEqual("function"); 55 | }); 56 | 57 | it("returns true if wrapped array contains value", () => { 58 | const havingFoo = { foo: "bar" }; 59 | 60 | expect(including(Object.keys(havingFoo))("foo")).toBeTruthy(); 61 | }); 62 | 63 | it("returns false if wrapped array does not contain value", () => { 64 | const notHavingBaz = ["foo", "bar"]; 65 | 66 | expect(including(Object.keys(notHavingBaz))("baz")).toBeFalsy(); 67 | }); 68 | }); 69 | 70 | describe("parsePoint", () => { 71 | it("returns array", () => { 72 | expect(parsePoint()).toHaveLength(0); 73 | }); 74 | 75 | it("returns array of strings split by comma if value is a string", () => { 76 | expect(parsePoint("13,37")).toEqual([13, 37]); 77 | }); 78 | 79 | it("returns array with single number if value is a number", () => { 80 | expect(parsePoint(42)).toEqual([42]); 81 | }); 82 | 83 | it("returns copy of an array if value is an array", () => { 84 | expect(parsePoint([100, 10])).toEqual([100, 10]); 85 | }); 86 | 87 | it("returns array with x and y from object if value is an object with x and y members", () => { 88 | expect(parsePoint({ x: 1, y: 0 })).toEqual([1, 0]); 89 | }); 90 | 91 | it("returns empty array otherwise", () => { 92 | expect(parsePoint(false)).toEqual([]); 93 | }); 94 | }); 95 | 96 | describe("isPointType", () => { 97 | const x = 100; 98 | const y = 50; 99 | 100 | it("returns true if value is instance of PIXI.Point", () => { 101 | expect(isPointType(new PIXI.Point(x, y))).toBeTruthy(); 102 | }); 103 | it("returns true if value is instance of PIXI.ObservablePoint", () => { 104 | expect(isPointType(new PIXI.ObservablePoint(jest.fn, null, x, y))).toBeTruthy(); 105 | }); 106 | it("returns false if value is not instance of PIXI.Point or PIXI.ObservablePoint", () => { 107 | expect(isPointType(`${x},${y}`)).toBeFalsy(); 108 | }); 109 | }); 110 | 111 | describe("setPixiValue", () => { 112 | it("copies value if current and next value are point types", () => { 113 | class JestPoint extends PIXI.Point {} 114 | JestPoint.prototype.copyFrom = jest.fn(PIXI.Point.prototype.copyFrom); 115 | const obj = { 116 | test: new JestPoint(0, 0), 117 | }; 118 | const test = new PIXI.Point(13, 37); 119 | 120 | setPixiValue(obj, "test", test); 121 | expect(JestPoint.prototype.copyFrom).toHaveBeenCalledTimes(1); 122 | expect(JestPoint.prototype.copyFrom).toHaveBeenCalledWith(test); 123 | expect(obj.test).toEqual(new PIXI.Point(13, 37)); 124 | }); 125 | 126 | it("parses next value and sets current if only current value is point", () => { 127 | class JestPoint extends PIXI.Point {} 128 | JestPoint.prototype.set = jest.fn(PIXI.Point.prototype.set); 129 | const obj = { 130 | test: new JestPoint(0, 0), 131 | }; 132 | setPixiValue(obj, "test", "13,37"); 133 | expect(JestPoint.prototype.set).toHaveBeenCalledTimes(1); 134 | expect(JestPoint.prototype.set).toHaveBeenCalledWith(13, 37); 135 | expect(obj.test).toEqual(new PIXI.Point(13, 37)); 136 | }); 137 | 138 | it("assigns value directly if neither current nor next value are point types", () => { 139 | const obj = {}; 140 | const value = "value"; 141 | expect(obj.test).not.toEqual(value); 142 | setPixiValue(obj, "test", value); 143 | expect(obj.test).toEqual(value); 144 | }); 145 | 146 | it("throws if current value is point and next value is not point-like", () => { 147 | const obj = { 148 | test: new PIXI.Point(0, 0), 149 | }; 150 | expect(() => setPixiValue(obj, "test", false)).toThrow(); 151 | }); 152 | }); 153 | 154 | // The copy method has been deprecated in PIXI 5.0. 155 | // Should react-pixi-fiber ever be updated to use 5.0, 156 | // this test should probably be updated test for existance of copyForm instead. 157 | describe("copyPoint", () => { 158 | const PixiJSv4Point = { copy: jest.fn() }; 159 | // Method Point.copy is still available in PixiJS v5 but it is deprecated 160 | const PixiJSv5Point = { copy: jest.fn(), copyFrom: jest.fn() }; 161 | 162 | it("copies value using copy method when using PixiJS v4", () => { 163 | const instance = { 164 | position: PixiJSv4Point, 165 | }; 166 | const position = new PIXI.Point(13, 37); 167 | 168 | copyPoint(instance, "position", position); 169 | expect(PixiJSv4Point.copy).toHaveBeenCalledTimes(1); 170 | expect(PixiJSv4Point.copy).toHaveBeenCalledWith(position); 171 | }); 172 | 173 | it("copies value using copyFrom method when using PixiJS v5", () => { 174 | const instance = { 175 | position: PixiJSv5Point, 176 | }; 177 | const position = new PIXI.Point(13, 37); 178 | 179 | copyPoint(instance, "position", position); 180 | expect(PixiJSv5Point.copy).not.toHaveBeenCalled(); 181 | expect(PixiJSv5Point.copyFrom).toHaveBeenCalledTimes(1); 182 | expect(PixiJSv5Point.copyFrom).toHaveBeenCalledWith(position); 183 | }); 184 | }); 185 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2015", "dom"], 4 | "noEmit": true, 5 | "strict": true, 6 | "jsx": "react", 7 | "baseUrl": "./", 8 | // Required for importing 3rd-party dependencies like EventEmitter3 9 | "esModuleInterop": true, 10 | "paths": { 11 | // Loaders needs this to use the more strict mini-signal types 12 | "mini-signals": [ 13 | "node_modules/resource-loader/typings/mini-signals.d.ts" 14 | ], 15 | "react-pixi-fiber": ["index.d.ts"] 16 | }, 17 | "types": [] 18 | }, 19 | "files": [ 20 | "test/typescript/index.tsx" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = function(wallaby) { 2 | return { 3 | files: ["src/**/*.js", "package.json", "./config/jest/**/*.js", "./config/jest.dev.json"], 4 | tests: ["test/**/*.js"], 5 | compilers: { 6 | "**/*.js": wallaby.compilers.babel(), 7 | }, 8 | env: { 9 | type: "node", 10 | }, 11 | // https://wallabyjs.com/docs/integration/jest.html 12 | testFramework: "jest", 13 | setup: function(wallaby) { 14 | const jestConfig = require("./config/jest.dev.json"); 15 | delete jestConfig.coverageDirectory; 16 | delete jestConfig.coverageReporters; 17 | delete jestConfig.rootDir; 18 | wallaby.testFramework.configure(jestConfig); 19 | }, 20 | }; 21 | }; 22 | --------------------------------------------------------------------------------