UNPKG

@sacredcasuals/shared-lib

Version:

Shared game utilities and UI components for Sacred Casuals apps

1 lines 417 kB
{"version":3,"sources":["../node_modules/typed-signals/dist/Collector.js","../node_modules/typed-signals/dist/CollectorArray.js","../node_modules/typed-signals/dist/CollectorLast.js","../node_modules/typed-signals/dist/CollectorUntil0.js","../node_modules/typed-signals/dist/CollectorWhile0.js","../node_modules/typed-signals/dist/SignalConnection.js","../node_modules/typed-signals/dist/SignalLink.js","../node_modules/typed-signals/dist/Signal.js","../node_modules/typed-signals/dist/SignalConnections.js","../node_modules/typed-signals/dist/index.js","../src/managers/DeviceManager.ts","../src/pixi/AbstractPixiRoot.ts","../src/pixi/base/GameView.ts","../src/pixi/base/GameObject.ts","../src/pixi/base/GameDispatcher.ts","../src/pixi/base/GameScreen.ts","../src/pixi/effects/BackgroundEffect.ts","../src/utils/MathUtils.ts","../src/pixi/utils/RectangleUtils.ts","../src/pixi/utils/ViewUtils.ts","../src/pixi/base/GameWaiter.ts","../src/pixi/base/GameDialog.ts","../src/managers/SoundManager.ts","../src/utils/DateUtils.ts","../src/managers/AbstractPreferencesManager.ts","../src/utils/ObjectUtils.ts","../src/pixi/layers/GameDialogLayer.ts","../src/pixi/layers/GameScreenLayer.ts","../src/pixi/base/GameSplashScreen.ts","../src/pixi/layers/SplashLayer.ts","../src/pixi/layers/UILayer.ts","../src/pixi/layers/WaiterLayer.ts","../src/utils/ArrayUtils.ts","../src/pixi/base/GameJuggler.ts","../src/pixi/base/GamePool.ts","../src/pixi/base/GameSprite.ts","../src/pixi/dialogs/AbstractInfoDialog.ts","../src/pixi/base/GameScreenNavigator.ts","../src/pixi/assets/PixiAssetsLoader.ts","../src/pixi/flump/library/LibraryFlump.ts","../src/pixi/flump/library/KeyframeMold.ts","../src/pixi/flump/library/MovieLayerMold.ts","../src/pixi/flump/display/Placer.ts","../src/pixi/flump/display/Container.ts","../src/pixi/flump/library/MovieLayer.ts","../src/utils/FuncUtil.ts","../src/pixi/flump/display/Movie.ts","../src/pixi/flump/library/MovieMold.ts","../src/pixi/flump/display/Button.ts","../src/pixi/flump/display/TabButton.ts","../src/pixi/flump/display/ClickArea.ts","../src/pixi/flump/display/TextInput.ts","../src/pixi/flump/display/Checkbox.ts","../src/pixi/flump/library/AtlasTextureMold.ts","../src/pixi/flump/library/AtlasMold.ts","../src/pixi/flump/library/TextureGroupMold.ts","../src/pixi/flump/library/LibraryMold.ts","../src/pixi/spine/SpineMovie.ts","../src/pixi/spine/LibrarySpine.ts","../src/pixi/text/LibraryText.ts","../src/pixi/quadtree/GameQuadtreeView.ts","../src/pixi/quadtree/Quadtree.ts","../src/pixi/quadtree/QuadtreeNode.ts","../src/pixi/ui/Input.ts","../src/pixi/ui/utils/helpers/view.ts","../src/pixi/ui/List.ts","../src/pixi/ui/ScrollBox.ts","../src/pixi/ui/utils/trackpad/Trackpad.ts","../src/pixi/ui/utils/trackpad/Spring.ts","../src/pixi/ui/utils/trackpad/ScrollSpring.ts","../src/pixi/ui/utils/trackpad/SlidingNumber.ts","../src/pixi/ui/utils/HelpTypes.ts","../src/pixi/ui/utils/helpers/cleanup.ts","../src/pixi/ui/utils/helpers/fit.ts","../src/pixi/ui/utils/helpers/resize.ts","../src/pixi/ui/utils/helpers/styles.ts","../src/pixi/ui/utils/helpers/text.ts","../src/managers/ErrorManager.ts","../src/managers/NetworkManager.ts","../src/managers/LocaleManager.ts","../src/app/AbstractApp.ts"],"sourcesContent":["\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.Collector = void 0;\n/**\n * Base class for collectors.\n *\n * @typeparam THandler The function signature to be implemented by handlers.\n */\nclass Collector {\n /**\n * Create a new collector.\n *\n * @param signal The signal to emit.\n */\n constructor(signal) {\n // eslint-disable-next-line dot-notation\n this.emit = (...args) => {\n // eslint-disable-next-line dot-notation\n signal[\"emitCollecting\"](this, args);\n };\n }\n}\nexports.Collector = Collector;\n","\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.CollectorArray = void 0;\nconst Collector_1 = require(\"./Collector\");\n/**\n * Returns the result of the all signal handlers from a signal emission in an array.\n *\n * @typeparam THandler The function signature to be implemented by handlers.\n */\nclass CollectorArray extends Collector_1.Collector {\n constructor() {\n super(...arguments);\n this.result = [];\n }\n handleResult(result) {\n this.result.push(result);\n return true;\n }\n /**\n * Get the list of results from the signal handlers.\n */\n getResult() {\n return this.result;\n }\n /**\n * Reset the result\n */\n reset() {\n this.result.length = 0;\n }\n}\nexports.CollectorArray = CollectorArray;\n","\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.CollectorLast = void 0;\nconst Collector_1 = require(\"./Collector\");\n/**\n * Returns the result of the last signal handler from a signal emission.\n *\n * @typeparam THandler The function signature to be implemented by handlers.\n */\nclass CollectorLast extends Collector_1.Collector {\n handleResult(result) {\n this.result = result;\n return true;\n }\n /**\n * Get the result of the last signal handler.\n */\n getResult() {\n return this.result;\n }\n /**\n * Reset the result\n */\n reset() {\n delete this.result;\n }\n}\nexports.CollectorLast = CollectorLast;\n","\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.CollectorUntil0 = void 0;\nconst Collector_1 = require(\"./Collector\");\n/**\n * Keep signal emissions going while all handlers return true.\n *\n * @typeparam THandler The function signature to be implemented by handlers.\n */\nclass CollectorUntil0 extends Collector_1.Collector {\n constructor() {\n super(...arguments);\n this.result = false;\n }\n handleResult(result) {\n this.result = result;\n return this.result;\n }\n /**\n * Get the result of the last signal handler.\n */\n getResult() {\n return this.result;\n }\n /**\n * Reset the result\n */\n reset() {\n this.result = false;\n }\n}\nexports.CollectorUntil0 = CollectorUntil0;\n","\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.CollectorWhile0 = void 0;\nconst Collector_1 = require(\"./Collector\");\n/**\n * Keep signal emissions going while all handlers return false.\n *\n * @typeparam THandler The function signature to be implemented by handlers.\n */\nclass CollectorWhile0 extends Collector_1.Collector {\n constructor() {\n super(...arguments);\n this.result = false;\n }\n handleResult(result) {\n this.result = result;\n return !this.result;\n }\n /**\n * Get the result of the last signal handler.\n */\n getResult() {\n return this.result;\n }\n /**\n * Reset the result\n */\n reset() {\n this.result = false;\n }\n}\nexports.CollectorWhile0 = CollectorWhile0;\n","\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.SignalConnectionImpl = void 0;\n/**\n * Implementation of SignalConnection, for internal use only.\n * @private\n */\nclass SignalConnectionImpl {\n /**\n * @param link The actual link of the connection.\n * @param parentCleanup Callback to cleanup the parent signal when a connection is disconnected\n */\n constructor(link, parentCleanup) {\n this.link = link;\n this.parentCleanup = parentCleanup;\n }\n disconnect() {\n if (this.link !== null) {\n this.link.unlink();\n this.link = null;\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n this.parentCleanup();\n this.parentCleanup = null;\n return true;\n }\n return false;\n }\n set enabled(enable) {\n if (this.link)\n this.link.setEnabled(enable);\n }\n get enabled() {\n // eslint-disable-next-line @typescript-eslint/prefer-optional-chain\n return this.link !== null && this.link.isEnabled();\n }\n}\nexports.SignalConnectionImpl = SignalConnectionImpl;\n","\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.SignalLink = void 0;\n/**\n * SignalLink implements a doubly-linked ring with nodes containing the signal handlers.\n * @private\n */\nclass SignalLink {\n constructor(prev = null, next = null, order = 0) {\n this.enabled = true;\n this.newLink = false;\n this.callback = null;\n this.prev = prev !== null && prev !== void 0 ? prev : this;\n this.next = next !== null && next !== void 0 ? next : this;\n this.order = order;\n }\n isEnabled() {\n return this.enabled && !this.newLink;\n }\n setEnabled(flag) {\n this.enabled = flag;\n }\n unlink() {\n this.callback = null;\n this.next.prev = this.prev;\n this.prev.next = this.next;\n }\n insert(callback, order) {\n let after = this.prev;\n while (after !== this) {\n if (after.order <= order)\n break;\n after = after.prev;\n }\n const link = new SignalLink(after, after.next, order);\n link.callback = callback;\n after.next = link;\n link.next.prev = link;\n return link;\n }\n}\nexports.SignalLink = SignalLink;\n","\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.Signal = void 0;\nconst SignalConnection_1 = require(\"./SignalConnection\");\nconst SignalLink_1 = require(\"./SignalLink\");\n/**\n * A signal is a way to publish and subscribe to events.\n *\n * @typeparam THandler The function signature to be implemented by handlers.\n */\nclass Signal {\n constructor() {\n this.head = new SignalLink_1.SignalLink();\n this.hasNewLinks = false;\n this.emitDepth = 0;\n this.connectionsCount = 0;\n }\n /**\n * @returns The number of connections on this signal.\n */\n getConnectionsCount() {\n return this.connectionsCount;\n }\n /**\n * @returns true if this signal has connections.\n */\n hasConnections() {\n return this.connectionsCount > 0;\n }\n /**\n * Subscribe to this signal.\n *\n * @param callback This callback will be run when emit() is called.\n * @param order Handlers with a higher order value will be called later.\n */\n connect(callback, order = 0) {\n this.connectionsCount++;\n const link = this.head.insert(callback, order);\n if (this.emitDepth > 0) {\n this.hasNewLinks = true;\n link.newLink = true;\n }\n return new SignalConnection_1.SignalConnectionImpl(link, () => this.decrementConnectionCount());\n }\n decrementConnectionCount() {\n this.connectionsCount--;\n }\n /**\n * Unsubscribe from this signal with the original callback instance.\n * While you can use this method, the SignalConnection returned by connect() will not be updated!\n *\n * @param callback The callback you passed to connect().\n */\n disconnect(callback) {\n for (let link = this.head.next; link !== this.head; link = link.next) {\n if (link.callback === callback) {\n this.decrementConnectionCount();\n link.unlink();\n return true;\n }\n }\n return false;\n }\n /**\n * Disconnect all handlers from this signal event.\n */\n disconnectAll() {\n while (this.head.next !== this.head) {\n this.head.next.unlink();\n }\n this.connectionsCount = 0;\n }\n /**\n * Publish this signal event (call all handlers).\n */\n emit(...args) {\n this.emitDepth++;\n for (let link = this.head.next; link !== this.head; link = link.next) {\n if (link.isEnabled() && link.callback)\n link.callback.apply(null, args);\n }\n this.emitDepth--;\n this.unsetNewLink();\n }\n emitCollecting(collector, args) {\n this.emitDepth++;\n for (let link = this.head.next; link !== this.head; link = link.next) {\n if (link.isEnabled() && link.callback) {\n const result = link.callback.apply(null, args);\n if (!collector.handleResult(result))\n break;\n }\n }\n this.emitDepth--;\n this.unsetNewLink();\n }\n unsetNewLink() {\n if (this.hasNewLinks && this.emitDepth === 0) {\n for (let link = this.head.next; link !== this.head; link = link.next)\n link.newLink = false;\n this.hasNewLinks = false;\n }\n }\n}\nexports.Signal = Signal;\n","\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.SignalConnections = void 0;\n/**\n * Represents a list of connections to a signal for easy disconnect.\n */\nclass SignalConnections {\n constructor() {\n this.list = [];\n }\n /**\n * Add a connection to the list.\n * @param connection\n */\n add(connection) {\n this.list.push(connection);\n }\n /**\n * Disconnect all connections in the list and empty the list.\n */\n disconnectAll() {\n for (const connection of this.list) {\n connection.disconnect();\n }\n this.list = [];\n }\n /**\n * @returns The number of connections in this list.\n */\n getCount() {\n return this.list.length;\n }\n /**\n * @returns true if this list is empty.\n */\n isEmpty() {\n return this.list.length === 0;\n }\n}\nexports.SignalConnections = SignalConnections;\n","\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.SignalConnections = exports.Signal = exports.CollectorWhile0 = exports.CollectorUntil0 = exports.CollectorLast = exports.CollectorArray = exports.Collector = void 0;\nvar Collector_1 = require(\"./Collector\");\nObject.defineProperty(exports, \"Collector\", { enumerable: true, get: function () { return Collector_1.Collector; } });\nvar CollectorArray_1 = require(\"./CollectorArray\");\nObject.defineProperty(exports, \"CollectorArray\", { enumerable: true, get: function () { return CollectorArray_1.CollectorArray; } });\nvar CollectorLast_1 = require(\"./CollectorLast\");\nObject.defineProperty(exports, \"CollectorLast\", { enumerable: true, get: function () { return CollectorLast_1.CollectorLast; } });\nvar CollectorUntil0_1 = require(\"./CollectorUntil0\");\nObject.defineProperty(exports, \"CollectorUntil0\", { enumerable: true, get: function () { return CollectorUntil0_1.CollectorUntil0; } });\nvar CollectorWhile0_1 = require(\"./CollectorWhile0\");\nObject.defineProperty(exports, \"CollectorWhile0\", { enumerable: true, get: function () { return CollectorWhile0_1.CollectorWhile0; } });\nvar Signal_1 = require(\"./Signal\");\nObject.defineProperty(exports, \"Signal\", { enumerable: true, get: function () { return Signal_1.Signal; } });\nvar SignalConnections_1 = require(\"./SignalConnections\");\nObject.defineProperty(exports, \"SignalConnections\", { enumerable: true, get: function () { return SignalConnections_1.SignalConnections; } });\n","import {Device} from \"@capacitor/device\";\nimport {App} from \"@capacitor/app\";\nimport {SafeArea} from 'capacitor-plugin-safe-area';\nimport {SecureStoragePlugin} from \"capacitor-secure-storage-plugin\";\nimport {GameDispatcher} from \"../pixi\";\n\nexport class DeviceDetectorService {\n\n private userAgent: string;\n\n constructor() {\n this.userAgent = navigator.userAgent || '';\n }\n\n public isMobile(): boolean {\n return /Mobi|Android|iPhone|iPod|IEMobile|Opera Mini/i.test(this.userAgent);\n }\n\n public isTablet(): boolean {\n return /iPad|Tablet|PlayBook|Silk/i.test(this.userAgent);\n }\n\n public isDesktop(): boolean {\n return !this.isMobile() && !this.isTablet();\n }\n\n public getDeviceType(): 'mobile' | 'tablet' | 'desktop' {\n if (this.isTablet()) return 'tablet';\n if (this.isMobile()) return 'mobile';\n return 'desktop';\n }\n\n public getOS(): string {\n if (/Windows NT/i.test(this.userAgent)) return 'Windows';\n if (/Macintosh|Mac OS X/i.test(this.userAgent)) return 'macOS';\n if (/Linux/i.test(this.userAgent)) return 'Linux';\n if (/Android/i.test(this.userAgent)) return 'Android';\n if (/iPhone|iPad|iPod/i.test(this.userAgent)) return 'iOS';\n return 'Unknown';\n }\n\n public getBrowser(): string {\n if (/Chrome\\/\\d+/i.test(this.userAgent)) return 'Chrome';\n if (/Firefox\\/\\d+/i.test(this.userAgent)) return 'Firefox';\n if (/Safari\\/\\d+/i.test(this.userAgent) && !/Chrome/i.test(this.userAgent)) return 'Safari';\n if (/Edg\\/\\d+/i.test(this.userAgent)) return 'Edge';\n if (/MSIE|Trident/i.test(this.userAgent)) return 'Internet Explorer';\n return 'Unknown';\n }\n\n public getInfo() {\n return {\n userAgent: this.userAgent,\n deviceType: this.getDeviceType(),\n os: this.getOS(),\n browser: this.getBrowser(),\n };\n }\n}\n\n\nexport class DeviceManager {\n\n public readonly onAppUrlOpenWithUrl = new GameDispatcher(this);\n public readonly onActivateEvent = new GameDispatcher(this);\n public readonly onDeactivateEvent = new GameDispatcher(this);\n public readonly onWindowResize = new GameDispatcher(this);\n public readonly onExit = new GameDispatcher(this);\n\n private _deviceLanguageCode = 'en';\n private _isAndroid = false;\n private _isIOS = false;\n private _isTablet = false;\n private _isDesktop = false;\n private _isMobile = false;\n private _deviceName = '';\n private _deviceModel = '';\n private _deviceManufacturer = '';\n private _deviceOSName = '';\n private _deviceOperatingSystem = '';\n private _deviceOSVersion = '';\n private _appOpenWithUrl = '';\n private _safeAreaTop = 0;\n private _safeAreaBottom = 0;\n private _resizeDebounceTimerId: any = null;\n private _resizeDebounceTime = 200; // ms\n private _lastWindowWidth = 0;\n private _lastWindowHeight = 0;\n\n public async init() {\n await this.waitForDOMReady();\n\n let deviceDetector = new DeviceDetectorService();\n this._isTablet = deviceDetector.isTablet();\n this._isDesktop = deviceDetector.isDesktop();\n this._isMobile = deviceDetector.isMobile();\n\n const languageCode = await Device.getLanguageCode();\n this._deviceLanguageCode = languageCode.value;\n\n const deviceInfo = await Device.getInfo();\n this._deviceName = deviceInfo.name || '';\n this._deviceModel = deviceInfo.model;\n this._deviceManufacturer = deviceInfo.manufacturer;\n this._deviceOSName = deviceInfo.operatingSystem;\n this._deviceOSVersion = deviceInfo.osVersion;\n this._deviceOperatingSystem = deviceInfo.operatingSystem;\n\n this._isAndroid = deviceInfo.platform === 'android';\n this._isIOS = deviceInfo.platform === 'ios';\n\n let launchData = await App.getLaunchUrl();\n this._appOpenWithUrl = launchData?.url || '';\n if (this._appOpenWithUrl) {\n this.onAppUrlOpenWithUrl.dispatch();\n }\n void App.addListener('appUrlOpen', data => {\n this._appOpenWithUrl = data.url;\n this.onAppUrlOpenWithUrl.dispatch();\n });\n\n if (this._isDesktop) {\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'visible') {\n this.onActivate();\n } else {\n this.onDeactivate();\n }\n });\n } else {\n void App.addListener('pause', () => this.onDeactivate());\n void App.addListener('resume', () => this.onActivate());\n }\n\n await this.updateSafeArea();\n\n this._lastWindowWidth = window.innerWidth;\n this._lastWindowHeight = window.innerHeight;\n window.addEventListener('resize', ()=>{\n this.checkWindowResize();\n });\n window.addEventListener('orientationchange', ()=>{\n this.checkWindowResize();\n });\n void App.addListener('appStateChange', ({ isActive }) => {\n if (isActive) {\n this.checkWindowResize();\n }\n });\n }\n\n get isPortrait() {\n return window.innerHeight > window.innerWidth;\n }\n\n get isLandscape() {\n return window.innerWidth > window.innerHeight;\n }\n\n get devicePixelRatio() {\n return window.devicePixelRatio || 1;\n }\n\n private async checkWindowResize() {\n clearTimeout(this._resizeDebounceTimerId);\n const onWindowResize = async () => {\n const newWidth = window.innerWidth;\n const newHeight = window.innerHeight;\n if (this._lastWindowWidth != newWidth || this._lastWindowHeight != newHeight) {\n this._lastWindowWidth = newWidth;\n this._lastWindowHeight = newHeight;\n await this.updateSafeArea();\n this.onWindowResize.dispatch();\n }\n }\n this._resizeDebounceTimerId = setTimeout(onWindowResize, this._resizeDebounceTime); // задержка 200 мс, можно настроить\n }\n\n private async updateSafeArea(){\n if (this.isMobile) {\n const safeArea = await SafeArea.getSafeAreaInsets();\n this._safeAreaTop = safeArea.insets.top;\n this._safeAreaBottom = safeArea.insets.bottom;\n }\n else {\n const style = getComputedStyle(document.documentElement);\n const top = style.getPropertyValue('--safe-area-inset-top');\n const bottom = style.getPropertyValue('--safe-area-inset-bottom');\n this._safeAreaTop = parseInt(top) || 0;\n this._safeAreaBottom = parseInt(bottom) || 0;\n }\n }\n\n get safeAreaTop(): number {\n return this._safeAreaTop;\n }\n\n get safeAreaBottom(): number {\n return this._safeAreaBottom;\n }\n\n private onActivate() {\n this.onActivateEvent.dispatch();\n }\n\n private onDeactivate() {\n this.onDeactivateEvent.dispatch();\n }\n\n public forceExit() {\n this.onExit.dispatch();\n if (this._isMobile) {\n App.exitApp();\n }\n }\n\n private async waitForDOMReady() {\n if (document.readyState === 'complete' || document.readyState === 'interactive') return;\n return new Promise<void>((resolve) => {\n document.addEventListener('DOMContentLoaded', () => resolve(), { once: true });\n });\n }\n\n public get appOpenWithUrl() { return this._appOpenWithUrl; }\n public get deviceLanguageCode() { return this._deviceLanguageCode; }\n public get deviceName() { return this._deviceName; }\n public get deviceOperatingSystem() { return this._deviceOperatingSystem; }\n public get deviceModel() { return this._deviceModel; }\n public get deviceManufacturer() { return this._deviceManufacturer; }\n public get deviceOSName() { return this._deviceOSName; }\n public get deviceOSVersion() { return this._deviceOSVersion; }\n public get isTablet() { return this._isTablet; }\n public get isDesktop() { return this._isDesktop; }\n public get isMobile() { return this._isMobile; }\n public get isAndroid() { return this._isAndroid; }\n public get isIOS() { return this._isIOS; }\n\n public async getDeviceId() {\n return await Device.getId();\n }\n\n public async getPersistentSecureDeviceId() {\n let { value } = await SecureStoragePlugin.get({ key: 'deviceId' }).catch(() => ({ value: null }));\n if (!value) {\n let DeviceId = await this.getDeviceId();\n let id = DeviceId.identifier;\n await SecureStoragePlugin.set({ key: 'deviceId', value: id });\n return id;\n }\n return value;\n }\n\n public async getDeviceInfo() {\n return await Device.getInfo();\n }\n\n public async getLanguageCode() {\n return await Device.getLanguageCode();\n }\n}\n","import * as PIXI from \"pixi.js\"\nimport {GameDispatcher} from \"./base/GameDispatcher\";\nimport {GameScreenNavigator} from \"./base/GameScreenNavigator\";\nimport {AbstractApp} from \"../app/AbstractApp\";\nimport {PixiAssetsLoader} from \"./assets\";\nimport {GameJuggler} from \"./base\";\nimport {Rectangle} from \"pixi.js\";\n\nexport type PixiAssetsScaleFactor = 2 | 4;\n\nexport abstract class AbstractPixiRoot extends GameScreenNavigator {\n\n readonly app:AbstractApp;\n readonly pixi: PIXI.Application;\n\n readonly onScreenHierarchyChanged = new GameDispatcher(this);\n readonly webglcontextlost = new GameDispatcher();\n readonly webglcontextrestored = new GameDispatcher();\n\n protected _currentPixiWidth = 0;\n protected _currentPixiHeight = 0;\n\n // 1536 - for scaleFactor 4; 768 - for scaleFactor 2;\n protected _scaleFactor:PixiAssetsScaleFactor = 4;\n protected _fixedStageWidth = 0;\n protected _safeAreaTop = 0;\n protected _safeAreaBottom = 0;\n protected _fixedStageHeight = 0;\n protected _isInitialized:boolean = false;\n\n protected _pixiContainer!:HTMLElement;\n protected _pixiContainerResizeObserver:ResizeObserver | null = null;\n\n protected static _isLoadKTX2Initialized = false;\n\n constructor(app:AbstractApp) {\n super();\n this.root = this;\n this._juggler = new GameJuggler(this, null);\n this.app = app;\n this.pixi = new PIXI.Application();\n }\n\n get isInitialized() {\n return this._isInitialized;\n }\n\n override destroy() {\n super.destroy();\n this.onScreenHierarchyChanged.destroy();\n this._pixiContainerResizeObserver?.unobserve(this._pixiContainer);\n this._pixiContainerResizeObserver?.disconnect();\n this.app.device.onActivateEvent.remove(this.onActivate);\n this.app.device.onDeactivateEvent.remove(this.onDeActivate);\n this.app.device.onExit.remove(this.onExit);\n this.app.device.onWindowResize.remove(this.onResize);\n }\n\n get scaleFactor() {\n return this._scaleFactor;\n }\n\n static multiply(scaleFactor:PixiAssetsScaleFactor) {\n if (scaleFactor == 4) {\n return 1;\n }\n if (scaleFactor == 2) {\n return .5;\n }\n return 1;\n }\n\n get fixedStageWidth() {\n return this._fixedStageWidth;\n }\n\n get fixedStageHeight() {\n return this._fixedStageHeight;\n }\n\n get safeZoneBottom() {\n return this._safeAreaBottom;\n }\n\n get safeZoneTop() {\n return this._safeAreaTop;\n }\n\n get fixedStageSafeHeight() {\n return this._fixedStageHeight - this.safeZoneBottom - this.safeZoneTop;\n }\n\n adapt(size: number) {\n if (this.scaleFactor == 4) {\n return size;\n }\n if (this.scaleFactor == 2) {\n return .5 * size;\n }\n return size;\n }\n\n deAdaptsize(size: number) {\n if (this.scaleFactor == 4)\n return size;\n if (this.scaleFactor == 2)\n return 2 * size;\n return size;\n }\n\n lock() {\n this.interactive = false;\n }\n\n unlock() {\n this.interactive = true;\n }\n \n get uiLib() {\n return this.assetsLoader!.getFlumpLib('ui')!;\n }\n\n get mainTextLib() {\n return this.assetsLoader!.getTextLib('main')!;\n }\n\n get dialogTitleTextLib() {\n return this.assetsLoader!.getTextLib('dialogTitle')!;\n }\n\n public async init(pixiContainer: HTMLElement, options: {\n backgroundColor: number,\n backgroundAlpha: number,\n } = {\n backgroundAlpha: 1,\n backgroundColor: 0xfbf4d9\n }) {\n if (this._isInitialized) {\n return;\n }\n\n this._pixiContainer = pixiContainer;\n\n await this.pixi.init({\n width: this._pixiContainer.clientWidth,\n height: this._pixiContainer.clientHeight,\n backgroundColor: options.backgroundColor,\n backgroundAlpha: options.backgroundAlpha,\n resolution: this.app.device.devicePixelRatio, // влияет только на четкость изображения, оставить так\n autoDensity: true,\n });\n\n this._pixiContainer.appendChild(this.pixi.canvas);\n this.pixi.stage.addChild(this);\n\n // this.pixi.stage.boundsArea = new Rectangle(0, 0, this._pixiContainer.clientWidth, this._pixiContainer.clientHeight);\n // this.boundsArea = new Rectangle(0, 0, this._pixiContainer.clientWidth, this._pixiContainer.clientHeight);\n\n // CONTEXT LOSS HANDLING\n this.pixi.canvas.addEventListener(\"webglcontextlost\", (e) => {\n // window.location.reload();\n this.webglcontextlost.dispatch();\n });\n this.pixi.canvas.addEventListener(\"webglcontextrestored\", (e) => {\n // SandboxPixiRoot.log('CONTEXT RESTORED');\n this.webglcontextrestored.dispatch();\n });\n\n const physicalHeight = window.screen.height * this.app.device.devicePixelRatio;\n if (physicalHeight >= (this.app.device.isAndroid ? 1280 : 1120)) {\n this._scaleFactor = 4;\n this._fixedStageWidth = 1536;\n this._fixedStageHeight = 2048;\n } else {\n this._scaleFactor = 2;\n this._fixedStageWidth = 768;\n this._fixedStageHeight = 1024;\n }\n\n this._pixiContainerResizeObserver = new ResizeObserver((entries) => {\n this.onResize();\n });\n this._pixiContainerResizeObserver.observe(this._pixiContainer);\n this.app.device.onWindowResize.add(this.onResize, this);\n this.onResize();\n\n this.pixi.ticker.minFPS = 60;\n this.pixi.ticker.maxFPS = 60;\n this.pixi.ticker.add((now) => {\n this.juggler?.advanceTime(now.deltaMS / 1000);\n });\n\n if (!AbstractPixiRoot._isLoadKTX2Initialized) {\n AbstractPixiRoot._isLoadKTX2Initialized = true;\n PIXI.extensions.add(\n PIXI.loadKTX2,\n );\n }\n\n this._isInitialized = true;\n }\n\n override onResize() {\n let newWidth = this._pixiContainer.clientWidth;\n let newHeight = this._pixiContainer.clientHeight;\n\n if (this._currentPixiWidth != newWidth || this._currentPixiHeight != newHeight) {\n this._currentPixiWidth = newWidth;\n this._currentPixiHeight = newHeight;\n\n this._safeAreaTop = this.app.device.safeAreaTop;\n this._safeAreaBottom = this.app.device.safeAreaBottom;\n\n // this.pixi.stage.boundsArea.width = newWidth;\n // this.pixi.stage.boundsArea.height = newHeight;\n\n this.pixi.renderer.resize(\n newWidth,\n newHeight\n );\n\n let rootScale = newHeight / this._fixedStageHeight;\n this._fixedStageWidth = newWidth / rootScale;\n this.scale = rootScale;\n\n\n // todo ??? расчитывать четко по краям экрана\n // this.boundsArea.width = this._fixedStageWidth / rootScale;\n // this.boundsArea.height = this._fixedStageHeight / rootScale;\n\n super.onResize();\n }\n }\n \n public createTextureByColor(color:PIXI.Color){\n const graphics = new PIXI.Graphics();\n graphics.beginFill(color); // Красный цвет\n graphics.drawRect(0, 0, 1, 1); // Рисуем прямоугольник 100x100\n graphics.endFill();\n\n const renderTexture = PIXI.RenderTexture.create({ width: 1, height: 1 });\n this.pixi.renderer.render(graphics, { renderTexture });\n return new PIXI.Texture(renderTexture);\n }\n}\n","import {IDestroy} from \"./GameObject\";\nimport {GameJuggler, GameTween, IAnimatable} from \"./GameJuggler\";\nimport * as PIXI from 'pixi.js';\nimport {AbstractPixiRoot} from \"../AbstractPixiRoot\";\nimport {Easing} from \"@tweenjs/tween.js\";\n\nexport class GameView extends PIXI.Container implements IDestroy, IAnimatable {\n\n public root!:AbstractPixiRoot;\n\n constructor(...args: any[]) {\n super();\n // this.interactive = false;\n }\n\n public onResize(): void {\n }\n\n override set interactive(value: boolean) {\n super.interactive = value;\n this.interactiveChildren = value;\n }\n\n protected get pulsatingDisplayObject(): GameView {\n return this;\n }\n\n protected _pulsateTween?: GameTween;\n\n public startPulsating(scaleDiff: number = 0.03, time: number = 0.6) {\n this._pulsateTween?.destroy();\n // console.log(this.pulsatingDisplayObject.scale.x, this.pulsatingDisplayObject.scale.y);\n\n this._pulsateTween = this.juggler.tween(this.pulsatingDisplayObject, 2, {\n scale: {\n y: this.pulsatingDisplayObject.scale.y - scaleDiff,\n x: this.pulsatingDisplayObject.scale.x - scaleDiff * 3,\n }\n })\n .repeat(Infinity)\n .yoyoFix((t: any) => Easing.Linear.InOut(t)).start();\n }\n\n public continuePulsating(): void {\n // Main.log('continuePulsating')\n if (this._pulsateTween) {\n this.juggler.addAnimatable(this._pulsateTween);\n }\n }\n\n public stopPulsating(): void {\n if (this._pulsateTween) {\n this.juggler.removeAnimatable(this._pulsateTween);\n }\n }\n\n protected _juggler?: GameJuggler;\n\n public advanceTime(delta: number) {\n }\n\n addToJuggler(): void {\n this.juggler?.addAnimatable(this);\n }\n\n removeFromJuggler() {\n this.juggler?.removeAnimatable(this);\n }\n\n onRemovedFromJuggler() {\n\n }\n\n public get juggler() {\n return this._juggler || this.root?.juggler;\n }\n\n public set juggler(value: GameJuggler) {\n this._juggler = value;\n }\n\n\n destroyChildren() {\n while (this.children.length > 0) {\n const child = this.removeChildAt(0);\n if ('destroy' in child && typeof child.destroy === 'function') {\n child.destroy({ children: true });\n }\n }\n }\n\n override destroy() {\n if (this.destroyed) {\n return;\n }\n this._pulsateTween?.destroy();\n this.removeFromJuggler();\n this.removeFromParent();\n this.destroyChildren();\n super.destroy({\n children: true\n });\n }\n\n}\n","import {GameJuggler, IAnimatable} from \"./GameJuggler\";\nimport {GamePool, IPoolable} from \"./GamePool\";\nimport {Container} from \"pixi.js/lib/scene/container/Container\";\nimport {GameView} from \"./GameView\";\nimport * as PIXI from 'pixi.js';\n\nexport interface IDestroy {\n destroy(): void;\n get destroyed(): boolean;\n}\n\nconst isIDestroy = (obj: any) => {\n return obj &&\n typeof obj.destroy === 'function' &&\n typeof obj.destroyed === 'boolean';\n}\n\nexport class GameObject implements IDestroy, IPoolable, IAnimatable {\n\n protected _isDestroyed = false;\n\n protected _juggler: GameJuggler | null = null;\n\n public advanceTime(delta: number) {\n }\n\n addToJuggler(): void {\n this.juggler?.addAnimatable(this);\n }\n\n onRemovedFromJuggler() {\n\n }\n\n removeFromJuggler() {\n this.juggler?.removeAnimatable(this);\n }\n\n public get juggler(): GameJuggler | null {\n return this._juggler;\n }\n\n public set juggler(value: GameJuggler) {\n this._juggler = value;\n }\n\n public get destroyed(): boolean {\n return this._isDestroyed;\n }\n\n static destroy(object: any) {\n if (!object) {\n return null;\n }\n\n // first check on interfaces\n\n if (object instanceof GameObject || object instanceof GameView) {\n (object as IDestroy).destroy();\n return null;\n }\n\n if (isIDestroy(object)) {\n (object as IDestroy).destroy();\n return null;\n }\n\n // second check on implementations\n\n if (Array.isArray(object)) {\n const objects = object as Object[] | null; // Проверяем, является ли объект массивом\n if (objects) {\n while (objects.length) {\n this.destroy(objects.pop()!); // Вызываем dispose для каждого элемента\n }\n }\n return null;\n }\n\n if (object instanceof PIXI.Container) {\n let container = object as Container;\n container.removeFromParent();\n container.destroy({children: true});\n }\n\n return null;\n }\n\n public onReturnToPool(): void {\n }\n\n public onGetFromPool(pool: GamePool): void {\n }\n\n public destroy(): void {\n if (!this._isDestroyed) {\n this._isDestroyed = true;\n this.removeFromJuggler();\n }\n }\n\n}\n","import {GameObject} from \"./GameObject\";\n\nexport type GameDispatcherListener = (sender?: GameObject | null, argument?: any | null) => void;\n\nexport class GameDispatcher extends GameObject {\n private _sender?: any | null;\n private _listeners?: Map<GameDispatcherListener, GameDispatcherListener> | null;\n\n constructor(sender?: any) {\n super();\n this._sender = sender;\n this._listeners = new Map();\n }\n\n public add(listener: GameDispatcherListener, context: any): void {\n if (this._isDestroyed || !listener) {\n return;\n }\n\n if (!this._listeners!.has(listener)) {\n // Привязываем к переданному контексту (если есть) или оставляем оригинал\n const boundListener = context ? listener.bind(context) : listener;\n this._listeners!.set(listener, boundListener);\n }\n }\n\n public remove(listener: GameDispatcherListener): void {\n if (this.destroyed || !listener) {\n return;\n }\n\n // Удаляем оригинальный и привязанный вариант\n if (this._listeners!.has(listener)) {\n this._listeners!.delete(listener);\n }\n }\n\n public dispatch(argument?: any): void {\n if (this._isDestroyed || !this._listeners) {\n return;\n }\n\n // Вызываем привязанные функции\n for (const [, boundListener] of this._listeners) {\n boundListener(this._sender, argument);\n }\n }\n\n public hasListener(listener: GameDispatcherListener): boolean {\n if (this.destroyed || !listener) {\n return false;\n }\n\n return this._listeners!.has(listener);\n }\n\n public removeAllListeners(): void {\n if (this._listeners) {\n this._listeners.clear();\n }\n }\n\n override destroy(): void {\n if (this._listeners) {\n this._listeners.clear();\n this._listeners = null;\n }\n this._sender = null;\n super.destroy();\n }\n}\n","import {GameView} from \"./GameView\";\nimport * as PIXI from \"pixi.js\";\nimport {BackgroundEffect} from \"../effects/BackgroundEffect\";\nimport {GameObject} from \"./GameObject\";\nimport {RectangleUtils, ScaleMode} from \"../utils/RectangleUtils\";\nimport {PixiAssetsLoader} from \"../assets/PixiAssetsLoader\";\n\nexport class GameScreen extends GameView {\n\n public assetsLoader: PixiAssetsLoader | null = null;\n \n protected _bgImage?: PIXI.Sprite | null = null;\n protected _bgParticleEffect: BackgroundEffect | null = null;\n protected _isAdded = false;\n\n constructor() {\n super();\n // this.interactive = true;\n }\n\n public getAreaForHtml(){\n return this.boundsArea;\n }\n\n public get screenID() {\n return \"GameScreen\";\n }\n\n override destroy() {\n super.destroy();\n this._bgImage = GameObject.destroy(this._bgImage);\n this._bgParticleEffect = GameObject.destroy(this._bgParticleEffect);\n this.assetsLoader = GameObject.destroy(this.assetsLoader);\n }\n\n public async onAdded() {\n this._isAdded = true;\n }\n\n public getStateDescription() {\n return \"GameScreen: \";\n }\n\n public async loadAssets(onLoad?: () => void) {\n if (onLoad) {\n onLoad();\n }\n }\n\n public createBg() {\n this._bgImage = new PIXI.Sprite(this.getBgTexture());\n this._bgImage.interactive = false;\n\n const launchImageSize = RectangleUtils.fitRectangle(\n new PIXI.Rectangle(0, 0, this._bgImage!.width, this._bgImage!.height),\n new PIXI.Rectangle(0, 0, this.root.fixedStageWidth, this.root.fixedStageHeight),\n ScaleMode.NO_BORDER\n );\n\n this._bgImage!.width = launchImageSize.width;\n this._bgImage!.height = launchImageSize.height;\n this.addChild(this._bgImage!);\n }\n\n protected getBgTexture() {\n return this.assetsLoader?.getTextureByName('bg');\n }\n\n protected createBgEffect(): void {\n this._bgParticleEffect = new BackgroundEffect(this.root, 80);\n this.addChild(this._bgParticleEffect)\n }\n\n public onActivate(): void {\n }\n\n public onDeActivate(): void {\n }\n\n public onExit(): void {\n }\n\n}\n","import {GameObject} from \"../base/GameObject\";\nimport {GameView} from \"../base/GameView\";\nimport {AbstractPixiRoot} from \"../AbstractPixiRoot\";\nimport * as PIXI from \"pixi.js\";\nimport {MathUtils} from \"../../utils/MathUtils\";\n\nexport class BackgroundEffectParticle extends GameView {\n\n protected _dx: number = 0;\n protected _dy: number = 0;\n protected _rotSpeed: number = 0;\n\n constructor(root:AbstractPixiRoot, layerNum: number) {\n super();\n this.root = root;\n\n let image: PIXI.Sprite = this.createParticleImage();\n image.anchor.set(0.5);\n this.addChild(image);\n\n this.x = MathUtils.getRandomInt(0, this.root.fixedStageWidth);\n this.y = MathUtils.getRandomInt(-32 * this.root.scaleFactor / 4, this.root.fixedStageHeight);\n\n let minSpeed = 0;\n let maxSpeed = 0;\n\n switch (layerNum) {\n case 1:\n this.scale = MathUtils.getRandomInt(20, 30) * .01;\n // this.alpha = .4;\n minSpeed = 3;\n maxSpeed = 4;\n break;\n case 2:\n this.scale = MathUtils.getRandomInt(40, 60) * .01;\n // this.alpha = .7;\n minSpeed = 4;\n maxSpeed = 6;\n break;\n case 3:\n this.scale = MathUtils.getRandomInt(70, 80) * .01;\n minSpeed = 7;\n maxSpeed = 9;\n break;\n }\n\n if (layerNum == 2 || layerNum == 3) {\n if (MathUtils.getRandomInt(0, 1) == 1) {\n this._rotSpeed = MathUtils.deg2rad(.1 * MathUtils.getRandomInt(10, 20));\n } else {\n this._rotSpeed = MathUtils.deg2rad(.1 * MathUtils.getRandomInt(-20, -10));\n }\n }\n\n minSpeed *= (this.root.scaleFactor / 4);\n maxSpeed *= (this.root.scaleFactor / 4);\n\n let fallSpeed = 0.75;\n let speed = MathUtils.getRandomInt(minSpeed, maxSpeed) * fallSpeed;\n let angle = MathUtils.getRandomInt(250, 290);\n\n this._dx = speed * Math.cos((180 - angle) * Math.PI / 180);\n this._dy = -speed * Math.sin((180 - angle) * Math.PI / 180);\n }\n\n protected get horizontalForce() {\n return 0;\n }\n\n public update() {\n this.x += (this._dx + this.horizontalForce * this.scale.x);\n this.y += this._dy;\n\n if (this._rotSpeed != 0) {\n this.rotation += this._rotSpeed;\n }\n\n if (this.y > this.root.fixedStageHeight || this.x > this.root.fixedStageWidth || this.x < 0) {\n this.x = MathUtils.getRandomInt(0, this.root.fixedStageWidth);\n this.y = -32 * this.root.scaleFactor / 4;\n }\n }\n\n protected createParticleImage() {\n return new PIXI.Sprite(this.root.uiLib.getTexture(\"petal\"));\n }\n}\n\nexport class BackgroundEffect extends GameView {\n\n private _particles: BackgroundEffectParticle[] = [];\n\n constructor(root:AbstractPixiRoot, particlesCount: number) {\n super();\n this.root = root;\n\n let particle: BackgroundEffectParticle;\n let distribution = this.distribution();\n let layer1Count = distribution[0] * particlesCount * .01;\n let layer2Count = distribution[1] * particlesCount * .01;\n\n for (let i = 0; i < particlesCount; i++) {\n if (i < layer1Count)\n particle = this.createParticle(1);\n else if (i > layer1Count && i < layer1Count + layer2Count)\n particle = this.createParticle(2);\n else\n particle = this.createParticle(3);\n\n this.addChild(particle);\n this._particles.push(particle);\n }\n\n this.addToJuggler();\n }\n\n override advanceTime(): void {\n for (let i = 0; i < this._particles.length; i++) {\n this._particles[i].update();\n }\n }\n\n override destroy() {\n GameObject.destroy(this._particles);\n super.destroy();\n }\n\n protected createParticle(layer: number) {\n return new BackgroundEffectParticle(this.root, layer);\n }\n\n protected distribution() {\n return [30, 40, 50];\n }\n}\n","import {Point} from \"pixi.js\";\n\nexport class MathUtils {\n static getMidPoint(startX: number, startY: number, endX: number, endY: number): { x: number; y: number } {\n const distance = MathUtils.getDistance(startX, startY, endX, endY);\n const point = {\n x: (startX + endX) / 2,\n y: (startY + endY) / 2,\n };\n\n if (MathUtils.getRandomInt(0, 1) === 0) {\n point.x += 0.01 * MathUtils.getRandomInt(10, 20) * distance;\n } else {\n point.x -= 0.01 * MathUtils.getRandomInt(10, 20) * distance;\n }\n\n return point;\n }\n\n static getRandomElementOf<T>(array: T[]): T {\n const idx = Math.floor(Math.random() * array.length);\n return array[idx];\n }\n\n static toArray<T>(iterable: Iterable<T>): T[] {\n return Array.from(iterable);\n }\n\n static isEvenInt(value: number): boolean {\n return value % 2 === 0;\n }\n\n static getDistance(x1: number, y1: number, x2: number, y2: number): number {\n return Math.sqrt((y2 - y1) ** 2 + (x2 - x1) ** 2);\n }\n\n static getRandomInt(fromNum: number, toNum: number): number {\n if (fromNum === toNum) {\n return fromNum;\n }\n return Math.floor(fromNum + Math.random() * (toNum - fromNum + 1));\n }\n\n static getRandomNumber(fromNum: number, toNum: number): number {\n if (fromNum === toNum) {\n return fromNum;\n }\n return fromNum + Math.random() * (toNum - fromNum);\n }\n\n static getAngle(x1: number, y1: number, x2: number, y2: number): number {\n if (x2 !== x1) {\n let angle = (180 / Math.PI) * Math.atan((y2 - y1) / (x2 - x1));\n if (x1 < x2) angle += 180;\n if (angle < 0) angle += 360;\n return angle;\n } else {\n return y1 < y2 ? 270 : 90;\n }\n }\n\n static generateRandomArray(length: number): number[] {\n const array = Array.from({ length }, (_, i) => i);\n\n for (let i = 0; i < length; i++) {\n const r = MathUtils.generateRandom(0, length);\n [array[i], array[r]] = [array[r], array[i]];\n }\n\n return array;\n }\n\n static generateRandom(min: number, max: number): number {\n if (min === max) return min;\n if (max > min) return MathUtils.randomInt() % (max - min) + min;\n return MathUtils.randomInt() % (min - max) + max;\n }\n\n static randomBoolean(): boolean {\n return MathUtils.randomInt() % 2 === 0;\n }\n\n static chance(chance: number): boolean {\n if (chance < 2) return true;\n return MathUtils.randomInt() % chance === 0;\n }\n\n static chooseWeighedItem(weights: number[]): number {\n const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);\n if (totalWeight === 0) return 0;\n\n let randomWeight = MathUtils.randomInt() % totalWeight;\n for (let i = 0; i < weights.length; i++) {\n randomWeight -= weights[i];\n if (randomWeight < 0) return i;\n }\n\n return 0; // Этот случай никогда не должен быть достигнут\n }\n\n static randomInt(): number {\n return Math.round(Math.random() * Number.MAX_SAFE_INTEGER);\n }\n\n static DEG_TO_RAD = Math.PI / 180;\n static deg2rad(degrees: number): number {\n return degrees * MathUtils.DEG_TO_RAD;\n }\n\n static int(num: number) {\n return Math.floor(num);\n }\n\n static float(num: number, decimals: number): number {\n const multiplier = Math.pow(10, decimals); // Вычисляем 10 в степени decimals\n return Math.round(num * multiplier) / multiplier;\n }\n\n static distance(point1: Point | null, point2: Point | null) {\n if (point1 && point2) {\n const dx = point2.x - point1.x;\n const dy = point2.y - point1.y;\n return Math.sqrt(dx * dx + dy * dy);\n }\n return 0;\n }\n}\n","import * as PIXI from \"pixi.js\";\n\nexport enum ScaleMode {\n SHOW_ALL = \"SHOW_ALL\",\n NO_BORDER = \"NO_BORDER\",\n}\nexport class RectangleUtils {\n static fitRectangle(\n source: PIXI.Rectangle,\n target: PIXI.Rectangle,\n scaleMode: ScaleMode\n ): PIXI.Rectangle {\n const sourceAspect = source.width / source.height;\n const targetAspect = target.width / target.height;\n\n let scale: number;\n\n if (scaleMode === ScaleMode.NO_BORDER) {\n scale = sourceAspect > targetAspect ? target.height / source.height : target.width / source.width;\n } else {\n // Default to SHOW_ALL\n scale = sourceAspect > targetAspect ? target.width / source.width : target.height / source.height;\n }\n\n const width = source.width * scale;\n const height = source.height * scale;\n const x = (target.width - width) / 2;\n const y = (target.height - height) / 2;\n\n return new PIXI.Rectangle(x, y, width, height);\n }\n}\n","import * as PIXI from 'pixi.js';\nimport {Color, Graphics} from 'pixi.js';\nimport {AbstractText} from \"pixi.js/lib/scene/text/AbstractText\";\n\nexport enum Align {\n LEFT = 'left',\n CENTER = 'center',\n RIGHT = 'right',\n TOP = 'top',\n BOTTOM = 'bottom'\n}\n\n\n\nexport class ViewUtils {\n\n // Функция возвращает видимое расстояние между двумя контейнерами\n static getVisibleGap(containerA: PIXI.Container, containerB: PIXI.Container): { horizontal: number; vertical: number } {\n // Получаем глобальные границы контейнеров\n const boundsA = containerA.getBounds();\n const boundsB = containerB.getBounds();\n\n // Вычисляем горизонтальное расстояние\n let horizontalGap = 0;\n if (boundsA.right < boundsB.left) {\n // A левее B\n horizontalGap = boundsB.left - boundsA.right;\n } else if (boundsB.right < boundsA.left) {\n // B левее A\n horizontalGap = boundsA.left - boundsB.right;\n }\n // Если пересекаются по горизонтали, gap = 0\n\n // Вычисляем вертикальное расстояние\n let verticalGap = 0;\n if (boundsA.bottom < boundsB.top) {\n // A выше B\n verticalGap = boundsB.top - boundsA.bottom;\n } else if (boundsB.bottom < boundsA.top) {\n // B выше A\n verticalGap = boundsA.top - boundsB.bottom;\n }\n // Если пересекаются по вертикали, gap = 0\n\n return {\n horizontal: horizontalGap,\n vertical: verticalGap,\n };\n }\n\n// Функция для выравнивания pivot\n static alignPivot(displayObject: PIXI.Container | null | undefined, horizont