diff --git a/app/react-components/pixi/extensions.js b/app/react-components/pixi/extensions.js new file mode 100644 index 0000000..c805f39 --- /dev/null +++ b/app/react-components/pixi/extensions.js @@ -0,0 +1,32 @@ +import {ExtensionType} from '@pixi/core'; +import {Layer, Stage as LayerStage} from '@pixi/layers'; + +let AmbientLight, diffuseGroup, normalGroup, lightGroup; +if ('undefined' !== typeof window) { + ({AmbientLight, diffuseGroup, normalGroup, lightGroup} = await import('@pixi/lights')); +} + +export const ApplicationStageLayers = { + type: ExtensionType.Application, + priority: 100, + ref: { + destroy: function() {}, + init: function() { + this.stage = new LayerStage(); + }, + }, +}; + +export const ApplicationStageLights = { + type: ExtensionType.Application, + ref: { + destroy: function() {}, + init: function() { + const {stage} = this; + stage.addChild(new Layer(diffuseGroup)); + stage.addChild(new Layer(normalGroup)); + stage.addChild(new Layer(lightGroup)); + stage.addChild(new AmbientLight(0xffffff, 1)); + }, + }, +}; diff --git a/app/react-components/pixi/pixi.jsx b/app/react-components/pixi/pixi.jsx index 9069daa..89c8261 100644 --- a/app/react-components/pixi/pixi.jsx +++ b/app/react-components/pixi/pixi.jsx @@ -1,5 +1,6 @@ import {SCALE_MODES} from '@pixi/constants'; -import {BaseTexture} from '@pixi/core'; +import {BaseTexture, extensions} from '@pixi/core'; +import {Stage as PixiStage} from '@pixi/react'; import {createElement, useContext} from 'react'; import {RESOLUTION} from '@/constants.js'; @@ -11,11 +12,16 @@ import MainEntityContext from '@/context/main-entity.js'; import RadiansContext from '@/context/radians.js'; import Ecs from './ecs.jsx'; +import {ApplicationStageLayers, ApplicationStageLights} from './extensions.js'; import styles from './pixi.module.css'; -import PixiStage from './stage.jsx'; BaseTexture.defaultOptions.scaleMode = SCALE_MODES.NEAREST; +extensions.add( + ApplicationStageLayers, + ApplicationStageLights, +); + const Contexts = [ AssetsContext, ClientContext, diff --git a/app/react-components/pixi/stage.jsx b/app/react-components/pixi/stage.jsx deleted file mode 100644 index a8ab8c3..0000000 --- a/app/react-components/pixi/stage.jsx +++ /dev/null @@ -1,350 +0,0 @@ -import { Application } from '@pixi/app'; -import { Layer, Stage as LayerStage } from '@pixi/layers'; - -import { Ticker } from '@pixi/ticker'; -import { AppProvider, PixiFiber } from '@pixi/react'; - -import PropTypes from 'prop-types'; -import React from 'react'; - -const PROPS_DISPLAY_OBJECT = { - alpha: 1, - buttonMode: false, - cacheAsBitmap: false, - cursor: null, - filterArea: null, - filters: null, - hitArea: null, - interactive: false, - mask: null, - pivot: 0, - position: 0, - renderable: true, - rotation: 0, - scale: 1, - skew: 0, - transform: null, - visible: true, - x: 0, - y: 0, -}; - -let AmbientLight, diffuseGroup, normalGroup, lightGroup; -if ('undefined' !== typeof window) { - ({AmbientLight, diffuseGroup, normalGroup, lightGroup} = await import('@pixi/lights')); -} - -const noop = () => {}; - -/** - * ------------------------------------------- - * Stage React Component (use this in react-dom) - * - * @usage - * - * const App = () => ( - * { - * console.log('PIXI renderer: ', renderer) - * console.log('Canvas element: ', canvas) - * }}> - * ); - * - * ------------------------------------------- - */ - -const propTypes = { - // dimensions - width: PropTypes.number, - height: PropTypes.number, - - // will return renderer - onMount: PropTypes.func, - onUnmount: PropTypes.func, - - // run ticker at start? - raf: PropTypes.bool, - - // render component on component lifecycle changes? - renderOnComponentChange: PropTypes.bool, - - children: PropTypes.node, - - // PIXI options, see https://pixijs.download/v7.x/docs/PIXI.Application.html - options: PropTypes.shape({ - autoStart: PropTypes.bool, - width: PropTypes.number, - height: PropTypes.number, - useContextAlpha: PropTypes.bool, - backgroundAlpha: PropTypes.number, - autoDensity: PropTypes.bool, - antialias: PropTypes.bool, - preserveDrawingBuffer: PropTypes.bool, - resolution: PropTypes.number, - forceCanvas: PropTypes.bool, - backgroundColor: PropTypes.number, - clearBeforeRender: PropTypes.bool, - powerPreference: PropTypes.string, - sharedTicker: PropTypes.bool, - sharedLoader: PropTypes.bool, - }), -}; - -const defaultProps = { - width: 800, - height: 600, - onMount: noop, - onUnmount: noop, - raf: true, - renderOnComponentChange: true, -}; - -export function getCanvasProps(props) -{ - const reserved = [ - ...Object.keys(propTypes), - ...Object.keys(PROPS_DISPLAY_OBJECT), - ]; - - return Object.keys(props) - .filter((p) => !reserved.includes(p)) - .reduce((all, prop) => ({ ...all, [prop]: props[prop] }), {}); -} - -class Stage extends React.Component -{ - _canvas = null; - _mediaQuery = null; - _ticker = null; - _needsUpdate = true; - app = null; - - componentDidMount() - { - const { - onMount, - width, - height, - options, - raf, - renderOnComponentChange, - } = this.props; - - this.app = new Application({ - width, - height, - view: this._canvas, - ...options, - autoDensity: options?.autoDensity !== false, - }); - const stage = new LayerStage(); - this.app.stage = stage; - stage.addChild(new Layer(diffuseGroup)); - stage.addChild(new Layer(normalGroup)); - stage.addChild(new Layer(lightGroup)); - stage.addChild(new AmbientLight(0xffffff, 1)); - - if (process.env.NODE_ENV === 'development') - { - // workaround for React 18 Strict Mode unmount causing - // webgl canvas context to be lost - if (this.app.renderer.context?.extensions) - { - this.app.renderer.context.extensions.loseContext = null; - } - } - - this.app.ticker.autoStart = false; - this.app.ticker[raf ? 'start' : 'stop'](); - - this.app.stage.__reactpixi = { root: this.app.stage }; - this.mountNode = PixiFiber.createContainer(this.app.stage); - PixiFiber.updateContainer(this.getChildren(), this.mountNode, this); - - onMount(this.app); - - // update size on media query resolution change? - // only if autoDensity = true - if ( - options?.autoDensity - && window.matchMedia - && options?.resolution === undefined - ) - { - this._mediaQuery = window.matchMedia( - `(-webkit-min-device-pixel-ratio: 1.3), (min-resolution: 120dpi)` - ); - this._mediaQuery.addListener(this.updateSize); - } - - // listen for reconciler changes - if (renderOnComponentChange && !raf) - { - this._ticker = new Ticker(); - this._ticker.autoStart = true; - this._ticker.add(this.renderStage); - this.app.stage.on( - '__REACT_PIXI_REQUEST_RENDER__', - this.needsRenderUpdate - ); - } - - this.updateSize(); - this.renderStage(); - } - - componentDidUpdate(prevProps) - { - const { width, height, raf, renderOnComponentChange, options } - = this.props; - - // update resolution - if ( - options?.resolution !== undefined - && prevProps?.options.resolution !== options?.resolution - ) - { - this.app.renderer.resolution = options.resolution; - this.resetInteractionManager(); - } - - // update size - if ( - prevProps.height !== height - || prevProps.width !== width - || prevProps.options?.resolution !== options?.resolution - ) - { - this.updateSize(); - } - - // handle raf change - if (prevProps.raf !== raf) - { - this.app.ticker[raf ? 'start' : 'stop'](); - } - - // flush fiber - PixiFiber.updateContainer(this.getChildren(), this.mountNode, this); - - if ( - prevProps.width !== width - || prevProps.height !== height - || prevProps.raf !== raf - || prevProps.renderOnComponentChange !== renderOnComponentChange - || prevProps.options !== options - ) - { - this._needsUpdate = true; - this.renderStage(); - } - } - - updateSize = () => - { - const { width, height, options } = this.props; - - if (!options?.resolution) - { - this.app.renderer.resolution = window.devicePixelRatio; - this.resetInteractionManager(); - } - - this.app.renderer.resize(width, height); - }; - - needsRenderUpdate = () => - { - this._needsUpdate = true; - }; - - renderStage = () => - { - const { renderOnComponentChange, raf } = this.props; - - if (!raf && renderOnComponentChange && this._needsUpdate) - { - this._needsUpdate = false; - this.app.renderer.render(this.app.stage); - } - }; - - // provide support for Pixi v6 still - resetInteractionManager() - { - // `interaction` property is absent in Pixi v7 and in v6 if user has installed Federated Events API plugin. - // https://api.pixijs.io/@pixi/events.html - // in v7 however, there's a stub object which displays a deprecation warning, so also check the resolution property: - const { interaction: maybeInteraction } = this.app.renderer.plugins; - - if (maybeInteraction?.resolution) - { - maybeInteraction.resolution = this.app.renderer.resolution; - } - } - - getChildren() - { - const { children } = this.props; - - return {children}; - } - - componentDidCatch(error, errorInfo) - { - console.error(`Error occurred in \`Stage\`.`); - console.error(error); - console.error(errorInfo); - } - - componentWillUnmount() - { - this.props.onUnmount(this.app); - - if (this._ticker) - { - this._ticker.remove(this.renderStage); - this._ticker.destroy(); - } - - this.app.stage.off( - '__REACT_PIXI_REQUEST_RENDER__', - this.needsRenderUpdate - ); - - PixiFiber.updateContainer(null, this.mountNode, this); - - if (this._mediaQuery) - { - this._mediaQuery.removeListener(this.updateSize); - this._mediaQuery = null; - } - - this.app.destroy(); - } - - render() - { - const { options } = this.props; - - if (options && options.view) - { - return null; - } - - return ( - (this._canvas = c)} - /> - ); - } -} - -Stage.propTypes = propTypes; -Stage.defaultProps = defaultProps; - -export default Stage;