From d4d0a07c8515820f9ee41bd04d496191ed04e1f3 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 22 Apr 2021 19:20:18 +0800 Subject: [PATCH] fixed documentation and types --- dist/cjs/viewport.js | 59 ++++++-- dist/cjs/viewport.js.map | 2 +- dist/esm/viewport.es.js | 59 ++++++-- dist/esm/viewport.es.js.map | 2 +- dist/viewport.min.js | 59 ++++++-- dist/viewport.min.js.map | 2 +- dist/viewport.min.min.js | 2 +- dist/viewport.min.min.js.map | 2 +- index.d.ts | 9 +- src/Viewport.ts | 282 +++++++++++++++++++++++++---------- src/plugins/ClampZoom.ts | 65 ++++++-- test/clamp-zoom.js | 203 +++++++++++++++---------- test/follow.js | 163 ++++++++++---------- test/mouse-edges.js | 76 +++++----- 14 files changed, 671 insertions(+), 314 deletions(-) diff --git a/dist/cjs/viewport.js b/dist/cjs/viewport.js index 94453909..fd9a3616 100644 --- a/dist/cjs/viewport.js +++ b/dist/cjs/viewport.js @@ -2,7 +2,7 @@ /*! * pixi-viewport - v4.30.0 - * Compiled Thu, 22 Apr 2021 09:16:32 UTC + * Compiled Thu, 22 Apr 2021 10:41:51 UTC * * pixi-viewport is licensed under the MIT License. * http://www.opensource.org/licenses/mit-license @@ -1143,7 +1143,7 @@ class ClampZoom extends Plugin this.clamp(); } - /** Clamp the viewport's zoom immediately. */ + /** Clamp the viewport scale zoom) */ clamp() { if (this.paused) @@ -1196,20 +1196,59 @@ class ClampZoom extends Plugin } } else + if (this.options.minScale || this.options.maxScale) { - let scale = this.parent.scale.x; + const minScale = { x: null, y: null }; + const maxScale = { x: null, y: null }; - if (this.options.minScale !== null && scale < this.options.minScale) + if (typeof this.options.minScale === 'number') { - scale = this.options.minScale; + minScale.x = this.options.minScale; + minScale.y = this.options.minScale; } - if (this.options.maxScale !== null && scale > this.options.maxScale) + else if (this.options.minScale !== null) { - scale = this.options.maxScale; + const optsMinScale = this.options.minScale ; + + minScale.x = typeof optsMinScale.x === 'undefined' ? null : optsMinScale.x; + minScale.y = typeof optsMinScale.y === 'undefined' ? null : optsMinScale.y; + } + + if (typeof this.options.maxScale === 'number') + { + maxScale.x = this.options.maxScale; + maxScale.y = this.options.maxScale; + } + else if (this.options.maxScale !== null) + { + const optsMaxScale = this.options.maxScale ; + + maxScale.x = typeof optsMaxScale.x === 'undefined' ? null : optsMaxScale.x; + maxScale.y = typeof optsMaxScale.y === 'undefined' ? null : optsMaxScale.y; + } + + let scaleX = this.parent.scale.x; + let scaleY = this.parent.scale.y; + + if (minScale.x !== null && scaleX < minScale.x) + { + scaleX = minScale.x; } - if (scale !== this.parent.scale.x) + if (maxScale.x !== null && scaleX > maxScale.x) { - this.parent.scale.set(scale); + scaleX = maxScale.x; + } + if (minScale.y !== null && scaleY < minScale.y) + { + scaleY = minScale.y; + } + if (maxScale.y !== null && scaleY > maxScale.y) + { + scaleY = maxScale.y; + } + if (scaleX !== this.parent.scale.x || scaleY !== this.parent.scale.y) + { + this.parent.scale.set(scaleX, scaleY); this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' }); } } @@ -1221,6 +1260,8 @@ class ClampZoom extends Plugin } } +/** This allows independent x and y values for min/maxScale */ + const DEFAULT_DECELERATE_OPTIONS = { friction: 0.98, bounce: 0.8, diff --git a/dist/cjs/viewport.js.map b/dist/cjs/viewport.js.map index 96e2594d..373b71e5 100644 --- a/dist/cjs/viewport.js.map +++ b/dist/cjs/viewport.js.map @@ -1 +1 @@ -{"version":3,"file":"viewport.js","sources":["../../src/plugins/Plugin.ts","../../src/ease.ts","../../src/plugins/Animate.ts","../../src/plugins/Bounce.ts","../../src/plugins/Clamp.ts","../../src/plugins/ClampZoom.ts","../../src/plugins/Decelerate.ts","../../src/plugins/Drag.ts","../../src/plugins/Follow.ts","../../src/plugins/MouseEdges.ts","../../src/plugins/Pinch.ts","../../src/plugins/Snap.ts","../../src/plugins/SnapZoom.ts","../../src/plugins/Wheel.ts","../../src/InputManager.ts","../../src/PluginManager.ts","../../src/Viewport.ts"],"sourcesContent":["import type { InteractionEvent } from '@pixi/interaction';\nimport type { Viewport } from '../Viewport';\n\n/**\n * Derive this class to create user-defined plugins\n *\n * @public\n */\nexport class Plugin\n{\n /** The viewport to which this plugin is attached. */\n public readonly parent: Viewport;\n\n /**\n * Flags whether this plugin has been \"paused\".\n *\n * @see Plugin#pause\n * @see Plugin#resume\n */\n public paused: boolean;\n\n /** @param {Viewport} parent */\n constructor(parent: Viewport)\n {\n this.parent = parent;\n this.paused = false;\n }\n\n /** Called when plugin is removed */\n public destroy()\n {\n // Override for implementation\n }\n\n /** Handler for pointerdown PIXI event */\n public down(_e: InteractionEvent): boolean\n {\n return false;\n }\n\n /** Handler for pointermove PIXI event */\n public move(_e: InteractionEvent): boolean\n {\n return false;\n }\n\n /** Handler for pointerup PIXI event */\n public up(_e: InteractionEvent): boolean\n {\n return false;\n }\n\n /** Handler for wheel event on div */\n public wheel(_e: WheelEvent): boolean | undefined\n {\n return false;\n }\n\n /**\n * Called on each tick\n * @param {number} elapsed time in millisecond since last update\n */\n public update(_delta: number): void\n {\n // Override for implementation\n }\n\n /** Called when the viewport is resized */\n public resize()\n {\n // Override for implementation\n }\n\n /** Called when the viewport is manually moved */\n public reset(): void\n {\n // Override for implementation\n }\n\n /** Pause the plugin */\n public pause(): void\n {\n this.paused = true;\n }\n\n /** Un-pause the plugin */\n public resume(): void\n {\n this.paused = false;\n }\n}\n","// eslint-disable-next-line\n// @ts-expect-error Penner seems to have no typings.\nimport Penner from 'penner';\n\n/**\n * Returns correct Penner equation using string or Function.\n *\n * @internal\n * @ignore\n * @param {(function|string)} [ease]\n * @param {defaults} default penner equation to use if none is provided\n */\nexport default function ease(ease: any, defaults?: any): any\n{\n if (!ease)\n {\n return Penner[defaults]\n }\n else if (typeof ease === 'function')\n {\n return ease\n }\n else if (typeof ease === 'string')\n {\n return Penner[ease]\n }\n}","import { Point } from '@pixi/math';\nimport { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Animate}. */\nexport interface IAnimateOptions {\n /** Time to animate */\n time?: number;\n\n /** Position to move the viewport to */\n position?: Point;\n\n /**\n * Desired viewport width in world pixels\n *\n * (use instead of scale; aspect ratio is maintained if height is not provided)\n */\n width?: number;\n\n /**\n * Desired viewport height in world pixels\n *\n * (use instead of scale; aspect ratio is maintained if width is not provided)\n */\n height?: number;\n\n /** Scale to change zoom (scale.x = scale.y) */\n scale?: number;\n\n /** Independently change zoom in x-direction */\n scaleX?: number;\n\n /** Independently change zoom in y-direction */\n scaleY?: number;\n\n /** Easing function to use */\n ease?: any;\n\n /** Callback to invoke when the animation completes */\n callbackOnComplete?: (viewport: Viewport) => void;\n\n /** Removes this plugin if interrupted by any user input */\n removeOnInterrupt?: boolean;\n}\n\nconst DEFAULT_ANIMATE_OPTIONS = {\n removeOnInterrupt: false,\n ease: 'linear',\n time: 1000,\n};\n\n/**\n * Animation plugin.\n *\n * @see Viewport#animate\n * @fires animate-end\n */\nexport class Animate extends Plugin\n{\n public readonly options: IAnimateOptions & { ease: any; time: number };\n\n /** The starting x-coordinate of the viewport. */\n protected startX?: number;\n\n /** The starting y-coordinate of the viewport. */\n protected startY?: number;\n\n /** The change in the x-coordinate of the viewport through the animation.*/\n protected deltaX?: number;\n\n /** The change in the y-coordinate of the viewport through the animation. */\n protected deltaY?: number;\n\n /** Marks whether the center of the viewport is preserved in the animation. */\n protected keepCenter!: boolean;\n\n /** The starting viewport width. */\n protected startWidth: number | null = null;\n\n /** The starting viewport height. */\n protected startHeight: number | null = null;\n\n /** The change in the viewport's width through the animation. */\n protected deltaWidth: number | null = null;\n\n /** The change in the viewport's height through the animation. */\n protected deltaHeight: number | null = null;\n\n /** The viewport's width post-animation. */\n protected width: number | null = null;\n\n /** The viewport's height post-animation. */\n protected height: number | null = null;\n\n /** The time since the animation started. */\n protected time = 0;\n\n /**\n * This is called by {@link Viewport.animate}.\n *\n * @param parent\n * @param options\n */\n constructor(parent: Viewport, options: IAnimateOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_ANIMATE_OPTIONS, options);\n this.options.ease = ease(this.options.ease);\n\n this.setupPosition();\n this.setupZoom();\n\n this.time = 0;\n }\n\n /**\n * Setup `startX`, `startY`, `deltaX`, `deltaY`, `keepCenter`.\n *\n * This is called during construction.\n */\n protected setupPosition(): void\n {\n if (typeof this.options.position !== 'undefined')\n {\n this.startX = this.parent.center.x;\n this.startY = this.parent.center.y;\n this.deltaX = this.options.position.x - this.parent.center.x;\n this.deltaY = this.options.position.y - this.parent.center.y;\n this.keepCenter = false;\n }\n else\n {\n this.keepCenter = true;\n }\n }\n\n /**\n * Setup `startWidth, `startHeight`, `deltaWidth, `deltaHeight, `width`, `height`.\n *\n * This is called during construction.\n */\n protected setupZoom(): void\n {\n this.width = null;\n this.height = null;\n\n if (typeof this.options.scale !== 'undefined')\n {\n this.width = this.parent.screenWidth / this.options.scale;\n }\n else if (typeof this.options.scaleX !== 'undefined' || typeof this.options.scaleY !== 'undefined')\n {\n if (typeof this.options.scaleX !== 'undefined')\n {\n // screenSizeInWorldPixels = screenWidth / scale\n this.width = this.parent.screenWidth / this.options.scaleX;\n }\n if (typeof this.options.scaleY !== 'undefined')\n {\n this.height = this.parent.screenHeight / this.options.scaleY;\n }\n }\n else\n {\n if (typeof this.options.width !== 'undefined')\n {\n this.width = this.options.width;\n }\n if (typeof this.options.height !== 'undefined')\n {\n this.height = this.options.height;\n }\n }\n\n if (this.width !== null)\n {\n this.startWidth = this.parent.screenWidthInWorldPixels;\n this.deltaWidth = this.width - this.startWidth;\n }\n if (this.height !== null)\n {\n this.startHeight = this.parent.screenHeightInWorldPixels;\n this.deltaHeight = this.height - this.startHeight;\n }\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('animate');\n }\n\n return false;\n }\n\n public complete(): void\n {\n this.parent.plugins.remove('animate');\n if (this.width !== null)\n {\n this.parent.fitWidth(this.width, this.keepCenter, this.height === null);\n }\n if (this.height !== null)\n {\n this.parent.fitHeight(this.height, this.keepCenter, this.width === null);\n }\n if (!this.keepCenter)\n {\n this.parent.moveCenter(this.options.position as Point);\n }\n\n this.parent.emit('animate-end', this.parent);\n\n if (this.options.callbackOnComplete)\n {\n this.options.callbackOnComplete(this.parent);\n }\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n this.time += elapsed;\n\n if (this.time >= this.options.time)\n {\n this.complete();\n }\n else\n {\n const originalZoom = new Point(this.parent.scale.x, this.parent.scale.y);\n const percent = this.options.ease(this.time, 0, 1, this.options.time);\n\n if (this.width !== null)\n {\n const startWidth = this.startWidth as number;\n const deltaWidth = this.deltaWidth as number;\n\n this.parent.fitWidth(\n startWidth + (deltaWidth * percent),\n this.keepCenter,\n this.height === null);\n }\n if (this.height !== null)\n {\n const startHeight = this.startHeight as number;\n const deltaHeight = this.deltaHeight as number;\n\n this.parent.fitHeight(\n startHeight + (deltaHeight * percent),\n this.keepCenter,\n this.width === null);\n }\n if (this.width === null)\n {\n this.parent.scale.x = this.parent.scale.y;\n }\n else if (this.height === null)\n {\n this.parent.scale.y = this.parent.scale.x;\n }\n if (!this.keepCenter)\n {\n const startX = this.startX as number;\n const startY = this.startY as number;\n const deltaX = this.deltaX as number;\n const deltaY = this.deltaY as number;\n const original = new Point(this.parent.x, this.parent.y);\n\n this.parent.moveCenter(startX + (deltaX * percent), startY + (deltaY * percent));\n this.parent.emit('moved', { viewport: this.parent, original, type: 'animate' });\n }\n if (this.width || this.height)\n {\n this.parent.emit('zoomed', { viewport: this.parent, original: originalZoom, type: 'animate' });\n }\n }\n }\n}\n","import { Point, Rectangle } from '@pixi/math';\nimport { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Drag } from './Drag';\nimport type { IDecelerateOptions } from './Decelerate';\nimport type { Pinch } from './Pinch';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Bounce}. */\nexport interface IBounceOptions {\n /** \"all\", \"horizontal\", \"vertical\", or combination of \"top\", \"bottom\", \"right\", \"left\" (e.g., 'top-bottom-right') */\n sides?:\n 'all'\n | 'horizontal'\n | 'vertical'\n | string;\n\n /** Friction to apply to decelerate if active */\n friction?: number;\n\n /** Time in ms to finish bounce */\n time?: number;\n\n /** Use this bounceBox instead of (0, 0, viewport.worldWidth, viewport.worldHeight) */\n bounceBox?: Rectangle | null;\n\n /** Ease function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /** (top/bottom/center and left/right/center, or center) where to place world if too small for screen */\n underflow?: 'center' | string;\n}\n\n/** Bounce state along an axis */\nexport interface IBounceState {\n /** Elapsed time since bounce started */\n time: number;\n\n /** Starting coordinate */\n start: number;\n\n /** Change in coordinate through bounce */\n delta: number;\n\n /** Ending coordinate */\n end: number;\n}\n\nconst DEFAULT_BOUNCE_OPTIONS: Required = {\n sides: 'all',\n friction: 0.5,\n time: 150,\n ease: 'easeInOutSine',\n underflow: 'center',\n bounceBox: null\n};\n\n/**\n * @fires bounce-start-x\n * @fires bounce.end-x\n * @fires bounce-start-y\n * @fires bounce-end-y\n * @public\n */\nexport class Bounce extends Plugin\n{\n /** The options passed to initialize this plugin, cannot be modified again. */\n public readonly options: Readonly>;\n\n /** Holds whether to bounce from left side. */\n public readonly left: boolean ;\n\n /** Holds whether to bounce from top side. */\n public readonly top: boolean;\n\n /** Holds whether to bounce from right side. */\n public readonly right: boolean;\n\n /** Holds whether to bounce from bottom side. */\n public readonly bottom: boolean;\n\n /** Direction of underflow along x-axis. */\n public readonly underflowX: -1 | 0 | 1;\n\n /** Direction of underflow along y-axis. */\n public readonly underflowY: -1 | 0 | 1;\n\n /** Easing */\n protected ease: any;\n\n /** Bounce state along x-axis */\n protected toX!: IBounceState | null;\n\n /** Bounce state along y-axis */\n protected toY!: IBounceState | null;\n\n /**\n * This is called by {@link Viewport.bounce}.\n */\n constructor(parent: Viewport, options: IBounceOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_BOUNCE_OPTIONS, options);\n this.ease = ease(this.options.ease, 'easeInOutSine');\n\n if (this.options.sides)\n {\n if (this.options.sides === 'all')\n {\n this.top = this.bottom = this.left = this.right = true;\n }\n else if (this.options.sides === 'horizontal')\n {\n this.right = this.left = true;\n this.top = this.bottom = false;\n }\n else if (this.options.sides === 'vertical')\n {\n this.left = this.right = false;\n this.top = this.bottom = true;\n }\n else\n {\n this.top = this.options.sides.indexOf('top') !== -1;\n this.bottom = this.options.sides.indexOf('bottom') !== -1;\n this.left = this.options.sides.indexOf('left') !== -1;\n this.right = this.options.sides.indexOf('right') !== -1;\n }\n } else {\n this.left = this.top = this.right = this.bottom = false;\n }\n\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'center')\n {\n this.underflowX = 0;\n this.underflowY = 0;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n }\n\n this.reset();\n }\n\n public isActive(): boolean\n {\n return this.toX !== null || this.toY !== null;\n }\n\n public down(): boolean\n {\n this.toX = this.toY = null;\n\n return false;\n }\n\n public up(): boolean\n {\n this.bounce();\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n this.bounce();\n\n if (this.toX)\n {\n const toX = this.toX;\n\n toX.time += elapsed;\n this.parent.emit('moved', { viewport: this.parent, type: 'bounce-x' });\n\n if (toX.time >= this.options.time)\n {\n this.parent.x = toX.end;\n this.toX = null;\n this.parent.emit('bounce-x-end', this.parent);\n }\n else\n {\n this.parent.x = this.ease(toX.time, toX.start, toX.delta, this.options.time);\n }\n }\n\n if (this.toY)\n {\n const toY = this.toY;\n\n toY.time += elapsed;\n this.parent.emit('moved', { viewport: this.parent, type: 'bounce-y' });\n\n if (toY.time >= this.options.time)\n {\n this.parent.y = toY.end;\n this.toY = null;\n this.parent.emit('bounce-y-end', this.parent);\n }\n else\n {\n this.parent.y = this.ease(toY.time, toY.start, toY.delta, this.options.time);\n }\n }\n }\n\n /** @internal */\n protected calcUnderflowX(): number\n {\n let x: number;\n\n switch (this.underflowX)\n {\n case -1:\n x = 0;\n break;\n case 1:\n x = (this.parent.screenWidth - this.parent.screenWorldWidth);\n break;\n default:\n x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n }\n\n return x;\n }\n\n /** @internal */\n protected calcUnderflowY(): number\n {\n let y: number;\n\n switch (this.underflowY)\n {\n case -1:\n y = 0;\n break;\n case 1:\n y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n break;\n default:\n y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n }\n\n return y;\n }\n\n private oob(): Record<'left' | 'right' | 'top' | 'bottom', boolean> & Record<'topLeft' | 'bottomRight', Point>\n {\n const box = this.options.bounceBox;\n\n if (box)\n {\n const x1 = typeof box.x === 'undefined' ? 0 : box.x;\n const y1 = typeof box.y === 'undefined' ? 0 : box.y;\n const width = typeof box.width === 'undefined' ? this.parent.worldWidth : box.width;\n const height = typeof box.height === 'undefined' ? this.parent.worldHeight : box.height;\n\n return {\n left: this.parent.left < x1,\n right: this.parent.right > width,\n top: this.parent.top < y1,\n bottom: this.parent.bottom > height,\n topLeft: new Point(\n x1 * this.parent.scale.x,\n y1 * this.parent.scale.y\n ),\n bottomRight: new Point(\n width * this.parent.scale.x - this.parent.screenWidth,\n height * this.parent.scale.y - this.parent.screenHeight\n )\n };\n }\n\n return {\n left: this.parent.left < 0,\n right: this.parent.right > this.parent.worldWidth,\n top: this.parent.top < 0,\n bottom: this.parent.bottom > this.parent.worldHeight,\n topLeft: new Point(0, 0),\n bottomRight: new Point(\n this.parent.worldWidth * this.parent.scale.x - this.parent.screenWidth,\n this.parent.worldHeight * this.parent.scale.y - this.parent.screenHeight\n )\n };\n }\n\n public bounce(): void\n {\n if (this.paused)\n {\n return;\n }\n\n let oob;\n let decelerate: undefined | null | {\n percentChangeX?: number;\n percentChangeY?: number;\n x?: number;\n y?: number;\n options?: IDecelerateOptions\n } = this.parent.plugins.get('decelerate', true) as any;\n\n if (decelerate && (decelerate.x || decelerate.y))\n {\n if ((decelerate.x && decelerate.percentChangeX === decelerate.options?.friction) || (decelerate.y && decelerate.percentChangeY === decelerate.options?.friction))\n {\n oob = this.oob();\n if ((oob.left && this.left) || (oob.right && this.right))\n {\n decelerate.percentChangeX = this.options.friction;\n }\n if ((oob.top && this.top) || (oob.bottom && this.bottom))\n {\n decelerate.percentChangeY = this.options.friction;\n }\n }\n }\n const drag: Partial = this.parent.plugins.get('drag', true) || {};\n const pinch: Partial = this.parent.plugins.get('pinch', true) || {};\n\n decelerate = decelerate || {};\n\n if (!drag?.active && !pinch?.active && ((!this.toX || !this.toY) && (!decelerate.x || !decelerate.y)))\n {\n oob = oob || this.oob();\n const topLeft = oob.topLeft;\n const bottomRight = oob.bottomRight;\n\n if (!this.toX && !decelerate.x)\n {\n let x = null;\n\n if (oob.left && this.left)\n {\n x = (this.parent.screenWorldWidth < this.parent.screenWidth) ? this.calcUnderflowX() : -topLeft.x;\n }\n else if (oob.right && this.right)\n {\n x = (this.parent.screenWorldWidth < this.parent.screenWidth) ? this.calcUnderflowX() : -bottomRight.x;\n }\n if (x !== null && this.parent.x !== x)\n {\n this.toX = { time: 0, start: this.parent.x, delta: x - this.parent.x, end: x };\n this.parent.emit('bounce-x-start', this.parent);\n }\n }\n if (!this.toY && !decelerate.y)\n {\n let y = null;\n\n if (oob.top && this.top)\n {\n y = (this.parent.screenWorldHeight < this.parent.screenHeight) ? this.calcUnderflowY() : -topLeft.y;\n }\n else if (oob.bottom && this.bottom)\n {\n y = (this.parent.screenWorldHeight < this.parent.screenHeight) ? this.calcUnderflowY() : -bottomRight.y;\n }\n if (y !== null && this.parent.y !== y)\n {\n this.toY = { time: 0, start: this.parent.y, delta: y - this.parent.y, end: y };\n this.parent.emit('bounce-y-start', this.parent);\n }\n }\n }\n }\n\n public reset(): void\n {\n this.toX = this.toY = null;\n this.bounce();\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\n\n/**\n * There are three ways to clamp:\n * 1. direction: 'all' = the world is clamped to its world boundaries, ie, you cannot drag any part of the world offscreen\n * direction: 'x' | 'y' = only the x or y direction is clamped to its world boundary\n * 2. left, right, top, bottom = true | number = the world is clamped to the world's pixel location for each side;\n * if any of these are set to true, then the location is set to the boundary [0, viewport.worldWidth/viewport.worldHeight]\n * eg: to allow the world to be completely dragged offscreen, set [-viewport.worldWidth, -viewport.worldHeight, viewport.worldWidth * 2, viewport.worldHeight * 2]\n *\n * Underflow determines what happens when the world is smaller than the viewport\n * 1. none = the world is clamped but there is no special behavior\n * 2. center = the world is centered on the viewport\n * 3. combination of top/bottom/center and left/right/center (case insensitive) = the world is stuck to the appropriate boundaries\n *\n */\nexport interface IClampOptions\n{\n /**\n * Clamp left; true = 0\n *\n * @default false\n */\n left?: number | boolean | null;\n\n /**\n * Clamp top; true = 0\n *\n * @default false\n */\n top?: number | boolean | null;\n\n /**\n * Clamp right; true = viewport.worldWidth\n *\n * @default false\n */\n right?: number | boolean | null;\n\n /**\n * Clamp bottom; true = viewport.worldHeight\n *\n * @default false\n */\n bottom?: number | boolean | null;\n\n /**\n * (all, x, or y) using clamps of [0, viewport.worldWidth/viewport.worldHeight]; replaces left/right/top/bottom if set\n *\n * @default null\n */\n direction?: 'all' | 'x' | 'y' | null;\n\n /**\n * Where to place world if too small for screen (e.g., top-right, center, none, bottomleft)\n *\n * @default 'center'\n */\n underflow?: 'center' | string;\n}\n\nconst DEFAULT_CLAMP_OPTIONS: Required = {\n left: false,\n right: false,\n top: false,\n bottom: false,\n direction: null,\n underflow: 'center'\n};\n\n/**\n * Plugin to clamp the viewport to a specific world bounding box.\n *\n * @public\n */\nexport class Clamp extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Required;\n\n /** Last state of viewport */\n protected last: {\n x: number | null;\n y: number | null;\n scaleX: number | null;\n scaleY: number | null;\n };\n\n protected noUnderflow!: boolean;\n protected underflowX!: -1 | 0 | 1;\n protected underflowY!: -1 | 0 | 1;\n\n /**\n * This is called by {@link Viewport.clamp}.\n */\n constructor(parent: Viewport, options : IClampOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_CLAMP_OPTIONS, options);\n\n if (this.options.direction)\n {\n this.options.left = this.options.direction === 'x' || this.options.direction === 'all' ? true : null;\n this.options.right = this.options.direction === 'x' || this.options.direction === 'all' ? true : null;\n this.options.top = this.options.direction === 'y' || this.options.direction === 'all' ? true : null;\n this.options.bottom = this.options.direction === 'y' || this.options.direction === 'all' ? true : null;\n }\n\n this.parseUnderflow();\n this.last = { x: null, y: null, scaleX: null, scaleY: null };\n this.update();\n }\n\n private parseUnderflow()\n {\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'none')\n {\n this.noUnderflow = true;\n }\n else if (clamp === 'center')\n {\n this.underflowX = this.underflowY = 0;\n this.noUnderflow = false;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n this.noUnderflow = false;\n }\n }\n\n public move(): boolean\n {\n this.update();\n\n return false;\n }\n\n public update(): void\n {\n if (this.paused)\n {\n return;\n }\n\n // only clamp on change\n if (this.parent.x === this.last.x\n && this.parent.y === this.last.y\n && this.parent.scale.x === this.last.scaleX\n && this.parent.scale.y === this.last.scaleY)\n {\n return;\n }\n const original = { x: this.parent.x, y: this.parent.y };\n // TODO: Fix\n const decelerate: any = (this.parent.plugins as any).decelerate || {};\n\n if (this.options.left !== null || this.options.right !== null)\n {\n let moved = false;\n\n if (!this.noUnderflow && this.parent.screenWorldWidth < this.parent.screenWidth)\n {\n switch (this.underflowX)\n {\n case -1:\n if (this.parent.x !== 0)\n {\n this.parent.x = 0;\n moved = true;\n }\n break;\n case 1:\n if (this.parent.x !== this.parent.screenWidth - this.parent.screenWorldWidth)\n {\n this.parent.x = this.parent.screenWidth - this.parent.screenWorldWidth;\n moved = true;\n }\n break;\n default:\n if (this.parent.x !== (this.parent.screenWidth - this.parent.screenWorldWidth) / 2)\n {\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n moved = true;\n }\n }\n }\n else\n {\n if (this.options.left !== null)\n {\n if (this.parent.left < (this.options.left === true ? 0 : this.options.left))\n {\n this.parent.x = -(this.options.left === true ? 0 : this.options.left) * this.parent.scale.x;\n decelerate.x = 0;\n moved = true;\n }\n }\n if (this.options.right !== null)\n {\n if (this.parent.right > (this.options.right === true ? this.parent.worldWidth : this.options.right))\n {\n this.parent.x = -(this.options.right === true ? this.parent.worldWidth : this.options.right) * this.parent.scale.x + this.parent.screenWidth;\n decelerate.x = 0;\n moved = true;\n }\n }\n }\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, original, type: 'clamp-x' });\n }\n }\n if (this.options.top !== null || this.options.bottom !== null)\n {\n let moved = false;\n\n if (!this.noUnderflow && this.parent.screenWorldHeight < this.parent.screenHeight)\n {\n switch (this.underflowY)\n {\n case -1:\n if (this.parent.y !== 0)\n {\n this.parent.y = 0;\n moved = true;\n }\n break;\n case 1:\n if (this.parent.y !== this.parent.screenHeight - this.parent.screenWorldHeight)\n {\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n moved = true;\n }\n break;\n default:\n if (this.parent.y !== (this.parent.screenHeight - this.parent.screenWorldHeight) / 2)\n {\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n moved = true;\n }\n }\n }\n else\n {\n if (this.options.top !== null)\n {\n if (this.parent.top < (this.options.top === true ? 0 : this.options.top))\n {\n this.parent.y = -(this.options.top === true ? 0 : this.options.top)\n * this.parent.scale.y;\n decelerate.y = 0;\n moved = true;\n }\n }\n if (this.options.bottom !== null)\n {\n if (this.parent.bottom > (this.options.bottom === true ? this.parent.worldHeight : this.options.bottom))\n {\n this.parent.y = -(this.options.bottom === true ? this.parent.worldHeight : this.options.bottom)\n * this.parent.scale.y + this.parent.screenHeight;\n decelerate.y = 0;\n moved = true;\n }\n }\n }\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, original, type: 'clamp-y' });\n }\n }\n this.last.x = this.parent.x;\n this.last.y = this.parent.y;\n this.last.scaleX = this.parent.scale.x;\n this.last.scaleY = this.parent.scale.y;\n }\n\n public reset(): void\n {\n this.update();\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\n\n/**\n * Options for {@link ClampZoom}.\n *\n * Use either minimum width/height or minimum scale\n */\nexport interface IClampZoomOptions\n{\n /** Minimum width */\n minWidth?: number | null;\n\n /** Minimum height */\n minHeight?: number | null;\n\n /** Maximum width */\n maxWidth?: number | null;\n\n /** Maximum height */\n maxHeight?: number | null;\n\n /** Minimum scale */\n minScale?: number | null;\n\n /** Maximum scale */\n maxScale?: number | null;\n}\n\nconst DEFAULT_CLAMP_ZOOM_OPTIONS: Required = {\n minWidth: null,\n minHeight: null,\n maxWidth: null,\n maxHeight: null,\n minScale: null,\n maxScale: null\n};\n\n/**\n * Plugin to clamp the viewport's zoom to a specific range.\n *\n * @public\n */\nexport class ClampZoom extends Plugin\n{\n public readonly options: Required;\n\n /**\n * This is called by {@link Viewport.clampZoom}.\n */\n constructor(parent: Viewport, options = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_CLAMP_ZOOM_OPTIONS, options);\n\n this.clamp();\n }\n\n public resize(): void\n {\n this.clamp();\n }\n\n /** Clamp the viewport's zoom immediately. */\n public clamp(): void\n {\n if (this.paused)\n {\n return;\n }\n\n if (this.options.minWidth || this.options.minHeight || this.options.maxWidth || this.options.maxHeight)\n {\n let width = this.parent.worldScreenWidth;\n let height = this.parent.worldScreenHeight;\n\n if (this.options.minWidth !== null && width < this.options.minWidth)\n {\n const original = this.parent.scale.x;\n\n this.parent.fitWidth(this.options.minWidth, false, false, true);\n this.parent.scale.y *= this.parent.scale.x / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.maxWidth !== null && width > this.options.maxWidth)\n {\n const original = this.parent.scale.x;\n\n this.parent.fitWidth(this.options.maxWidth, false, false, true);\n this.parent.scale.y *= this.parent.scale.x / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.minHeight !== null && height < this.options.minHeight)\n {\n const original = this.parent.scale.y;\n\n this.parent.fitHeight(this.options.minHeight, false, false, true);\n this.parent.scale.x *= this.parent.scale.y / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.maxHeight !== null && height > this.options.maxHeight)\n {\n const original = this.parent.scale.y;\n\n this.parent.fitHeight(this.options.maxHeight, false, false, true);\n this.parent.scale.x *= this.parent.scale.y / original;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n }\n else\n {\n let scale = this.parent.scale.x;\n\n if (this.options.minScale !== null && scale < this.options.minScale)\n {\n scale = this.options.minScale;\n }\n if (this.options.maxScale !== null && scale > this.options.maxScale)\n {\n scale = this.options.maxScale;\n }\n if (scale !== this.parent.scale.x)\n {\n this.parent.scale.set(scale);\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n }\n }\n\n public reset(): void\n {\n this.clamp();\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Point } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\nexport interface IDecelerateOptions {\n /**\n * Percent to decelerate after movement. This should be between 0 and 1, exclusive.\n *\n * @default 0.95\n */\n friction?: number;\n\n /**\n * Percent to decelerate when past boundaries (only applicable when viewport.bounce() is active)\n *\n * @default 0.8\n */\n bounce?: number;\n\n /**\n * Minimum velocity before stopping/reversing acceleration\n *\n * @default 0.01\n */\n minSpeed?: number;\n}\n\n/** Viewport position snapshot that's saved by {@link DeceleratePlugin} to estimate panning velocity. */\nexport interface IDecelerateSnapshot {\n /** x-coordinate of the viewport. */\n x: number;\n\n /** y-coordinate of the viewport. */\n y: number;\n\n /** Time at which this snapshot was taken. */\n time: number;\n}\n\nconst DEFAULT_DECELERATE_OPTIONS: Required = {\n friction: 0.98,\n bounce: 0.8,\n minSpeed: 0.01\n};\n\n/**\n * Time period of decay (1 frame)\n *\n * @internal\n * @ignore\n */\nconst TP = 16;\n\n/**\n * Plugin to decelerate viewport velocity smoothly after panning ends.\n *\n * @public\n */\nexport class Decelerate extends Plugin\n{\n /** Options used to initialize this plugin. */\n public readonly options: Required;\n\n /**\n * x-component of the velocity of viewport provided by this plugin, at the current time.\n *\n * This is measured in px/frame, where a frame is normalized to 16 milliseconds.\n */\n public x!: number | null;\n\n /**\n * y-component of the velocity of the viewport provided by this plugin, at the current time.\n *\n * This is measured in px/frame, where a frame is normalized to 16 milliseconds.\n */\n public y!: number | null;\n\n /**\n * The decay factor for the x-component of the viewport.\n *\n * The viewport's velocity decreased by this amount each 16 milliseconds.\n */\n public percentChangeX!: number;\n\n /**\n * The decay factor for the y-component of the viewport.\n *\n * The viewport's velocity decreased by this amount each 16 milliseconds.\n */\n public percentChangeY!: number;\n\n /** Saved list of recent viewport position snapshots, to estimate velocity. */\n protected saved: Array;\n\n /** The time since the user released panning of the viewport. */\n protected timeSinceRelease: number;\n\n /**\n * This is called by {@link Viewport.decelerate}.\n */\n constructor(parent: Viewport, options: IDecelerateOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_DECELERATE_OPTIONS, options);\n this.saved = [];\n this.timeSinceRelease = 0;\n\n this.reset();\n this.parent.on('moved', (data) => this.moved(data));\n }\n\n public down(): boolean\n {\n this.saved = [];\n this.x = this.y = null;\n\n return false;\n }\n\n public isActive(): boolean\n {\n return !!(this.x || this.y);\n }\n\n public move(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n const count = this.parent.input.count();\n\n if (count === 1 || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n this.saved.push({ x: this.parent.x, y: this.parent.y, time: performance.now() });\n\n if (this.saved.length > 60)\n {\n this.saved.splice(0, 30);\n }\n }\n\n // Silently recording viewport positions\n return false;\n }\n\n /** Listener to viewport's \"moved\" event. */\n protected moved(data: { type: 'clamp-x' | 'clamp-y'; original: Point }): void\n {\n if (this.saved.length)\n {\n const last = this.saved[this.saved.length - 1];\n\n if (data.type === 'clamp-x')\n {\n if (last.x === data.original.x)\n {\n last.x = this.parent.x;\n }\n }\n else if (data.type === 'clamp-y')\n {\n if (last.y === data.original.y)\n {\n last.y = this.parent.y;\n }\n }\n }\n }\n\n public up(): boolean\n {\n if (this.parent.input.count() === 0 && this.saved.length)\n {\n const now = performance.now();\n\n for (const save of this.saved)\n {\n if (save.time >= now - 100)\n {\n const time = now - save.time;\n\n this.x = (this.parent.x - save.x) / time;\n this.y = (this.parent.y - save.y) / time;\n this.percentChangeX = this.percentChangeY = this.options.friction;\n this.timeSinceRelease = 0;\n break;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Manually activate deceleration, starting from the (x, y) velocity components passed in the options.\n *\n * @param {object} options\n * @param {number} [options.x] - Specify x-component of initial velocity.\n * @param {number} [options.y] - Specify y-component of initial velocity.\n */\n public activate(options: { x?: number; y?: number; }): void\n {\n options = options || {};\n\n if (typeof options.x !== 'undefined')\n {\n this.x = options.x;\n this.percentChangeX = this.options.friction;\n }\n if (typeof options.y !== 'undefined')\n {\n this.y = options.y;\n this.percentChangeY = this.options.friction;\n }\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n /*\n * See https://github.com/davidfig/pixi-viewport/issues/271 for math.\n *\n * The viewport velocity (this.x, this.y) decays exponentially by the the decay factor\n * (this.percentChangeX, this.percentChangeY) each frame. This velocity function is integrated\n * to calculate the displacement.\n */\n\n const moved = this.x || this.y;\n\n const ti = this.timeSinceRelease;\n const tf = this.timeSinceRelease + elapsed;\n\n if (this.x)\n {\n const k = this.percentChangeX;\n const lnk = Math.log(k);\n\n // Apply velocity delta on the viewport x-coordinate.\n this.parent.x += ((this.x * TP) / lnk) * (Math.pow(k, tf / TP) - Math.pow(k, ti / TP));\n\n // Apply decay on x-component of velocity\n this.x *= Math.pow(this.percentChangeX, elapsed / TP);\n }\n if (this.y)\n {\n const k = this.percentChangeY;\n const lnk = Math.log(k);\n\n // Apply velocity delta on the viewport y-coordinate.\n this.parent.y += ((this.y * TP) / lnk) * (Math.pow(k, tf / TP) - Math.pow(k, ti / TP));\n\n // Apply decay on y-component of velocity\n this.y *= Math.pow(this.percentChangeY, elapsed / TP);\n }\n\n this.timeSinceRelease += elapsed;\n\n // End decelerate velocity once it goes under a certain amount of precision.\n if (Math.abs(this.x || 0) < this.options.minSpeed)\n {\n this.x = 0;\n }\n if (Math.abs(this.y || 0) < this.options.minSpeed)\n {\n this.y = 0;\n }\n\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, type: 'decelerate' });\n }\n }\n\n public reset(): void\n {\n this.x = this.y = null;\n }\n}\n","import { Point } from '@pixi/math';\nimport { Plugin } from './Plugin';\n\nimport type { Decelerate } from './Decelerate';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { IPointData } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Drag}. */\nexport interface IDragOptions {\n /**\n * direction to drag\n *\n * @default \"all\"\n */\n direction?: string;\n\n /**\n * whether click to drag is active\n *\n * @default true\n */\n pressDrag?: boolean;\n\n /**\n * Use wheel to scroll in direction (unless wheel plugin is active)\n *\n * @default true\n */\n wheel?: boolean;\n\n /**\n * number of pixels to scroll with each wheel spin\n *\n * @default 1\n */\n wheelScroll?: number;\n\n /**\n * reverse the direction of the wheel scroll\n *\n * @default false\n */\n reverse?: boolean;\n\n /**\n * clamp wheel(to avoid weird bounce with mouse wheel). Can be 'x' or 'y' or `true`.\n *\n * @default false\n */\n clampWheel?: boolean | string;\n\n /**\n * where to place world if too small for screen\n *\n * @default \"center\"\n */\n underflow?: string;\n\n /**\n * factor to multiply drag to increase the speed of movement\n *\n * @default 1\n */\n factor?: number;\n\n /**\n * Changes which mouse buttons trigger drag.\n *\n * Use: 'all', 'left', right' 'middle', or some combination, like, 'middle-right'; you may want to set\n * `viewport.options.disableOnContextMenu` if you want to use right-click dragging.\n *\n * @default \"all\"\n */\n mouseButtons?: 'all' | string;\n\n /**\n * Array containing {@link key|https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code} codes of\n * keys that can be pressed for the drag to be triggered, e.g.: ['ShiftLeft', 'ShiftRight'}.\n *\n * @default null\n */\n keyToPress?: string[] | null;\n\n /**\n * Ignore keyToPress for touch events.\n *\n * @default false\n */\n ignoreKeyToPressOnTouch?: boolean;\n\n /**\n * Scaling factor for non-DOM_DELTA_PIXEL scrolling events.\n *\n * @default 20\n */\n lineHeight?: number;\n}\n\nconst DEFAULT_DRAG_OPTIONS: Required = {\n direction: 'all',\n pressDrag: true,\n wheel: true,\n wheelScroll: 1,\n reverse: false,\n clampWheel: false,\n underflow: 'center',\n factor: 1,\n mouseButtons: 'all',\n keyToPress: null,\n ignoreKeyToPressOnTouch: false,\n lineHeight: 20,\n};\n\n/**\n * Plugin to enable panning/dragging of the viewport to move around.\n *\n * @public\n */\nexport class Drag extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Readonly>;\n\n /** Flags when viewport is moving. */\n protected moved: boolean;\n\n /** Factor to apply from {@link IDecelerateOptions}'s reverse. */\n protected reverse: 1 | -1;\n\n /** Holds whether dragging is enabled along the x-axis. */\n protected xDirection: boolean\n\n /** Holds whether dragging is enabled along the y-axis. */\n protected yDirection: boolean;\n\n /** Flags whether the keys required to drag are pressed currently. */\n protected keyIsPressed: boolean;\n\n /** Holds whether the left, center, and right buttons are required to pan. */\n protected mouse!: [boolean, boolean, boolean]\n\n /** Underflow factor along x-axis */\n protected underflowX!: -1 | 0 | 1;\n\n /** Underflow factor along y-axis */\n protected underflowY!: -1 | 0 | 1;\n\n /** Last pointer position while panning. */\n protected last?: IPointData | null\n\n /** The ID of the pointer currently panning the viewport. */\n protected current?: number;\n\n /**\n * This is called by {@link Viewport.drag}.\n */\n constructor(parent: Viewport, options = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_DRAG_OPTIONS, options);\n this.moved = false;\n this.reverse = this.options.reverse ? 1 : -1;\n this.xDirection = !this.options.direction || this.options.direction === 'all' || this.options.direction === 'x';\n this.yDirection = !this.options.direction || this.options.direction === 'all' || this.options.direction === 'y';\n this.keyIsPressed = false;\n\n this.parseUnderflow();\n this.mouseButtons(this.options.mouseButtons);\n\n if (this.options.keyToPress)\n {\n this.handleKeyPresses(this.options.keyToPress);\n }\n }\n\n /**\n * Handles keypress events and set the keyIsPressed boolean accordingly\n *\n * @param {array} codes - key codes that can be used to trigger drag event\n */\n protected handleKeyPresses(codes: string[]): void\n {\n window.addEventListener('keydown', (e) =>\n {\n if (codes.includes(e.code))\n { this.keyIsPressed = true; }\n });\n\n window.addEventListener('keyup', (e) =>\n {\n if (codes.includes(e.code))\n { this.keyIsPressed = false; }\n });\n }\n\n /**\n * initialize mousebuttons array\n * @param {string} buttons\n */\n protected mouseButtons(buttons: string): void\n {\n if (!buttons || buttons === 'all')\n {\n this.mouse = [true, true, true];\n }\n else\n {\n this.mouse = [\n buttons.indexOf('left') !== -1,\n buttons.indexOf('middle') !== -1,\n buttons.indexOf('right') !== -1\n ];\n }\n }\n\n protected parseUnderflow(): void\n {\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'center')\n {\n this.underflowX = 0;\n this.underflowY = 0;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n }\n }\n\n /**\n * @param {PIXI.InteractionEvent} event\n * @returns {boolean}\n */\n protected checkButtons(event: InteractionEvent): boolean\n {\n const isMouse = event.data.pointerType === 'mouse';\n const count = this.parent.input.count();\n\n if ((count === 1) || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n if (!isMouse || this.mouse[event.data.button])\n {\n return true;\n }\n }\n\n return false;\n }\n\n /**\n * @param {PIXI.InteractionEvent} event\n * @returns {boolean}\n */\n protected checkKeyPress(event: InteractionEvent): boolean\n {\n return (!this.options.keyToPress\n || this.keyIsPressed\n || (this.options.ignoreKeyToPressOnTouch && event.data.pointerType === 'touch'));\n }\n\n public down(event: InteractionEvent): boolean\n {\n if (this.paused || !this.options.pressDrag)\n {\n return false;\n }\n if (this.checkButtons(event) && this.checkKeyPress(event))\n {\n this.last = { x: event.data.global.x, y: event.data.global.y };\n this.current = event.data.pointerId;\n\n return true;\n }\n this.last = null;\n\n return false;\n }\n\n get active(): boolean\n {\n return this.moved;\n }\n\n public move(event: InteractionEvent): boolean\n {\n if (this.paused || !this.options.pressDrag)\n {\n return false;\n }\n if (this.last && this.current === event.data.pointerId)\n {\n const x = event.data.global.x;\n const y = event.data.global.y;\n const count = this.parent.input.count();\n\n if (count === 1 || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n const distX = x - this.last.x;\n const distY = y - this.last.y;\n\n if (this.moved\n || ((this.xDirection && this.parent.input.checkThreshold(distX))\n || (this.yDirection && this.parent.input.checkThreshold(distY))))\n {\n const newPoint = { x, y };\n\n if (this.xDirection)\n {\n this.parent.x += (newPoint.x - this.last.x) * this.options.factor;\n }\n if (this.yDirection)\n {\n this.parent.y += (newPoint.y - this.last.y) * this.options.factor;\n }\n this.last = newPoint;\n if (!this.moved)\n {\n this.parent.emit('drag-start', {\n event,\n screen: new Point(this.last.x, this.last.y),\n world: this.parent.toWorld(new Point(this.last.x, this.last.y)),\n viewport: this.parent\n });\n }\n this.moved = true;\n this.parent.emit('moved', { viewport: this.parent, type: 'drag' });\n\n return true;\n }\n }\n else\n {\n this.moved = false;\n }\n }\n\n return false;\n }\n\n public up(event: InteractionEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n const touches = this.parent.input.touches;\n\n if (touches.length === 1)\n {\n const pointer = touches[0];\n\n if (pointer.last)\n {\n this.last = { x: pointer.last.x, y: pointer.last.y };\n this.current = pointer.id;\n }\n this.moved = false;\n\n return true;\n }\n else if (this.last)\n {\n if (this.moved)\n {\n const screen = new Point(this.last.x, this.last.y);\n\n this.parent.emit('drag-end', {\n event, screen,\n world: this.parent.toWorld(screen),\n viewport: this.parent,\n });\n this.last = null;\n this.moved = false;\n\n return true;\n }\n }\n\n return false;\n }\n\n public wheel(event: WheelEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n if (this.options.wheel)\n {\n const wheel = this.parent.plugins.get('wheel', true);\n\n if (!wheel)\n {\n const step = event.deltaMode ? this.options.lineHeight : 1;\n\n if (this.xDirection)\n {\n this.parent.x += event.deltaX * step * this.options.wheelScroll * this.reverse;\n }\n if (this.yDirection)\n {\n this.parent.y += event.deltaY * step * this.options.wheelScroll * this.reverse;\n }\n if (this.options.clampWheel)\n {\n this.clamp();\n }\n this.parent.emit('wheel-scroll', this.parent);\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n if (!this.parent.options.passiveWheel)\n {\n event.preventDefault();\n }\n\n return true;\n }\n }\n\n return false;\n }\n\n public resume()\n {\n this.last = null;\n this.paused = false;\n }\n\n public clamp()\n {\n const decelerate: Partial = this.parent.plugins.get('decelerate', true) || {};\n\n if (this.options.clampWheel !== 'y')\n {\n if (this.parent.screenWorldWidth < this.parent.screenWidth)\n {\n switch (this.underflowX)\n {\n case -1:\n this.parent.x = 0;\n break;\n case 1:\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth);\n break;\n default:\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n }\n }\n else\n if (this.parent.left < 0)\n {\n this.parent.x = 0;\n decelerate.x = 0;\n }\n else if (this.parent.right > this.parent.worldWidth)\n {\n this.parent.x = -this.parent.worldWidth * this.parent.scale.x + this.parent.screenWidth;\n decelerate.x = 0;\n }\n }\n if (this.options.clampWheel !== 'x')\n {\n if (this.parent.screenWorldHeight < this.parent.screenHeight)\n {\n switch (this.underflowY)\n {\n case -1:\n this.parent.y = 0;\n break;\n case 1:\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n break;\n default:\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n }\n }\n else\n {\n if (this.parent.top < 0)\n {\n this.parent.y = 0;\n decelerate.y = 0;\n }\n if (this.parent.bottom > this.parent.worldHeight)\n {\n this.parent.y = -this.parent.worldHeight * this.parent.scale.y + this.parent.screenHeight;\n decelerate.y = 0;\n }\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { DisplayObject } from '@pixi/display';\nimport type { IPointData } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Follow}. */\nexport interface IFollowOptions\n{\n /**\n * Speed to follow in px/frame (0 = teleport to location)\n *\n * @default 9\n */\n speed?: number;\n\n /**\n * Set acceleration to accelerate and decelerate at this rate; speed cannot be 0 to use acceleration\n *\n * @default null\n */\n acceleration?: number | null;\n\n /**\n * Radius (in world coordinates) of center circle where movement is allowed without moving the viewport\n *\n * @default null\n */\n radius?: number | null;\n}\n\nconst DEFAULT_FOLLOW_OPTIONS: Required = {\n speed: 0,\n acceleration: null,\n radius: null\n};\n\n/**\n * Plugin to follow a display-object.\n *\n * @see Viewport.follow\n * @public\n */\nexport class Follow extends Plugin\n{\n /** The options used to initialize this plugin. */\n public readonly options: Required;\n\n /** The target this plugin will make the viewport follow. */\n public target: DisplayObject;\n\n /** The velocity provided the viewport by following, at the current time. */\n protected velocity: IPointData;\n\n /**\n * This is called by {@link Viewport.follow}.\n *\n * @param parent\n * @param target - target to follow\n * @param options\n */\n constructor(parent: Viewport, target: DisplayObject, options: IFollowOptions = {})\n {\n super(parent);\n\n this.target = target;\n this.options = Object.assign({}, DEFAULT_FOLLOW_OPTIONS, options);\n this.velocity = { x: 0, y: 0 };\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n const center = this.parent.center;\n let toX = this.target.x;\n let toY = this.target.y;\n\n if (this.options.radius)\n {\n const distance = Math.sqrt(Math.pow(this.target.y - center.y, 2) + Math.pow(this.target.x - center.x, 2));\n\n if (distance > this.options.radius)\n {\n const angle = Math.atan2(this.target.y - center.y, this.target.x - center.x);\n\n toX = this.target.x - (Math.cos(angle) * this.options.radius);\n toY = this.target.y - (Math.sin(angle) * this.options.radius);\n }\n else\n {\n return;\n }\n }\n\n const deltaX = toX - center.x;\n const deltaY = toY - center.y;\n\n if (deltaX || deltaY)\n {\n if (this.options.speed)\n {\n if (this.options.acceleration)\n {\n const angle = Math.atan2(toY - center.y, toX - center.x);\n const distance = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));\n\n if (distance)\n {\n const decelerationDistance = (Math.pow(this.velocity.x, 2) + Math.pow(this.velocity.y, 2)) / (2 * this.options.acceleration);\n\n if (distance > decelerationDistance)\n {\n this.velocity = {\n x: Math.min(this.velocity.x + this.options.acceleration * elapsed, this.options.speed),\n y: Math.min(this.velocity.y + this.options.acceleration * elapsed, this.options.speed)\n };\n }\n else\n {\n this.velocity = {\n x: Math.max(this.velocity.x - this.options.acceleration * this.options.speed, 0),\n y: Math.max(this.velocity.y - this.options.acceleration * this.options.speed, 0)\n };\n }\n const changeX = Math.cos(angle) * this.velocity.x;\n const changeY = Math.sin(angle) * this.velocity.y;\n const x = Math.abs(changeX) > Math.abs(deltaX) ? toX : center.x + changeX;\n const y = Math.abs(changeY) > Math.abs(deltaY) ? toY : center.y + changeY;\n\n this.parent.moveCenter(x, y);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n else\n {\n const angle = Math.atan2(toY - center.y, toX - center.x);\n const changeX = Math.cos(angle) * this.options.speed;\n const changeY = Math.sin(angle) * this.options.speed;\n const x = Math.abs(changeX) > Math.abs(deltaX) ? toX : center.x + changeX;\n const y = Math.abs(changeY) > Math.abs(deltaY) ? toY : center.y + changeY;\n\n this.parent.moveCenter(x, y);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n else\n {\n this.parent.moveCenter(toX, toY);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\nimport type { InteractionEvent } from '@pixi/interaction';\n\n/** Insets for mouse edges scrolling regions */\nexport interface IMouseEdgesInsets {\n /** Distance from center of screen in screen pixels */\n radius?: number | null;\n\n /** Distance from all sides in screen pixels */\n distance?: number | null;\n\n /** Alternatively, set top distance (leave unset for no top scroll) */\n top?: number | null;\n\n /** Alternatively, set bottom distance (leave unset for no top scroll) */\n bottom?: number | null;\n\n /** Alternatively, set left distance (leave unset for no top scroll) */\n left?: number | null;\n\n /** Alternatively, set right distance (leave unset for no top scroll) */\n right?: number | null;\n}\n\n/** Options for {@link MouseEdges}. */\nexport interface IMouseEdgesOptions extends IMouseEdgesInsets {\n /** Speed in pixels/frame to scroll viewport */\n speed?: number;\n\n /** Reverse direction of scroll */\n reverse?: boolean;\n\n /** Don't use decelerate plugin even if it's installed */\n noDecelerate?: boolean;\n\n /**\n * If using radius, use linear movement (+/- 1, +/- 1) instead of angled movement.\n *\n * (Math.cos(angle from center), Math.sin(angle from center))\n */\n linear?: boolean;\n\n /** Allows plugin to continue working even when there's a `mousedown` event. */\n allowButtons?: boolean;\n}\n\nconst MOUSE_EDGES_OPTIONS: Required = {\n radius: null,\n distance: null,\n top: null,\n bottom: null,\n left: null,\n right: null,\n speed: 8,\n reverse: false,\n noDecelerate: false,\n linear: false,\n allowButtons: false,\n};\n\n/**\n * Scroll viewport when mouse hovers near one of the edges.\n *\n * @event mouse-edge-start(Viewport) emitted when mouse-edge starts\n * @event mouse-edge-end(Viewport) emitted when mouse-edge ends\n */\nexport class MouseEdges extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Readonly>;\n\n /** Factor from reverse option. */\n protected readonly reverse: -1 | 1;\n\n /** Radius squared */\n protected readonly radiusSquared: number | null;\n\n /** Scroll region size on the left side. */\n protected left!: number | null;\n\n /** Scroll region size on the top size. */\n protected top!: number | null;\n\n /** Scroll region size on the right side. */\n protected right!: number | null;\n\n /** Scroll region size on the bottom side. */\n protected bottom!: number | null;\n\n protected horizontal?: number | null;\n\n protected vertical?: number | null;\n\n /**\n * This is called by {@link Viewport.mouseEdges}.\n */\n constructor(parent: Viewport, options: IMouseEdgesOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, MOUSE_EDGES_OPTIONS, options);\n this.reverse = this.options.reverse ? 1 : -1;\n this.radiusSquared = typeof this.options.radius === 'number' ? Math.pow(this.options.radius, 2) : null;\n\n this.resize();\n }\n\n public resize(): void\n {\n const distance = this.options.distance;\n\n if (distance !== null)\n {\n this.left = distance;\n this.top = distance;\n this.right = this.parent.worldScreenWidth - distance;\n this.bottom = this.parent.worldScreenHeight - distance;\n }\n else if (!this.options.radius)\n {\n this.left = this.options.left;\n this.top = this.options.top;\n this.right = this.options.right === null ? null : this.parent.worldScreenWidth - this.options.right;\n this.bottom = this.options.bottom === null ? null : this.parent.worldScreenHeight - this.options.bottom;\n }\n }\n\n public down(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if (!this.options.allowButtons)\n {\n this.horizontal = this.vertical = null;\n }\n\n return false;\n }\n\n public move(event: InteractionEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if ((event.data.pointerType !== 'mouse' && event.data.identifier !== 1)\n || (!this.options.allowButtons && event.data.buttons !== 0))\n {\n return false;\n }\n\n const x = event.data.global.x;\n const y = event.data.global.y;\n\n if (this.radiusSquared)\n {\n const center = this.parent.toScreen(this.parent.center);\n const distance = Math.pow(center.x - x, 2) + Math.pow(center.y - y, 2);\n\n if (distance >= this.radiusSquared)\n {\n const angle = Math.atan2(center.y - y, center.x - x);\n\n if (this.options.linear)\n {\n this.horizontal = Math.round(Math.cos(angle)) * this.options.speed * this.reverse * (60 / 1000);\n this.vertical = Math.round(Math.sin(angle)) * this.options.speed * this.reverse * (60 / 1000);\n }\n else\n {\n this.horizontal = Math.cos(angle) * this.options.speed * this.reverse * (60 / 1000);\n this.vertical = Math.sin(angle) * this.options.speed * this.reverse * (60 / 1000);\n }\n }\n else\n {\n if (this.horizontal)\n {\n this.decelerateHorizontal();\n }\n if (this.vertical)\n {\n this.decelerateVertical();\n }\n\n this.horizontal = this.vertical = 0;\n }\n }\n else\n {\n if (this.left !== null && x < this.left)\n {\n this.horizontal = Number(this.reverse) * this.options.speed * (60 / 1000);\n }\n else if (this.right !== null && x > this.right)\n {\n this.horizontal = -1 * this.reverse * this.options.speed * (60 / 1000);\n }\n else\n {\n this.decelerateHorizontal();\n this.horizontal = 0;\n }\n if (this.top !== null && y < this.top)\n {\n this.vertical = Number(this.reverse) * this.options.speed * (60 / 1000);\n }\n else if (this.bottom !== null && y > this.bottom)\n {\n this.vertical = -1 * this.reverse * this.options.speed * (60 / 1000);\n }\n else\n {\n this.decelerateVertical();\n this.vertical = 0;\n }\n }\n\n return false;\n }\n\n private decelerateHorizontal(): void\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (this.horizontal && decelerate && !this.options.noDecelerate)\n {\n decelerate.activate({ x: (this.horizontal * this.options.speed * this.reverse) / (1000 / 60) });\n }\n }\n\n private decelerateVertical(): void\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (this.vertical && decelerate && !this.options.noDecelerate)\n {\n decelerate.activate({ y: (this.vertical * this.options.speed * this.reverse) / (1000 / 60) });\n }\n }\n\n public up(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if (this.horizontal)\n {\n this.decelerateHorizontal();\n }\n if (this.vertical)\n {\n this.decelerateVertical();\n }\n this.horizontal = this.vertical = null;\n\n return false;\n }\n\n public update(): void\n {\n if (this.paused)\n {\n return;\n }\n\n if (this.horizontal || this.vertical)\n {\n const center = this.parent.center;\n\n if (this.horizontal)\n {\n center.x += this.horizontal * this.options.speed;\n }\n if (this.vertical)\n {\n center.y += this.vertical * this.options.speed;\n }\n\n this.parent.moveCenter(center);\n this.parent.emit('moved', { viewport: this.parent, type: 'mouse-edges' });\n }\n }\n}\n","import { Plugin } from './Plugin';\nimport { Point } from '@pixi/math';\n\nimport type { IPointData } from '@pixi/math';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { IViewportTouch } from '../InputManager';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Pinch}. */\nexport interface IPinchOptions\n{\n /** Disable two-finger dragging. */\n noDrag?: boolean;\n\n /**\n * Percent to modify pinch speed.\n *\n * @default 1\n */\n percent?: number;\n\n /**\n * Factor to multiply two-finger drag to increase speed of movement\n *\n * @default 1\n */\n factor?: number;\n\n /** Place this point at center during zoom instead of center of two fingers */\n center?: Point | null;\n\n /** Axis to zoom */\n axis?: 'all' | 'x' | 'y';\n}\n\nconst DEFAULT_PINCH_OPTIONS: Required = {\n noDrag: false,\n percent: 1,\n center: null,\n factor: 1,\n axis: 'all',\n};\n\n/**\n * Plugin for enabling two-finger pinching (or dragging).\n *\n * @public\n */\nexport class Pinch extends Plugin\n{\n /** Options used to initialize this plugin. */\n public readonly options: Required;\n\n /** Flags whether this plugin is active, i.e. a pointer is down on the viewport. */\n public active = false;\n\n /** Flags whether the viewport is being pinched. */\n public pinching = false;\n\n protected moved = false;\n protected lastCenter?: IPointData | null;\n\n /**\n * This is called by {@link Viewport.pinch}.\n */\n constructor(parent: Viewport, options: IPinchOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_PINCH_OPTIONS, options);\n }\n\n public down(): boolean\n {\n if (this.parent.input.count() >= 2)\n {\n this.active = true;\n return true;\n }\n\n return false;\n }\n\n public isAxisX(): boolean\n {\n return ['all', 'x'].includes(this.options.axis);\n }\n\n public isAxisY(): boolean\n {\n return ['all', 'y'].includes(this.options.axis);\n }\n\n public move(e: InteractionEvent): boolean\n {\n if (this.paused || !this.active)\n {\n return false;\n }\n\n const x = e.data.global.x;\n const y = e.data.global.y;\n\n const pointers = this.parent.input.touches;\n\n if (pointers.length >= 2)\n {\n const first = pointers[0] as IViewportTouch;\n const second = pointers[1] as IViewportTouch;\n const last = (first.last && second.last)\n ? Math.sqrt(Math.pow(second.last.x - first.last.x, 2) + Math.pow(second.last.y - first.last.y, 2))\n : null;\n\n if (first.id === e.data.pointerId)\n {\n first.last = { x, y, data: e.data } as IPointData;\n }\n else if (second.id === e.data.pointerId)\n {\n second.last = { x, y, data: e.data } as IPointData;\n }\n if (last)\n {\n let oldPoint: IPointData | undefined;\n\n const point = {\n x: (first.last as IPointData).x +\n ((second.last as IPointData).x - (first.last as IPointData).x) / 2,\n y: (first.last as IPointData).y +\n ((second.last as IPointData).y - (first.last as IPointData).y) / 2,\n };\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point);\n }\n let dist = Math.sqrt(Math.pow(\n (second.last as IPointData).x - (first.last as IPointData).x, 2) +\n Math.pow((second.last as IPointData).y - (first.last as IPointData).y, 2));\n\n dist = dist === 0 ? dist = 0.0000000001 : dist;\n\n const change = (1 - last / dist) * this.options.percent\n * (this.isAxisX() ? this.parent.scale.x : this.parent.scale.y);\n\n if (this.isAxisX())\n {\n this.parent.scale.x += change;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y += change;\n }\n\n this.parent.emit('zoomed', { viewport: this.parent, type: 'pinch', center: point });\n\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += (point.x - newPoint.x) * this.options.factor;\n this.parent.y += (point.y - newPoint.y) * this.options.factor;\n this.parent.emit('moved', { viewport: this.parent, type: 'pinch' });\n }\n if (!this.options.noDrag && this.lastCenter)\n {\n this.parent.x += (point.x - this.lastCenter.x) * this.options.factor;\n this.parent.y += (point.y - this.lastCenter.y) * this.options.factor;\n this.parent.emit('moved', { viewport: this.parent, type: 'pinch' });\n }\n\n this.lastCenter = point;\n this.moved = true;\n }\n else if (!this.pinching)\n {\n this.parent.emit('pinch-start', this.parent);\n this.pinching = true;\n }\n\n return true;\n }\n\n return false;\n }\n\n public up(): boolean\n {\n if (this.pinching)\n {\n if (this.parent.input.touches.length <= 1)\n {\n this.active = false;\n this.lastCenter = null;\n this.pinching = false;\n this.moved = false;\n this.parent.emit('pinch-end', this.parent);\n\n return true;\n }\n }\n\n return false;\n }\n}\n","import { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Viewport } from '../Viewport';\n\nexport interface ISnapOptions\n{\n /** snap to the top-left of viewport instead of center */\n topLeft?: boolean;\n\n /**\n * Friction/frame to apply if decelerate is active\n *\n * @default 0.8\n */\n friction?: number;\n\n /**\n * @default 1000\n */\n time?: number;\n\n /** Easing function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /**\n * Pause snapping with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Removes this plugin after snapping is complete\n *\n * @default false\n */\n removeOnComplete?: boolean;\n\n /**\n * Removes this plugin if interrupted by any user input\n *\n * @default false\n */\n removeOnInterrupt?: boolean;\n\n /**\n * Starts the snap immediately regardless of whether the viewport is at the desired location\n *\n * @default false\n */\n forceStart?: boolean;\n}\n\nconst DEFAULT_SNAP_OPTIONS: Required = {\n topLeft: false,\n friction: 0.8,\n time: 1000,\n ease: 'easeInOutSine',\n interrupt: true,\n removeOnComplete: false,\n removeOnInterrupt: false,\n forceStart: false\n};\n\n/**\n * @event snap-start(Viewport) emitted each time a snap animation starts\n * @event snap-restart(Viewport) emitted each time a snap resets because of a change in viewport size\n * @event snap-end(Viewport) emitted each time snap reaches its target\n * @event snap-remove(Viewport) emitted if snap plugin is removed\n */\nexport class Snap extends Plugin\n{\n public readonly options: Required;\n public ease?: any;\n public x: number;\n public y: number;\n\n protected percent?: number;\n protected snapping?: { time: number } | null;\n protected deltaX?: number;\n protected deltaY?: number;\n protected startX?: number;\n protected startY?: number;\n\n /**\n * This is called by {@link Viewport.snap}.\n */\n constructor(parent: Viewport, x: number, y: number, options: ISnapOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_SNAP_OPTIONS, options);\n this.ease = ease(options.ease, 'easeInOutSine');\n this.x = x;\n this.y = y;\n\n if (this.options.forceStart)\n {\n this.snapStart();\n }\n }\n\n public snapStart(): void\n {\n this.percent = 0;\n this.snapping = { time: 0 };\n const current = this.options.topLeft ? this.parent.corner : this.parent.center;\n\n this.deltaX = this.x - current.x;\n this.deltaY = this.y - current.y;\n this.startX = current.x;\n this.startY = current.y;\n this.parent.emit('snap-start', this.parent);\n }\n\n public wheel(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap');\n }\n\n return false;\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap');\n }\n else if (this.options.interrupt)\n {\n this.snapping = null;\n }\n\n return false;\n }\n\n public up(): boolean\n {\n if (this.parent.input.count() === 0)\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (decelerate && (decelerate.x || decelerate.y))\n {\n decelerate.percentChangeX = decelerate.percentChangeY = this.options.friction;\n }\n }\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n if (this.options.interrupt && this.parent.input.count() !== 0)\n {\n return;\n }\n if (!this.snapping)\n {\n const current = this.options.topLeft ? this.parent.corner : this.parent.center;\n\n if (current.x !== this.x || current.y !== this.y)\n {\n this.snapStart();\n }\n }\n else\n {\n const snapping = this.snapping;\n\n snapping.time += elapsed;\n let finished;\n let x;\n let y;\n\n const startX = this.startX as number;\n const startY = this.startY as number;\n const deltaX = this.deltaX as number;\n const deltaY = this.deltaY as number;\n\n if (snapping.time > this.options.time)\n {\n finished = true;\n x = startX + deltaX;\n y = startY + deltaY;\n }\n else\n {\n const percent = this.ease(snapping.time, 0, 1, this.options.time);\n\n x = startX + (deltaX * percent);\n y = startY + (deltaY * percent);\n }\n if (this.options.topLeft)\n {\n this.parent.moveCorner(x, y);\n }\n else\n {\n this.parent.moveCenter(x, y);\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'snap' });\n\n if (finished)\n {\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap');\n }\n this.parent.emit('snap-end', this.parent);\n this.snapping = null;\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Point } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link SnapZoom}. */\nexport interface ISnapZoomOptions\n{\n /** the desired width to snap (to maintain aspect ratio, choose only width or height) */\n width?: number;\n\n /** the desired height to snap (to maintain aspect ratio, choose only width or height) */\n height?: number;\n\n /**\n * time for snapping in ms\n *\n * @default 1000\n */\n time?: number;\n\n /** ease function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /** Place this point at center during zoom instead of center of the viewport */\n center?: Point | null;\n\n /**\n * Pause snapping with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Removes this plugin after snapping is complete\n *\n * @default false\n */\n removeOnComplete?: boolean;\n\n /**\n * Removes this plugin if interrupted by any user input\n *\n * @default false\n */\n removeOnInterrupt?: boolean;\n\n /**\n * Starts the snap immediately regardless of whether the viewport is at the desired zoom\n *\n * @default false\n */\n forceStart?: boolean;\n\n /**\n * Zoom but do not move\n *\n * @default false\n */\n noMove?: boolean;\n}\n\nconst DEFAULT_SNAP_ZOOM_OPTIONS: Required = {\n width: 0,\n height: 0,\n time: 1000,\n ease: 'easeInOutSine',\n center: null,\n interrupt: true,\n removeOnComplete: false,\n removeOnInterrupt: false,\n forceStart: false,\n noMove: false\n};\n\n/**\n * @event snap-zoom-start(Viewport) emitted each time a fit animation starts\n * @event snap-zoom-end(Viewport) emitted each time fit reaches its target\n * @event snap-zoom-end(Viewport) emitted each time fit reaches its target\n */\nexport class SnapZoom extends Plugin\n{\n public readonly options: Required;\n\n protected ease: any;\n protected xScale: number;\n protected yScale: number;\n protected xIndependent: boolean;\n protected yIndependent: boolean;\n protected snapping?: {\n time: number;\n startX: number;\n startY: number;\n deltaX: number;\n deltaY: number;\n } | null;\n\n /**\n * This is called by {@link Viewport.snapZoom}.\n */\n constructor(parent: Viewport, options: ISnapZoomOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_SNAP_ZOOM_OPTIONS, options);\n this.ease = ease(this.options.ease);\n\n // Assign defaults for typescript.\n this.xIndependent = false;\n this.yIndependent = false;\n this.xScale = 0;\n this.yScale = 0;\n\n if (this.options.width > 0)\n {\n this.xScale = parent.screenWidth / this.options.width;\n this.xIndependent = true;\n }\n if (this.options.height > 0)\n {\n this.yScale = parent.screenHeight / this.options.height;\n this.yIndependent = true;\n }\n\n this.xScale = this.xIndependent ? (this.xScale as number) : (this.yScale as number);\n this.yScale = this.yIndependent ? (this.yScale as number) : this.xScale;\n\n if (this.options.time === 0)\n {\n // TODO: Fix this\n // @ts-expect-error todo\n parent.container.scale.x = this.xScale;\n\n // TODO: Fix this\n // @ts-expect-error todo\n parent.container.scale.y = this.yScale;\n\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n }\n else if (options.forceStart)\n {\n this.createSnapping();\n }\n }\n\n private createSnapping(): void\n {\n const startWorldScreenWidth = this.parent.worldScreenWidth;\n const startWorldScreenHeight = this.parent.worldScreenHeight;\n const endWorldScreenWidth = this.parent.screenWidth / this.xScale;\n const endWorldScreenHeight = this.parent.screenHeight / this.yScale;\n\n this.snapping = {\n time: 0,\n startX: startWorldScreenWidth,\n startY: startWorldScreenHeight,\n deltaX: endWorldScreenWidth - startWorldScreenWidth,\n deltaY: endWorldScreenHeight - startWorldScreenHeight\n };\n\n this.parent.emit('snap-zoom-start', this.parent);\n }\n\n public resize(): void\n {\n this.snapping = null;\n\n if (this.options.width > 0)\n {\n this.xScale = this.parent.screenWidth / this.options.width;\n }\n if (this.options.height > 0)\n {\n this.yScale = this.parent.screenHeight / this.options.height;\n }\n this.xScale = this.xIndependent ? this.xScale : this.yScale;\n this.yScale = this.yIndependent ? this.yScale : this.xScale;\n }\n\n public wheel(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n\n return false;\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n else if (this.options.interrupt)\n {\n this.snapping = null;\n }\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n if (this.options.interrupt && this.parent.input.count() !== 0)\n {\n return;\n }\n\n let oldCenter: Point | undefined;\n\n if (!this.options.center && !this.options.noMove)\n {\n oldCenter = this.parent.center;\n }\n if (!this.snapping)\n {\n if (this.parent.scale.x !== this.xScale || this.parent.scale.y !== this.yScale)\n {\n this.createSnapping();\n }\n }\n else if (this.snapping)\n {\n const snapping = this.snapping;\n\n snapping.time += elapsed;\n\n if (snapping.time >= this.options.time)\n {\n this.parent.scale.set(this.xScale, this.yScale);\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n this.parent.emit('snap-zoom-end', this.parent);\n this.snapping = null;\n }\n else\n {\n const snapping = this.snapping;\n const worldScreenWidth = this.ease(snapping.time, snapping.startX, snapping.deltaX, this.options.time);\n const worldScreenHeight = this.ease(snapping.time, snapping.startY, snapping.deltaY, this.options.time);\n\n this.parent.scale.x = this.parent.screenWidth / worldScreenWidth;\n this.parent.scale.y = this.parent.screenHeight / worldScreenHeight;\n }\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (!this.options.noMove)\n {\n if (!this.options.center)\n {\n this.parent.moveCenter(oldCenter as Point);\n }\n else\n {\n this.parent.moveCenter(this.options.center);\n }\n }\n }\n }\n\n public resume(): void\n {\n this.snapping = null;\n super.resume();\n }\n}\n","import { Plugin } from './Plugin';\nimport { IPointData, Point } from '@pixi/math';\n\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Wheel}. */\nexport interface IWheelOptions\n{\n /**\n * Percent to scroll with each spin\n *\n * @default 0.1\n */\n percent?: number;\n\n /**\n * smooth the zooming by providing the number of frames to zoom between wheel spins\n *\n * @default false\n */\n smooth?: false | number;\n\n /**\n * Stop smoothing with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Reverse the direction of the scroll\n *\n * @default false\n */\n reverse?: boolean;\n\n /**\n * Place this point at center during zoom instead of current mouse position\n *\n * @default null\n */\n center?: Point | null;\n\n /**\n * Scaling factor for non-DOM_DELTA_PIXEL scrolling events\n *\n * @default 20\n */\n lineHeight?: number;\n\n /**\n * Axis to zoom\n *\n * @default 'all'\n */\n axis?: 'all' | 'x' | 'y';\n}\n\nconst DEFAULT_WHEEL_OPTIONS: Required = {\n percent: 0.1,\n smooth: false,\n interrupt: true,\n reverse: false,\n center: null,\n lineHeight: 20,\n axis: 'all',\n};\n\n/**\n * Plugin for handling wheel scrolling for viewport zoom.\n *\n * @event wheel({wheel: {dx, dy, dz}, event, viewport})\n */\nexport class Wheel extends Plugin\n{\n public readonly options: Required;\n\n protected smoothing?: IPointData | null;\n protected smoothingCenter?: Point | null;\n protected smoothingCount?: number;\n\n /**\n * This is called by {@link Viewport.wheel}.\n */\n constructor(parent: Viewport, options: IWheelOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_WHEEL_OPTIONS, options);\n }\n\n public down(): boolean\n {\n if (this.options.interrupt)\n {\n this.smoothing = null;\n }\n\n return false;\n }\n\n protected isAxisX(): boolean\n {\n return ['all', 'x'].includes(this.options.axis);\n }\n\n protected isAxisY(): boolean\n {\n return ['all', 'y'].includes(this.options.axis);\n }\n\n public update(): void\n {\n if (this.smoothing)\n {\n const point = this.smoothingCenter;\n const change = this.smoothing;\n let oldPoint;\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point as IPointData);\n }\n if (this.isAxisX())\n {\n this.parent.scale.x += change.x;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y += change.y;\n }\n\n this.parent.emit('zoomed', { viewport: this.parent, type: 'wheel' });\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += (point as IPointData).x - newPoint.x;\n this.parent.y += (point as IPointData).y - newPoint.y;\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n (this.smoothingCount as number)++;\n\n if (this.smoothingCount as number >= this.options.smooth)\n {\n this.smoothing = null;\n }\n }\n }\n\n public wheel(e: WheelEvent): boolean | undefined\n {\n if (this.paused)\n {\n return;\n }\n\n const point = this.parent.input.getPointerPosition(e);\n const sign = this.options.reverse ? -1 : 1;\n const step = sign * -e.deltaY * (e.deltaMode ? this.options.lineHeight : 1) / 500;\n const change = Math.pow(2, (1 + this.options.percent) * step);\n\n if (this.options.smooth)\n {\n const original = {\n x: this.smoothing ? this.smoothing.x * (this.options.smooth - (this.smoothingCount as number)) : 0,\n y: this.smoothing ? this.smoothing.y * (this.options.smooth - (this.smoothingCount as number)) : 0\n };\n\n this.smoothing = {\n x: ((this.parent.scale.x + original.x) * change - this.parent.scale.x) / this.options.smooth,\n y: ((this.parent.scale.y + original.y) * change - this.parent.scale.y) / this.options.smooth,\n };\n this.smoothingCount = 0;\n this.smoothingCenter = point;\n }\n else\n {\n let oldPoint: IPointData | undefined;\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point);\n }\n if (this.isAxisX())\n {\n this.parent.scale.x *= change;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y *= change;\n }\n this.parent.emit('zoomed', { viewport: this.parent, type: 'wheel' });\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += point.x - newPoint.x;\n this.parent.y += point.y - newPoint.y;\n }\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n this.parent.emit('wheel', { wheel: { dx: e.deltaX, dy: e.deltaY, dz: e.deltaZ }, event: e, viewport: this.parent });\n\n if (!this.parent.options.passiveWheel)\n {\n return true;\n }\n }\n}\n","import { Point, Rectangle } from '@pixi/math';\n\nimport type { IPointData } from '@pixi/math';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { Viewport } from './Viewport';\n\nexport interface IViewportTouch {\n id: number;\n last: IPointData | null;\n}\n\n/**\n * Handles all input for Viewport\n *\n * @internal\n * @ignore\n * @private\n */\nexport class InputManager\n{\n public readonly viewport: Viewport;\n\n public clickedAvailable?: boolean;\n public isMouseDown?: boolean;\n public last?: Point | null;\n public wheelFunction?: (e: WheelEvent) => void;\n /** List of active touches on viewport */\n public touches: IViewportTouch[];\n\n constructor(viewport: Viewport)\n {\n this.viewport = viewport;\n this.touches = [];\n\n this.addListeners();\n }\n\n /** Add input listeners */\n private addListeners()\n {\n this.viewport.interactive = true;\n if (!this.viewport.forceHitArea)\n {\n this.viewport.hitArea = new Rectangle(0, 0, this.viewport.worldWidth, this.viewport.worldHeight);\n }\n this.viewport.on('pointerdown', this.down, this);\n this.viewport.on('pointermove', this.move, this);\n this.viewport.on('pointerup', this.up, this);\n this.viewport.on('pointerupoutside', this.up, this);\n this.viewport.on('pointercancel', this.up, this);\n this.viewport.on('pointerout', this.up, this);\n this.wheelFunction = (e) => this.handleWheel(e);\n this.viewport.options.divWheel.addEventListener(\n 'wheel',\n this.wheelFunction as any,\n { passive: this.viewport.options.passiveWheel });\n this.isMouseDown = false;\n }\n\n /**\n * Removes all event listeners from viewport\n * (useful for cleanup of wheel when removing viewport)\n */\n public destroy()\n {\n this.viewport.options.divWheel.removeEventListener('wheel', this.wheelFunction as any);\n }\n\n /**\n * handle down events for viewport\n *\n * @param {PIXI.InteractionEvent} event\n */\n public down(event: InteractionEvent)\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n if (event.data.pointerType === 'mouse')\n {\n this.isMouseDown = true;\n }\n else if (!this.get(event.data.pointerId))\n {\n this.touches.push({ id: event.data.pointerId, last: null });\n }\n if (this.count() === 1)\n {\n this.last = event.data.global.clone();\n\n // clicked event does not fire if viewport is decelerating or bouncing\n const decelerate = this.viewport.plugins.get('decelerate', true);\n const bounce = this.viewport.plugins.get('bounce', true);\n\n if ((!decelerate || !decelerate.isActive()) && (!bounce || !bounce.isActive()))\n {\n this.clickedAvailable = true;\n }\n else\n {\n this.clickedAvailable = false;\n }\n }\n else\n {\n this.clickedAvailable = false;\n }\n\n const stop = this.viewport.plugins.down(event);\n\n if (stop && this.viewport.options.stopPropagation)\n {\n event.stopPropagation();\n }\n }\n\n /** Clears all pointer events */\n public clear(): void\n {\n this.isMouseDown = false;\n this.touches = [];\n this.last = null;\n }\n\n /**\n * @param {number} change\n * @returns whether change exceeds threshold\n */\n public checkThreshold(change: number): boolean\n {\n if (Math.abs(change) >= this.viewport.threshold)\n {\n return true;\n }\n\n return false;\n }\n\n /** Handle move events for viewport */\n public move(event: InteractionEvent): void\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n\n const stop = this.viewport.plugins.move(event);\n\n if (this.clickedAvailable && this.last)\n {\n const distX = event.data.global.x - this.last.x;\n const distY = event.data.global.y - this.last.y;\n\n if (this.checkThreshold(distX) || this.checkThreshold(distY))\n {\n this.clickedAvailable = false;\n }\n }\n\n if (stop && this.viewport.options.stopPropagation)\n {\n event.stopPropagation();\n }\n }\n\n /** Handle up events for viewport */\n public up(event: InteractionEvent): void\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n\n if (event.data.pointerType === 'mouse')\n {\n this.isMouseDown = false;\n }\n\n if (event.data.pointerType !== 'mouse')\n {\n this.remove(event.data.pointerId);\n }\n\n const stop = this.viewport.plugins.up(event);\n\n if (this.clickedAvailable && this.count() === 0 && this.last)\n {\n this.viewport.emit('clicked', {\n event,\n screen: this.last,\n world: this.viewport.toWorld(this.last),\n viewport: this\n });\n this.clickedAvailable = false;\n }\n\n if (stop && this.viewport.options.stopPropagation)\n {\n event.stopPropagation();\n }\n }\n\n /** Gets pointer position if this.interaction is set */\n public getPointerPosition(event: WheelEvent): Point\n {\n const point = new Point();\n\n if (this.viewport.options.interaction)\n {\n this.viewport.options.interaction.mapPositionToPoint(point, event.clientX, event.clientY);\n }\n else\n {\n point.x = event.clientX;\n point.y = event.clientY;\n }\n\n return point;\n }\n\n /** Handle wheel events */\n public handleWheel(event: WheelEvent): void\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n\n // do not handle events coming from other elements\n if (this.viewport.options.interaction\n && (this.viewport.options.interaction as any).interactionDOMElement !== event.target)\n {\n return;\n }\n\n // only handle wheel events where the mouse is over the viewport\n const point = this.viewport.toLocal(this.getPointerPosition(event));\n\n if (this.viewport.left <= point.x\n && point.x <= this.viewport.right\n && this.viewport.top <= point.y\n && point.y <= this.viewport.bottom)\n {\n const stop = this.viewport.plugins.wheel(event);\n\n if (stop && !this.viewport.options.passiveWheel)\n {\n event.preventDefault();\n }\n }\n }\n\n public pause(): void\n {\n this.touches = [];\n this.isMouseDown = false;\n }\n\n /** Get touch by id */\n public get(id: number): IViewportTouch | null\n {\n for (const touch of this.touches)\n {\n if (touch.id === id)\n {\n return touch;\n }\n }\n\n return null;\n }\n\n /** Remove touch by number */\n remove(id: number): void\n {\n for (let i = 0; i < this.touches.length; i++)\n {\n if (this.touches[i].id === id)\n {\n this.touches.splice(i, 1);\n\n return;\n }\n }\n }\n\n /**\n * @returns {number} count of mouse/touch pointers that are down on the viewport\n */\n count(): number\n {\n return (this.isMouseDown ? 1 : 0) + this.touches.length;\n }\n}\n","import type {\n Animate,\n Bounce,\n Clamp,\n ClampZoom,\n Decelerate,\n Drag,\n Follow,\n MouseEdges,\n Pinch,\n Plugin,\n Snap,\n SnapZoom,\n Wheel,\n} from './plugins';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { Viewport } from './Viewport';\n\nconst PLUGIN_ORDER = [\n 'drag',\n 'pinch',\n 'wheel',\n 'follow',\n 'mouse-edges',\n 'decelerate',\n 'aniamte',\n 'bounce',\n 'snap-zoom',\n 'clamp-zoom',\n 'snap',\n 'clamp',\n];\n\n/**\n * Use this to access current plugins or add user-defined plugins\n *\n * @public\n */\nexport class PluginManager\n{\n /** Maps mounted plugins by their type */\n public plugins: Partial>;\n\n /**\n * List of plugins mounted\n *\n * This list is kept sorted by the internal priority of plugins (hard-coded).\n */\n public list: Array;\n\n /** The viewport using the plugins managed by `this`. */\n public readonly viewport: Viewport;\n\n /** This is called by {@link Viewport} to initialize the {@link Viewport.plugins plugins}. */\n constructor(viewport: Viewport)\n {\n this.viewport = viewport;\n this.list = [];\n this.plugins = {};\n }\n\n /**\n * Inserts a named plugin or a user plugin into the viewport\n * default plugin order: 'drag', 'pinch', 'wheel', 'follow', 'mouse-edges', 'decelerate', 'bounce',\n * 'snap-zoom', 'clamp-zoom', 'snap', 'clamp'\n *\n * @param {string} name of plugin\n * @param {Plugin} plugin - instantiated Plugin class\n * @param {number} index to insert userPlugin (otherwise inserts it at the end)\n */\n public add(name: string, plugin: Plugin, index: number = PLUGIN_ORDER.length)\n {\n this.plugins[name] = plugin;\n\n const current = PLUGIN_ORDER.indexOf(name);\n\n if (current !== -1)\n {\n PLUGIN_ORDER.splice(current, 1);\n }\n\n PLUGIN_ORDER.splice(index, 0, name);\n this.sort();\n }\n\n public get(name: 'animate', ignorePaused?: boolean): Animate | undefined | null;\n public get(name: 'bounce', ignorePaused?: boolean): Bounce | undefined | null;\n public get(name: 'clamp', ignorePaused?: boolean): Clamp | undefined | null;\n public get(name: 'clamp-zoom', ignorePaused?: boolean): ClampZoom | undefined | null;\n public get(name: 'decelerate', ignorePaused?: boolean): Decelerate | undefined | null;\n public get(name: 'drag', ignorePaused?: boolean): Drag | undefined | null;\n public get(name: 'follow', ignorePaused?: boolean): Follow | undefined | null;\n public get(name: 'mouse-edges', ignorePaused?: boolean): MouseEdges | undefined | null;\n public get(name: 'pinch', ignorePaused?: boolean): Pinch | undefined | null;\n public get(name: 'snap', ignorePaused?: boolean): Snap | undefined | null;\n public get(name: 'snap-zoom', ignorePaused?: boolean): SnapZoom | undefined | null;\n public get(name: 'wheel', ignorePaused?: boolean): Wheel | undefined | null;\n public get(name: string, ignorePaused?: boolean): T | undefined | null;\n\n /**\n * Get plugin\n *\n * @param {string} name of plugin\n * @param {boolean} [ignorePaused] return null if plugin is paused\n */\n public get(name: string, ignorePaused?: boolean): T | undefined | null\n {\n if (ignorePaused)\n {\n if (this.plugins[name]?.paused)\n {\n return null;\n }\n }\n\n return this.plugins[name] as T;\n }\n\n /**\n * Update all active plugins\n *\n * @internal\n * @ignore\n * @param {number} elapsed type in milliseconds since last update\n */\n public update(elapsed: number): void\n {\n for (const plugin of this.list)\n {\n plugin.update(elapsed);\n }\n }\n\n /**\n * Resize all active plugins\n *\n * @internal\n * @ignore\n */\n public resize(): void\n {\n for (const plugin of this.list)\n {\n plugin.resize();\n }\n }\n\n /** Clamps and resets bounce and decelerate (as needed) after manually moving viewport */\n public reset(): void\n {\n for (const plugin of this.list)\n {\n plugin.reset();\n }\n }\n\n /** removes all installed plugins */\n public removeAll(): void\n {\n this.plugins = {};\n this.sort();\n }\n\n /**\n * Removes installed plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public remove(name: string): void\n {\n if (this.plugins[name])\n {\n delete this.plugins[name];\n this.viewport.emit(`${name}-remove`);\n this.sort();\n }\n }\n\n /**\n * Pause plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public pause(name: string): void\n {\n this.plugins[name]?.pause();\n }\n\n /**\n * Resume plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public resume(name: string): void\n {\n this.plugins[name]?.resume();\n }\n\n /**\n * Sort plugins according to PLUGIN_ORDER\n *\n * @internal\n * @ignore\n */\n public sort()\n {\n this.list = [];\n\n for (const plugin of PLUGIN_ORDER)\n {\n if (this.plugins[plugin])\n {\n this.list.push(this.plugins[plugin] as Plugin);\n }\n }\n }\n\n /**\n * Handle down for all plugins\n *\n * @internal\n * @ignore\n */\n public down(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.list)\n {\n if (plugin.down(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle move for all plugins\n *\n * @internal\n * @ignore\n */\n public move(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.viewport.plugins.list)\n {\n if (plugin.move(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle up for all plugins\n *\n * @internal\n * @ignore\n */\n public up(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.list)\n {\n if (plugin.up(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle wheel event for all plugins\n *\n * @internal\n * @ignore\n */\n public wheel(e: WheelEvent): boolean\n {\n let result = false;\n\n for (const plugin of this.list)\n {\n if (plugin.wheel(e))\n {\n result = true;\n }\n }\n\n return result;\n }\n}\n","import { Container } from '@pixi/display';\nimport { Point, Rectangle } from '@pixi/math';\nimport { Ticker } from '@pixi/ticker';\n\nimport { InputManager } from './InputManager';\nimport { PluginManager } from './PluginManager';\nimport {\n Animate,\n Bounce,\n Clamp,\n ClampZoom,\n Decelerate,\n Drag,\n Follow,\n MouseEdges,\n Pinch,\n Snap,\n SnapZoom,\n Wheel\n} from './plugins';\n\nimport type { DisplayObject, IDestroyOptions } from '@pixi/display';\nimport type { IHitArea, InteractionManager } from '@pixi/interaction';\n\n/** Options for {@link Viewport}. */\nexport interface IViewportOptions {\n /** @default window.innerWidth */\n screenWidth?: number;\n\n /** @default window.innerHeight */\n screenHeight?: number;\n\n /** @default this.width */\n worldWidth?: number | null;\n\n /** @default this.height */\n worldHeight?: number | null;\n\n /**\n * Number of pixels to move to trigger an input event (e.g., drag, pinch) or disable a clicked event\n *\n * @default 5\n */\n threshold?: number;\n\n /**\n * Whether the 'wheel' event is set to passive (note: if false, e.preventDefault() will be called when wheel is used over the viewport)\n *\n * @default true\n */\n passiveWheel?: boolean;\n\n /**\n * Whether to stopPropagation of events that impact the viewport (except wheel events, see options.passiveWheel)\n */\n stopPropagation?: boolean;\n\n /**\n * Change the default hitArea from world size to a new value\n */\n forceHitArea?: Rectangle | null;\n\n /**\n * Set this if you want to manually call update() function on each frame\n *\n * @default false\n */\n noTicker?: boolean;\n\n /**\n * InteractionManager, available from instantiated `WebGLRenderer/CanvasRenderer.plugins.interaction`\n *\n * It's used to calculate pointer postion relative to canvas location on screen\n */\n interaction?: InteractionManager | null;\n\n /**\n * Remove oncontextmenu=() => {} from the divWheel element\n */\n disableOnContextMenu?: boolean;\n\n /**\n * div to attach the wheel event\n *\n * @default document.body\n */\n divWheel?: HTMLElement;\n\n /**\n * Use this PIXI.ticker for updates\n *\n * @default PIXI.Ticker.shared\n */\n ticker?: Ticker;\n}\n\nexport interface ICompleteViewportOptions extends IViewportOptions {\n screenWidth: number;\n screenHeight: number;\n threshold: number;\n passiveWheel: boolean;\n stopPropagation: boolean;\n noTicker: boolean;\n ticker: Ticker;\n}\n\nexport interface IViewportTransformState {\n x: number;\n y: number;\n scaleX: number;\n scaleY: number;\n}\n\nconst DEFAULT_VIEWPORT_OPTIONS: ICompleteViewportOptions = {\n screenWidth: window.innerWidth,\n screenHeight: window.innerHeight,\n worldWidth: null,\n worldHeight: null,\n threshold: 5,\n passiveWheel: true,\n stopPropagation: false,\n forceHitArea: null,\n noTicker: false,\n interaction: null,\n disableOnContextMenu: false,\n ticker: Ticker.shared,\n};\n\n/**\n * Main class to use when creating a Viewport\n *\n * @public\n * @fires clicked\n * @fires drag-start\n * @fires drag-end\n * @fires drag-remove\n * @fires pinch-start\n * @fires pinch-end\n * @fires pinch-remove\n * @fires snap-start\n * @fires snap-end\n * @fires snap-remove\n * @fires snap-zoom-start\n * @fires snap-zoom-end\n * @fires snap-zoom-remove\n * @fires bounce-x-start\n * @fires bounce-x-end\n * @fires bounce-y-start\n * @fires bounce-y-end\n * @fires bounce-remove\n * @fires wheel\n * @fires wheel-remove\n * @fires wheel-scroll\n * @fires wheel-scroll-remove\n * @fires mouse-edge-start\n * @fires mouse-edge-end\n * @fires mouse-edge-remove\n * @fires moved\n * @fires moved-end\n * @fires zoomed\n * @fires zoomed-end\n * @fires frame-end\n */\nexport class Viewport extends Container\n{\n /** Flags whether the viewport is being panned */\n public moving?: boolean;\n\n public screenWidth: number;\n public screenHeight: number;\n\n /** Number of pixels to move to trigger an input event (e.g., drag, pinch) or disable a clicked event */\n public threshold: number;\n\n public readonly input: InputManager;\n\n /** Use this to add user plugins or access existing plugins (e.g., to pause, resume, or remove them) */\n public readonly plugins: PluginManager;\n\n /** Flags whether the viewport zoom is being changed. */\n public zooming?: boolean;\n\n public lastViewport?: IViewportTransformState | null;\n\n /** The options passed when creating this viewport, merged with the default values */\n public readonly options: ICompleteViewportOptions & { divWheel: HTMLElement };\n\n private _dirty?: boolean;\n private _forceHitArea?: IHitArea | null;\n private _hitAreaDefault?: Rectangle;\n private _pause?: boolean;\n private readonly tickerFunction?: () => void;\n private _worldWidth?: number | null;\n private _worldHeight?: number | null;\n\n /**\n * @param options\n */\n constructor(options: IViewportOptions = {})\n {\n super();\n this.options = Object.assign(\n {},\n { divWheel: document.body },\n DEFAULT_VIEWPORT_OPTIONS,\n options\n );\n\n this.screenWidth = this.options.screenWidth;\n this.screenHeight = this.options.screenHeight;\n\n this._worldWidth = this.options.worldWidth;\n this._worldHeight = this.options.worldHeight;\n this.forceHitArea = this.options.forceHitArea;\n this.threshold = this.options.threshold;\n\n this.options.divWheel = this.options.divWheel || document.body;\n\n if (this.options.disableOnContextMenu)\n {\n this.options.divWheel.oncontextmenu = (e) => e.preventDefault();\n }\n if (!this.options.noTicker)\n {\n this.tickerFunction = () => this.update(this.options.ticker.elapsedMS);\n this.options.ticker.add(this.tickerFunction);\n }\n\n this.input = new InputManager(this);\n this.plugins = new PluginManager(this);\n }\n\n /** Overrides PIXI.Container's destroy to also remove the 'wheel' and PIXI.Ticker listeners */\n destroy(options: IDestroyOptions)\n {\n if (!this.options.noTicker && this.tickerFunction)\n {\n this.options.ticker.remove(this.tickerFunction);\n }\n\n this.input.destroy();\n super.destroy(options);\n }\n\n /**\n * Update viewport on each frame.\n *\n * By default, you do not need to call this unless you set `options.noTicker=true`.\n *\n * @param {number} elapsed time in milliseconds since last update\n */\n update(elapsed: number): void\n {\n if (!this.pause)\n {\n this.plugins.update(elapsed);\n\n if (this.lastViewport)\n {\n // Check for moved-end event\n if (this.lastViewport.x !== this.x || this.lastViewport.y !== this.y)\n {\n this.moving = true;\n }\n else if (this.moving)\n {\n this.emit('moved-end', this);\n this.moving = false;\n }\n\n // Check for zoomed-end event\n if (this.lastViewport.scaleX !== this.scale.x || this.lastViewport.scaleY !== this.scale.y)\n {\n this.zooming = true;\n }\n else if (this.zooming)\n {\n this.emit('zoomed-end', this);\n this.zooming = false;\n }\n }\n\n if (!this.forceHitArea)\n {\n this._hitAreaDefault = new Rectangle(this.left, this.top, this.worldScreenWidth, this.worldScreenHeight);\n this.hitArea = this._hitAreaDefault;\n }\n\n this._dirty = this._dirty || !this.lastViewport\n || this.lastViewport.x !== this.x || this.lastViewport.y !== this.y\n || this.lastViewport.scaleX !== this.scale.x || this.lastViewport.scaleY !== this.scale.y;\n\n this.lastViewport = {\n x: this.x,\n y: this.y,\n scaleX: this.scale.x,\n scaleY: this.scale.y\n };\n this.emit('frame-end', this);\n }\n }\n\n /** Use this to set screen and world sizes, needed for pinch/wheel/clamp/bounce. */\n resize(\n screenWidth = window.innerWidth,\n screenHeight = window.innerHeight,\n worldWidth: number,\n worldHeight: number\n ): void\n {\n this.screenWidth = screenWidth;\n this.screenHeight = screenHeight;\n\n if (typeof worldWidth !== 'undefined')\n {\n this._worldWidth = worldWidth;\n }\n if (typeof worldHeight !== 'undefined')\n {\n this._worldHeight = worldHeight;\n }\n\n this.plugins.resize();\n this.dirty = true;\n }\n\n /** World width, in pixels */\n get worldWidth(): number\n {\n if (this._worldWidth)\n {\n return this._worldWidth;\n }\n\n return this.width / this.scale.x;\n }\n set worldWidth(value)\n {\n this._worldWidth = value;\n this.plugins.resize();\n }\n\n /** World height, in pixels */\n get worldHeight(): number\n {\n if (this._worldHeight)\n {\n return this._worldHeight;\n }\n\n return this.height / this.scale.y;\n }\n set worldHeight(value)\n {\n this._worldHeight = value;\n this.plugins.resize();\n }\n\n /** Get visible world bounds of viewport */\n public getVisibleBounds(): Rectangle\n {\n return new Rectangle(this.left, this.top, this.worldScreenWidth, this.worldScreenHeight);\n }\n\n /** Change coordinates from screen to world */\n public toWorld(x: number, y: number): Point;\n /** Change coordinates from screen to world */\n public toWorld(screenPoint: Point): Point;\n\n public toWorld(x: number | Point, y?: number): Point\n {\n if (arguments.length === 2)\n {\n return this.toLocal(new Point(x as number, y));\n }\n\n return this.toLocal(x as Point);\n }\n\n /** Change coordinates from world to screen */\n public toScreen(x: number, y: number): Point;\n /** Change coordinates from world to screen */\n public toScreen(worldPoint: Point): Point;\n\n public toScreen(x: number | Point, y?: number): Point\n {\n if (arguments.length === 2)\n {\n return this.toGlobal(new Point(x as number, y));\n }\n\n return this.toGlobal(x as Point);\n }\n\n /** Screen width in world coordinates */\n get worldScreenWidth(): number\n {\n return this.screenWidth / this.scale.x;\n }\n\n /** Screen height in world coordinates */\n get worldScreenHeight(): number\n {\n return this.screenHeight / this.scale.y;\n }\n\n /** World width in screen coordinates */\n get screenWorldWidth(): number\n {\n return this.worldWidth * this.scale.x;\n }\n\n /** World height in screen coordinates */\n get screenWorldHeight()\n {\n return this.worldHeight * this.scale.y;\n }\n\n /** Center of screen in world coordinates */\n get center(): Point\n {\n return new Point(\n this.worldScreenWidth / 2 - this.x / this.scale.x,\n this.worldScreenHeight / 2 - this.y / this.scale.y,\n );\n }\n set center(value: Point)\n {\n this.moveCenter(value);\n }\n\n /** Move center of viewport to (x, y) */\n public moveCenter(x: number, y: number): this;\n\n /** Move center of viewport to {@code center}. */\n public moveCenter(center: Point): this;\n\n public moveCenter(...args: [number, number] | [Point]): this\n {\n let x: number;\n let y: number;\n\n if (typeof args[0] === 'number')\n {\n x = args[0];\n y = args[1] as number;\n }\n else\n {\n x = args[0].x;\n y = args[0].y;\n }\n\n const newX = (this.worldScreenWidth / 2 - x) * this.scale.x;\n const newY = (this.worldScreenHeight / 2 - y) * this.scale.y;\n\n if (this.x !== newX || this.y !== newY)\n {\n this.position.set(newX, newY);\n this.plugins.reset();\n this.dirty = true;\n }\n\n return this;\n }\n\n /** Top-left corner of Viewport */\n get corner(): Point\n {\n return new Point(-this.x / this.scale.x, -this.y / this.scale.y);\n }\n set corner(value: Point)\n {\n this.moveCorner(value);\n }\n\n /** Move viewport's top-left corner; also clamps and resets decelerate and bounce (as needed) */\n public moveCorner(x: number, y: number): this;\n\n /** move viewport's top-left corner; also clamps and resets decelerate and bounce (as needed) */\n public moveCorner(center: Point): this;\n\n public moveCorner(...args: [number, number] | [Point])\n {\n let x;\n let y;\n\n if (args.length === 1)\n {\n x = -args[0].x * this.scale.x;\n y = -args[0].y * this.scale.y;\n }\n else\n {\n x = -args[0] * this.scale.x;\n y = -args[1] * this.scale.y;\n }\n\n if (x !== this.x || y !== this.y)\n {\n this.position.set(x, y);\n this.plugins.reset();\n this.dirty = true;\n }\n\n return this;\n }\n\n /** Get how many world pixels fit in screen's width */\n get screenWidthInWorldPixels()\n {\n return this.screenWidth / this.scale.x;\n }\n\n /** Get how many world pixels fit on screen's height */\n get screenHeightInWorldPixels()\n {\n return this.screenHeight / this.scale.y;\n }\n\n /**\n * Find the scale value that fits a world width on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param width - Width in world pixels\n * @return - scale\n */\n findFitWidth(width: number): number\n {\n return this.screenWidth / width;\n }\n\n /**\n * Finds the scale value that fits a world height on the screens\n * does not change the viewport (use fit... to change)\n *\n * @param height - Height in world pixels\n * @return - scale\n */\n findFitHeight(height: number): number\n {\n return this.screenHeight / height;\n }\n\n /**\n * Finds the scale value that fits the smaller of a world width and world height on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param {number} width in world pixels\n * @param {number} height in world pixels\n * @returns {number} scale\n */\n findFit(width: number, height: number): number\n {\n const scaleX = this.screenWidth / width;\n const scaleY = this.screenHeight / height;\n\n return Math.min(scaleX, scaleY);\n }\n\n /**\n * Finds the scale value that fits the larger of a world width and world height on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param {number} width in world pixels\n * @param {number} height in world pixels\n * @returns {number} scale\n */\n findCover(width: number, height: number): number\n {\n const scaleX = this.screenWidth / width;\n const scaleY = this.screenHeight / height;\n\n return Math.max(scaleX, scaleY);\n }\n\n /**\n * Change zoom so the width fits in the viewport\n *\n * @param width - width in world coordinates\n * @param center - maintain the same center\n * @param scaleY - whether to set scaleY=scaleX\n * @param noClamp - whether to disable clamp-zoom\n * @returns {Viewport} this\n */\n fitWidth(width = this.worldWidth, center?: boolean, scaleY = true, noClamp?: boolean): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.x = this.screenWidth / width;\n\n if (scaleY)\n {\n this.scale.y = this.scale.x;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (!noClamp && clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so the height fits in the viewport\n *\n * @param {number} [height=this.worldHeight] in world coordinates\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @param {boolean} [scaleX=true] whether to set scaleX = scaleY\n * @param {boolean} [noClamp] whether to disable clamp-zoom\n * @returns {Viewport} this\n */\n fitHeight(height = this.worldHeight, center?: boolean, scaleX = true, noClamp?: boolean)\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.y = this.screenHeight / height;\n\n if (scaleX)\n {\n this.scale.x = this.scale.y;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (!noClamp && clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so it fits the entire world in the viewport\n *\n * @param {boolean} center maintain the same center of the screen after zoom\n * @returns {Viewport} this\n */\n fitWorld(center?: boolean): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n\n this.scale.x = this.screenWidth / this.worldWidth;\n this.scale.y = this.screenHeight / this.worldHeight;\n\n if (this.scale.x < this.scale.y)\n {\n this.scale.y = this.scale.x;\n }\n else\n {\n this.scale.x = this.scale.y;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so it fits the size or the entire world in the viewport\n *\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @param {number} [width=this.worldWidth] desired width\n * @param {number} [height=this.worldHeight] desired height\n * @returns {Viewport} this\n */\n fit(center?: boolean, width = this.worldWidth, height = this.worldHeight): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n\n this.scale.x = this.screenWidth / width;\n this.scale.y = this.screenHeight / height;\n\n if (this.scale.x < this.scale.y)\n {\n this.scale.y = this.scale.x;\n }\n else\n {\n this.scale.x = this.scale.y;\n }\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n // eslint-disable-next-line\n // @ts-ignore\n set visible(value: boolean)\n {\n if (!value)\n {\n this.input.clear();\n }\n\n super.visible = value;\n }\n\n /**\n * Zoom viewport to specific value.\n *\n * @param {number} scale value (e.g., 1 would be 100%, 0.25 would be 25%)\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n setZoom(scale: number, center?: boolean): this\n {\n let save;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.set(scale);\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Zoom viewport by a certain percent (in both x and y direction).\n *\n * @param {number} percent change (e.g., 0.25 would increase a starting scale of 1.0 to 1.25)\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n zoomPercent(percent: number, center?: boolean): this\n {\n return this.setZoom(this.scale.x + this.scale.x * percent, center);\n }\n\n /**\n * Zoom viewport by increasing/decreasing width by a certain number of pixels.\n *\n * @param {number} change in pixels\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n zoom(change: number, center?: boolean)\n {\n this.fitWidth(change + this.worldScreenWidth, center);\n\n return this;\n }\n\n /** Changes scale of viewport and maintains center of viewport */\n get scaled(): number\n {\n return this.scale.x;\n }\n set scaled(scale: number)\n {\n this.setZoom(scale, true);\n }\n\n /**\n * @param {SnapZoomOptions} options\n */\n snapZoom(options: any): this\n {\n this.plugins.add('snap-zoom', new SnapZoom(this, options));\n\n return this;\n }\n\n /** Is container out of world bounds */\n OOB(): {\n left: boolean;\n right: boolean;\n top: boolean;\n bottom: boolean;\n cornerPoint: Point;\n }\n {\n return {\n left: this.left < 0,\n right: this.right > this.worldWidth,\n top: this.top < 0,\n bottom: this.bottom > this.worldHeight,\n cornerPoint: new Point(\n this.worldWidth * this.scale.x - this.screenWidth,\n this.worldHeight * this.scale.y - this.screenHeight\n )\n };\n }\n\n /** World coordinates of the right edge of the screen */\n get right(): number\n {\n return -this.x / this.scale.x + this.worldScreenWidth;\n }\n set right(value: number)\n {\n this.x = -value * this.scale.x + this.screenWidth;\n this.plugins.reset();\n }\n\n /** World coordinates of the left edge of the screen */\n get left(): number\n {\n return -this.x / this.scale.x;\n }\n set left(value: number)\n {\n this.x = -value * this.scale.x;\n this.plugins.reset();\n }\n\n /** World coordinates of the top edge of the screen */\n get top(): number\n {\n return -this.y / this.scale.y;\n }\n set top(value: number)\n {\n this.y = -value * this.scale.y;\n this.plugins.reset();\n }\n\n /** World coordinates of the bottom edge of the screen */\n get bottom(): number\n {\n return -this.y / this.scale.y + this.worldScreenHeight;\n }\n set bottom(value: number)\n {\n this.y = -value * this.scale.y + this.screenHeight;\n this.plugins.reset();\n }\n\n /**\n * Determines whether the viewport is dirty (i.e., needs to be renderered to the screen because of a change)\n */\n get dirty(): boolean\n {\n return !!this._dirty;\n }\n set dirty(value: boolean)\n {\n this._dirty = value;\n }\n\n /**\n * Permanently changes the Viewport's hitArea\n *\n * NOTE: if not set then hitArea = PIXI.Rectangle(Viewport.left, Viewport.top, Viewport.worldScreenWidth, Viewport.worldScreenHeight)\n */\n get forceHitArea(): IHitArea | null | undefined\n {\n return this._forceHitArea;\n }\n set forceHitArea(value: IHitArea | null | undefined)\n {\n if (value)\n {\n this._forceHitArea = value;\n this.hitArea = value;\n }\n else\n {\n this._forceHitArea = null;\n this.hitArea = new Rectangle(0, 0, this.worldWidth, this.worldHeight);\n }\n }\n\n /**\n * Enable one-finger touch to drag\n *\n * NOTE: if you expect users to use right-click dragging, you should enable `viewport.options.disableOnContextMenu`\n * to avoid the context menu popping up on each right-click drag.\n *\n * @param {DragOptions} [options]\n * @returns {Viewport} this\n */\n public drag(options: any): this\n {\n this.plugins.add('drag', new Drag(this, options));\n\n return this;\n }\n\n /**\n * Clamp to world boundaries or other provided boundaries\n *\n * NOTES:\n * clamp is disabled if called with no options; use { direction: 'all' } for all edge clamping\n * screenWidth, screenHeight, worldWidth, and worldHeight needs to be set for this to work properly\n *\n * @param {ClampOptions} [options]\n * @returns {Viewport} this\n */\n public clamp(options: any): this\n {\n this.plugins.add('clamp', new Clamp(this, options));\n\n return this;\n }\n\n /**\n * Decelerate after a move\n *\n * NOTE: this fires 'moved' event during deceleration\n *\n * @param {DecelerateOptions} [options]\n * @return {Viewport} this\n */\n public decelerate(options: any): this\n {\n this.plugins.add('decelerate', new Decelerate(this, options));\n\n return this;\n }\n\n /**\n * Bounce on borders\n * NOTES:\n * screenWidth, screenHeight, worldWidth, and worldHeight needs to be set for this to work properly\n * fires 'moved', 'bounce-x-start', 'bounce-y-start', 'bounce-x-end', and 'bounce-y-end' events\n * @param {object} [options]\n * @param {string} [options.sides=all] all, horizontal, vertical, or combination of top, bottom, right, left (e.g., 'top-bottom-right')\n * @param {number} [options.friction=0.5] friction to apply to decelerate if active\n * @param {number} [options.time=150] time in ms to finish bounce\n * @param {object} [options.bounceBox] use this bounceBox instead of (0, 0, viewport.worldWidth, viewport.worldHeight)\n * @param {number} [options.bounceBox.x=0]\n * @param {number} [options.bounceBox.y=0]\n * @param {number} [options.bounceBox.width=viewport.worldWidth]\n * @param {number} [options.bounceBox.height=viewport.worldHeight]\n * @param {string|function} [options.ease=easeInOutSine] ease function or name (see http://easings.net/ for supported names)\n * @param {string} [options.underflow=center] (top/bottom/center and left/right/center, or center) where to place world if too small for screen\n * @return {Viewport} this\n */\n public bounce(options: any): this\n {\n this.plugins.add('bounce', new Bounce(this, options));\n\n return this;\n }\n\n /**\n * Enable pinch to zoom and two-finger touch to drag\n *\n * @param {PinchOptions} [options]\n * @return {Viewport} this\n */\n public pinch(options: any): this\n {\n this.plugins.add('pinch', new Pinch(this, options));\n\n return this;\n }\n\n /**\n * Snap to a point\n *\n * @param {number} x\n * @param {number} y\n * @param {SnapOptions} [options]\n * @return {Viewport} this\n */\n public snap(x: number, y: number, options: any): this\n {\n this.plugins.add('snap', new Snap(this, x, y, options));\n\n return this;\n }\n\n /**\n * Follow a target\n *\n * NOTES:\n * uses the (x, y) as the center to follow; for PIXI.Sprite to work properly, use sprite.anchor.set(0.5)\n * options.acceleration is not perfect as it doesn't know the velocity of the target\n * it adds acceleration to the start of movement and deceleration to the end of movement when the target is stopped\n * fires 'moved' event\n * @param {PIXI.DisplayObject} target to follow\n * @param {FollowOptions} [options]\n * @returns {Viewport} this\n */\n public follow(target: DisplayObject, options: any): this\n {\n this.plugins.add('follow', new Follow(this, target, options));\n\n return this;\n }\n\n /**\n * Zoom using mouse wheel\n *\n * @param {WheelOptions} [options]\n * @return {Viewport} this\n */\n public wheel(options: any): this\n {\n this.plugins.add('wheel', new Wheel(this, options));\n\n return this;\n }\n\n /**\n * Animate the position and/or scale of the viewport\n *\n * @param {AnimateOptions} options\n * @returns {Viewport} this\n */\n public animate(options: any): this\n {\n this.plugins.add('animate', new Animate(this, options));\n\n return this;\n }\n\n /**\n * Enable clamping of zoom to constraints\n *\n * The minWidth/Height settings are how small the world can get (as it would appear on the screen)\n * before clamping. The maxWidth/maxHeight is how larger the world can scale (as it would appear on\n * the screen) before clamping.\n *\n * For example, if you have a world size of 1000 x 1000 and a screen size of 100 x 100, if you set\n * minWidth/Height = 100 then the world will not be able to zoom smaller than the screen size (ie,\n * zooming out so it appears smaller than the screen). Similarly, if you set maxWidth/Height = 100\n * the world will not be able to zoom larger than the screen size (ie, zooming in so it appears\n * larger than the screen).\n *\n * @param {ClampZoomOptions} [options]\n * @return {Viewport} this\n */\n public clampZoom(options: any): this\n {\n this.plugins.add('clamp-zoom', new ClampZoom(this, options));\n\n return this;\n }\n\n /**\n * Scroll viewport when mouse hovers near one of the edges or radius-distance from center of screen.\n *\n * NOTE: fires 'moved' event\n *\n * @param {MouseEdgesOptions} [options]\n */\n public mouseEdges(options: any): this\n {\n this.plugins.add('mouse-edges', new MouseEdges(this, options));\n\n return this;\n }\n\n /** Pause viewport (including animation updates such as decelerate) */\n get pause(): boolean\n {\n return !!this._pause;\n }\n set pause(value: boolean)\n {\n this._pause = value;\n\n this.lastViewport = null;\n this.moving = false;\n this.zooming = false;\n\n if (value)\n {\n this.input.pause();\n }\n }\n\n /**\n * Move the viewport so the bounding box is visible\n *\n * @param x - left\n * @param y - top\n * @param width\n * @param height\n * @param resizeToFit - Resize the viewport so the box fits within the viewport\n */\n public ensureVisible(x: number, y: number, width: number, height: number, resizeToFit?: boolean)\n {\n if (resizeToFit && (width > this.worldScreenWidth || height > this.worldScreenHeight))\n {\n this.fit(true, width, height);\n this.emit('zoomed', { viewport: this, type: 'ensureVisible' });\n }\n let moved = false;\n\n if (x < this.left)\n {\n this.left = x;\n moved = true;\n }\n else if (x + width > this.right)\n {\n this.right = x + width;\n moved = true;\n }\n if (y < this.top)\n {\n this.top = y;\n moved = true;\n }\n else if (y + height > this.bottom)\n {\n this.bottom = y + height;\n moved = true;\n }\n if (moved)\n {\n this.emit('moved', { viewport: this, type: 'ensureVisible' });\n }\n }\n}\n\n/**\n * Fires after a mouse or touch click\n * @event Viewport#clicked\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a drag starts\n * @event Viewport#drag-start\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a drag ends\n * @event Viewport#drag-end\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a pinch starts\n * @event Viewport#pinch-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a pinch end\n * @event Viewport#pinch-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap starts\n * @event Viewport#snap-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap ends\n * @event Viewport#snap-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap-zoom starts\n * @event Viewport#snap-zoom-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap-zoom ends\n * @event Viewport#snap-zoom-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce starts in the x direction\n * @event Viewport#bounce-x-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce ends in the x direction\n * @event Viewport#bounce-x-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce starts in the y direction\n * @event Viewport#bounce-y-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce ends in the y direction\n * @event Viewport#bounce-y-end\n * @type {Viewport}\n */\n\n/**\n * Fires when for a mouse wheel event\n * @event Viewport#wheel\n * @type {object}\n * @property {object} wheel\n * @property {number} wheel.dx\n * @property {number} wheel.dy\n * @property {number} wheel.dz\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a wheel-scroll occurs\n * @event Viewport#wheel-scroll\n * @type {Viewport}\n */\n\n/**\n * Fires when a mouse-edge starts to scroll\n * @event Viewport#mouse-edge-start\n * @type {Viewport}\n */\n\n/**\n * Fires when the mouse-edge scrolling ends\n * @event Viewport#mouse-edge-end\n * @type {Viewport}\n */\n\n/**\n * Fires when viewport moves through UI interaction, deceleration, ensureVisible, or follow\n * @event Viewport#moved\n * @type {object}\n * @property {Viewport} viewport\n * @property {string} type - (drag, snap, pinch, follow, bounce-x, bounce-y,\n * clamp-x, clamp-y, decelerate, mouse-edges, wheel, ensureVisible)\n */\n\n/**\n * Fires when viewport moves through UI interaction, deceleration, ensureVisible, or follow\n * @event Viewport#zoomed\n * @type {object}\n * @property {Viewport} viewport\n * @property {string} type (drag-zoom, pinch, wheel, clamp-zoom, ensureVisible)\n */\n\n/**\n * Fires when viewport stops moving\n * @event Viewport#moved-end\n * @type {Viewport}\n */\n\n/**\n * Fires when viewport stops zooming\n * @event Viewport#zoomed-end\n * @type {Viewport}\n */\n\n/**\n* Fires at the end of an update frame\n* @event Viewport#frame-end\n* @type {Viewport}\n*/\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAGA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC1FA;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACpBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wRACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;qgBC1RA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC3XA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC1RA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACpGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACrRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC1eA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACvJA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACxRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,8HACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC/JA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACxNA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACrRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC3NA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;ACrSA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACpRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;"} \ No newline at end of file +{"version":3,"file":"viewport.js","sources":["../../src/plugins/Plugin.ts","../../src/ease.ts","../../src/plugins/Animate.ts","../../src/plugins/Bounce.ts","../../src/plugins/Clamp.ts","../../src/plugins/ClampZoom.ts","../../src/plugins/Decelerate.ts","../../src/plugins/Drag.ts","../../src/plugins/Follow.ts","../../src/plugins/MouseEdges.ts","../../src/plugins/Pinch.ts","../../src/plugins/Snap.ts","../../src/plugins/SnapZoom.ts","../../src/plugins/Wheel.ts","../../src/InputManager.ts","../../src/PluginManager.ts","../../src/Viewport.ts"],"sourcesContent":["import type { InteractionEvent } from '@pixi/interaction';\nimport type { Viewport } from '../Viewport';\n\n/**\n * Derive this class to create user-defined plugins\n *\n * @public\n */\nexport class Plugin\n{\n /** The viewport to which this plugin is attached. */\n public readonly parent: Viewport;\n\n /**\n * Flags whether this plugin has been \"paused\".\n *\n * @see Plugin#pause\n * @see Plugin#resume\n */\n public paused: boolean;\n\n /** @param {Viewport} parent */\n constructor(parent: Viewport)\n {\n this.parent = parent;\n this.paused = false;\n }\n\n /** Called when plugin is removed */\n public destroy()\n {\n // Override for implementation\n }\n\n /** Handler for pointerdown PIXI event */\n public down(_e: InteractionEvent): boolean\n {\n return false;\n }\n\n /** Handler for pointermove PIXI event */\n public move(_e: InteractionEvent): boolean\n {\n return false;\n }\n\n /** Handler for pointerup PIXI event */\n public up(_e: InteractionEvent): boolean\n {\n return false;\n }\n\n /** Handler for wheel event on div */\n public wheel(_e: WheelEvent): boolean | undefined\n {\n return false;\n }\n\n /**\n * Called on each tick\n * @param {number} elapsed time in millisecond since last update\n */\n public update(_delta: number): void\n {\n // Override for implementation\n }\n\n /** Called when the viewport is resized */\n public resize()\n {\n // Override for implementation\n }\n\n /** Called when the viewport is manually moved */\n public reset(): void\n {\n // Override for implementation\n }\n\n /** Pause the plugin */\n public pause(): void\n {\n this.paused = true;\n }\n\n /** Un-pause the plugin */\n public resume(): void\n {\n this.paused = false;\n }\n}\n","// eslint-disable-next-line\n// @ts-expect-error Penner seems to have no typings.\nimport Penner from 'penner';\n\n/**\n * Returns correct Penner equation using string or Function.\n *\n * @internal\n * @ignore\n * @param {(function|string)} [ease]\n * @param {defaults} default penner equation to use if none is provided\n */\nexport default function ease(ease: any, defaults?: any): any\n{\n if (!ease)\n {\n return Penner[defaults]\n }\n else if (typeof ease === 'function')\n {\n return ease\n }\n else if (typeof ease === 'string')\n {\n return Penner[ease]\n }\n}","import { Point } from '@pixi/math';\nimport { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Animate}. */\nexport interface IAnimateOptions {\n /** Time to animate */\n time?: number;\n\n /** Position to move the viewport to */\n position?: Point;\n\n /**\n * Desired viewport width in world pixels\n *\n * (use instead of scale; aspect ratio is maintained if height is not provided)\n */\n width?: number;\n\n /**\n * Desired viewport height in world pixels\n *\n * (use instead of scale; aspect ratio is maintained if width is not provided)\n */\n height?: number;\n\n /** Scale to change zoom (scale.x = scale.y) */\n scale?: number;\n\n /** Independently change zoom in x-direction */\n scaleX?: number;\n\n /** Independently change zoom in y-direction */\n scaleY?: number;\n\n /** Easing function to use */\n ease?: any;\n\n /** Callback to invoke when the animation completes */\n callbackOnComplete?: (viewport: Viewport) => void;\n\n /** Removes this plugin if interrupted by any user input */\n removeOnInterrupt?: boolean;\n}\n\nconst DEFAULT_ANIMATE_OPTIONS = {\n removeOnInterrupt: false,\n ease: 'linear',\n time: 1000,\n};\n\n/**\n * Animation plugin.\n *\n * @see Viewport#animate\n * @fires animate-end\n */\nexport class Animate extends Plugin\n{\n public readonly options: IAnimateOptions & { ease: any; time: number };\n\n /** The starting x-coordinate of the viewport. */\n protected startX?: number;\n\n /** The starting y-coordinate of the viewport. */\n protected startY?: number;\n\n /** The change in the x-coordinate of the viewport through the animation.*/\n protected deltaX?: number;\n\n /** The change in the y-coordinate of the viewport through the animation. */\n protected deltaY?: number;\n\n /** Marks whether the center of the viewport is preserved in the animation. */\n protected keepCenter!: boolean;\n\n /** The starting viewport width. */\n protected startWidth: number | null = null;\n\n /** The starting viewport height. */\n protected startHeight: number | null = null;\n\n /** The change in the viewport's width through the animation. */\n protected deltaWidth: number | null = null;\n\n /** The change in the viewport's height through the animation. */\n protected deltaHeight: number | null = null;\n\n /** The viewport's width post-animation. */\n protected width: number | null = null;\n\n /** The viewport's height post-animation. */\n protected height: number | null = null;\n\n /** The time since the animation started. */\n protected time = 0;\n\n /**\n * This is called by {@link Viewport.animate}.\n *\n * @param parent\n * @param options\n */\n constructor(parent: Viewport, options: IAnimateOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_ANIMATE_OPTIONS, options);\n this.options.ease = ease(this.options.ease);\n\n this.setupPosition();\n this.setupZoom();\n\n this.time = 0;\n }\n\n /**\n * Setup `startX`, `startY`, `deltaX`, `deltaY`, `keepCenter`.\n *\n * This is called during construction.\n */\n protected setupPosition(): void\n {\n if (typeof this.options.position !== 'undefined')\n {\n this.startX = this.parent.center.x;\n this.startY = this.parent.center.y;\n this.deltaX = this.options.position.x - this.parent.center.x;\n this.deltaY = this.options.position.y - this.parent.center.y;\n this.keepCenter = false;\n }\n else\n {\n this.keepCenter = true;\n }\n }\n\n /**\n * Setup `startWidth, `startHeight`, `deltaWidth, `deltaHeight, `width`, `height`.\n *\n * This is called during construction.\n */\n protected setupZoom(): void\n {\n this.width = null;\n this.height = null;\n\n if (typeof this.options.scale !== 'undefined')\n {\n this.width = this.parent.screenWidth / this.options.scale;\n }\n else if (typeof this.options.scaleX !== 'undefined' || typeof this.options.scaleY !== 'undefined')\n {\n if (typeof this.options.scaleX !== 'undefined')\n {\n // screenSizeInWorldPixels = screenWidth / scale\n this.width = this.parent.screenWidth / this.options.scaleX;\n }\n if (typeof this.options.scaleY !== 'undefined')\n {\n this.height = this.parent.screenHeight / this.options.scaleY;\n }\n }\n else\n {\n if (typeof this.options.width !== 'undefined')\n {\n this.width = this.options.width;\n }\n if (typeof this.options.height !== 'undefined')\n {\n this.height = this.options.height;\n }\n }\n\n if (this.width !== null)\n {\n this.startWidth = this.parent.screenWidthInWorldPixels;\n this.deltaWidth = this.width - this.startWidth;\n }\n if (this.height !== null)\n {\n this.startHeight = this.parent.screenHeightInWorldPixels;\n this.deltaHeight = this.height - this.startHeight;\n }\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('animate');\n }\n\n return false;\n }\n\n public complete(): void\n {\n this.parent.plugins.remove('animate');\n if (this.width !== null)\n {\n this.parent.fitWidth(this.width, this.keepCenter, this.height === null);\n }\n if (this.height !== null)\n {\n this.parent.fitHeight(this.height, this.keepCenter, this.width === null);\n }\n if (!this.keepCenter)\n {\n this.parent.moveCenter(this.options.position as Point);\n }\n\n this.parent.emit('animate-end', this.parent);\n\n if (this.options.callbackOnComplete)\n {\n this.options.callbackOnComplete(this.parent);\n }\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n this.time += elapsed;\n\n if (this.time >= this.options.time)\n {\n this.complete();\n }\n else\n {\n const originalZoom = new Point(this.parent.scale.x, this.parent.scale.y);\n const percent = this.options.ease(this.time, 0, 1, this.options.time);\n\n if (this.width !== null)\n {\n const startWidth = this.startWidth as number;\n const deltaWidth = this.deltaWidth as number;\n\n this.parent.fitWidth(\n startWidth + (deltaWidth * percent),\n this.keepCenter,\n this.height === null);\n }\n if (this.height !== null)\n {\n const startHeight = this.startHeight as number;\n const deltaHeight = this.deltaHeight as number;\n\n this.parent.fitHeight(\n startHeight + (deltaHeight * percent),\n this.keepCenter,\n this.width === null);\n }\n if (this.width === null)\n {\n this.parent.scale.x = this.parent.scale.y;\n }\n else if (this.height === null)\n {\n this.parent.scale.y = this.parent.scale.x;\n }\n if (!this.keepCenter)\n {\n const startX = this.startX as number;\n const startY = this.startY as number;\n const deltaX = this.deltaX as number;\n const deltaY = this.deltaY as number;\n const original = new Point(this.parent.x, this.parent.y);\n\n this.parent.moveCenter(startX + (deltaX * percent), startY + (deltaY * percent));\n this.parent.emit('moved', { viewport: this.parent, original, type: 'animate' });\n }\n if (this.width || this.height)\n {\n this.parent.emit('zoomed', { viewport: this.parent, original: originalZoom, type: 'animate' });\n }\n }\n }\n}\n","import { Point, Rectangle } from '@pixi/math';\nimport { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Drag } from './Drag';\nimport type { IDecelerateOptions } from './Decelerate';\nimport type { Pinch } from './Pinch';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Bounce}. */\nexport interface IBounceOptions {\n /** \"all\", \"horizontal\", \"vertical\", or combination of \"top\", \"bottom\", \"right\", \"left\" (e.g., 'top-bottom-right') */\n sides?:\n 'all'\n | 'horizontal'\n | 'vertical'\n | string;\n\n /** Friction to apply to decelerate if active */\n friction?: number;\n\n /** Time in ms to finish bounce */\n time?: number;\n\n /** Use this bounceBox instead of (0, 0, viewport.worldWidth, viewport.worldHeight) */\n bounceBox?: Rectangle | null;\n\n /** Ease function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /** (top/bottom/center and left/right/center, or center) where to place world if too small for screen */\n underflow?: 'center' | string;\n}\n\n/** Bounce state along an axis */\nexport interface IBounceState {\n /** Elapsed time since bounce started */\n time: number;\n\n /** Starting coordinate */\n start: number;\n\n /** Change in coordinate through bounce */\n delta: number;\n\n /** Ending coordinate */\n end: number;\n}\n\nconst DEFAULT_BOUNCE_OPTIONS: Required = {\n sides: 'all',\n friction: 0.5,\n time: 150,\n ease: 'easeInOutSine',\n underflow: 'center',\n bounceBox: null\n};\n\n/**\n * @fires bounce-start-x\n * @fires bounce.end-x\n * @fires bounce-start-y\n * @fires bounce-end-y\n * @public\n */\nexport class Bounce extends Plugin\n{\n /** The options passed to initialize this plugin, cannot be modified again. */\n public readonly options: Readonly>;\n\n /** Holds whether to bounce from left side. */\n public readonly left: boolean ;\n\n /** Holds whether to bounce from top side. */\n public readonly top: boolean;\n\n /** Holds whether to bounce from right side. */\n public readonly right: boolean;\n\n /** Holds whether to bounce from bottom side. */\n public readonly bottom: boolean;\n\n /** Direction of underflow along x-axis. */\n public readonly underflowX: -1 | 0 | 1;\n\n /** Direction of underflow along y-axis. */\n public readonly underflowY: -1 | 0 | 1;\n\n /** Easing */\n protected ease: any;\n\n /** Bounce state along x-axis */\n protected toX!: IBounceState | null;\n\n /** Bounce state along y-axis */\n protected toY!: IBounceState | null;\n\n /**\n * This is called by {@link Viewport.bounce}.\n */\n constructor(parent: Viewport, options: IBounceOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_BOUNCE_OPTIONS, options);\n this.ease = ease(this.options.ease, 'easeInOutSine');\n\n if (this.options.sides)\n {\n if (this.options.sides === 'all')\n {\n this.top = this.bottom = this.left = this.right = true;\n }\n else if (this.options.sides === 'horizontal')\n {\n this.right = this.left = true;\n this.top = this.bottom = false;\n }\n else if (this.options.sides === 'vertical')\n {\n this.left = this.right = false;\n this.top = this.bottom = true;\n }\n else\n {\n this.top = this.options.sides.indexOf('top') !== -1;\n this.bottom = this.options.sides.indexOf('bottom') !== -1;\n this.left = this.options.sides.indexOf('left') !== -1;\n this.right = this.options.sides.indexOf('right') !== -1;\n }\n } else {\n this.left = this.top = this.right = this.bottom = false;\n }\n\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'center')\n {\n this.underflowX = 0;\n this.underflowY = 0;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n }\n\n this.reset();\n }\n\n public isActive(): boolean\n {\n return this.toX !== null || this.toY !== null;\n }\n\n public down(): boolean\n {\n this.toX = this.toY = null;\n\n return false;\n }\n\n public up(): boolean\n {\n this.bounce();\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n this.bounce();\n\n if (this.toX)\n {\n const toX = this.toX;\n\n toX.time += elapsed;\n this.parent.emit('moved', { viewport: this.parent, type: 'bounce-x' });\n\n if (toX.time >= this.options.time)\n {\n this.parent.x = toX.end;\n this.toX = null;\n this.parent.emit('bounce-x-end', this.parent);\n }\n else\n {\n this.parent.x = this.ease(toX.time, toX.start, toX.delta, this.options.time);\n }\n }\n\n if (this.toY)\n {\n const toY = this.toY;\n\n toY.time += elapsed;\n this.parent.emit('moved', { viewport: this.parent, type: 'bounce-y' });\n\n if (toY.time >= this.options.time)\n {\n this.parent.y = toY.end;\n this.toY = null;\n this.parent.emit('bounce-y-end', this.parent);\n }\n else\n {\n this.parent.y = this.ease(toY.time, toY.start, toY.delta, this.options.time);\n }\n }\n }\n\n /** @internal */\n protected calcUnderflowX(): number\n {\n let x: number;\n\n switch (this.underflowX)\n {\n case -1:\n x = 0;\n break;\n case 1:\n x = (this.parent.screenWidth - this.parent.screenWorldWidth);\n break;\n default:\n x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n }\n\n return x;\n }\n\n /** @internal */\n protected calcUnderflowY(): number\n {\n let y: number;\n\n switch (this.underflowY)\n {\n case -1:\n y = 0;\n break;\n case 1:\n y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n break;\n default:\n y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n }\n\n return y;\n }\n\n private oob(): Record<'left' | 'right' | 'top' | 'bottom', boolean> & Record<'topLeft' | 'bottomRight', Point>\n {\n const box = this.options.bounceBox;\n\n if (box)\n {\n const x1 = typeof box.x === 'undefined' ? 0 : box.x;\n const y1 = typeof box.y === 'undefined' ? 0 : box.y;\n const width = typeof box.width === 'undefined' ? this.parent.worldWidth : box.width;\n const height = typeof box.height === 'undefined' ? this.parent.worldHeight : box.height;\n\n return {\n left: this.parent.left < x1,\n right: this.parent.right > width,\n top: this.parent.top < y1,\n bottom: this.parent.bottom > height,\n topLeft: new Point(\n x1 * this.parent.scale.x,\n y1 * this.parent.scale.y\n ),\n bottomRight: new Point(\n width * this.parent.scale.x - this.parent.screenWidth,\n height * this.parent.scale.y - this.parent.screenHeight\n )\n };\n }\n\n return {\n left: this.parent.left < 0,\n right: this.parent.right > this.parent.worldWidth,\n top: this.parent.top < 0,\n bottom: this.parent.bottom > this.parent.worldHeight,\n topLeft: new Point(0, 0),\n bottomRight: new Point(\n this.parent.worldWidth * this.parent.scale.x - this.parent.screenWidth,\n this.parent.worldHeight * this.parent.scale.y - this.parent.screenHeight\n )\n };\n }\n\n public bounce(): void\n {\n if (this.paused)\n {\n return;\n }\n\n let oob;\n let decelerate: undefined | null | {\n percentChangeX?: number;\n percentChangeY?: number;\n x?: number;\n y?: number;\n options?: IDecelerateOptions\n } = this.parent.plugins.get('decelerate', true) as any;\n\n if (decelerate && (decelerate.x || decelerate.y))\n {\n if ((decelerate.x && decelerate.percentChangeX === decelerate.options?.friction) || (decelerate.y && decelerate.percentChangeY === decelerate.options?.friction))\n {\n oob = this.oob();\n if ((oob.left && this.left) || (oob.right && this.right))\n {\n decelerate.percentChangeX = this.options.friction;\n }\n if ((oob.top && this.top) || (oob.bottom && this.bottom))\n {\n decelerate.percentChangeY = this.options.friction;\n }\n }\n }\n const drag: Partial = this.parent.plugins.get('drag', true) || {};\n const pinch: Partial = this.parent.plugins.get('pinch', true) || {};\n\n decelerate = decelerate || {};\n\n if (!drag?.active && !pinch?.active && ((!this.toX || !this.toY) && (!decelerate.x || !decelerate.y)))\n {\n oob = oob || this.oob();\n const topLeft = oob.topLeft;\n const bottomRight = oob.bottomRight;\n\n if (!this.toX && !decelerate.x)\n {\n let x = null;\n\n if (oob.left && this.left)\n {\n x = (this.parent.screenWorldWidth < this.parent.screenWidth) ? this.calcUnderflowX() : -topLeft.x;\n }\n else if (oob.right && this.right)\n {\n x = (this.parent.screenWorldWidth < this.parent.screenWidth) ? this.calcUnderflowX() : -bottomRight.x;\n }\n if (x !== null && this.parent.x !== x)\n {\n this.toX = { time: 0, start: this.parent.x, delta: x - this.parent.x, end: x };\n this.parent.emit('bounce-x-start', this.parent);\n }\n }\n if (!this.toY && !decelerate.y)\n {\n let y = null;\n\n if (oob.top && this.top)\n {\n y = (this.parent.screenWorldHeight < this.parent.screenHeight) ? this.calcUnderflowY() : -topLeft.y;\n }\n else if (oob.bottom && this.bottom)\n {\n y = (this.parent.screenWorldHeight < this.parent.screenHeight) ? this.calcUnderflowY() : -bottomRight.y;\n }\n if (y !== null && this.parent.y !== y)\n {\n this.toY = { time: 0, start: this.parent.y, delta: y - this.parent.y, end: y };\n this.parent.emit('bounce-y-start', this.parent);\n }\n }\n }\n }\n\n public reset(): void\n {\n this.toX = this.toY = null;\n this.bounce();\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\n\n/**\n * There are three ways to clamp:\n * 1. direction: 'all' = the world is clamped to its world boundaries, ie, you cannot drag any part of the world offscreen\n * direction: 'x' | 'y' = only the x or y direction is clamped to its world boundary\n * 2. left, right, top, bottom = true | number = the world is clamped to the world's pixel location for each side;\n * if any of these are set to true, then the location is set to the boundary [0, viewport.worldWidth/viewport.worldHeight]\n * eg: to allow the world to be completely dragged offscreen, set [-viewport.worldWidth, -viewport.worldHeight, viewport.worldWidth * 2, viewport.worldHeight * 2]\n *\n * Underflow determines what happens when the world is smaller than the viewport\n * 1. none = the world is clamped but there is no special behavior\n * 2. center = the world is centered on the viewport\n * 3. combination of top/bottom/center and left/right/center (case insensitive) = the world is stuck to the appropriate boundaries\n *\n */\nexport interface IClampOptions\n{\n /**\n * Clamp left; true = 0\n *\n * @default false\n */\n left?: number | boolean | null;\n\n /**\n * Clamp top; true = 0\n *\n * @default false\n */\n top?: number | boolean | null;\n\n /**\n * Clamp right; true = viewport.worldWidth\n *\n * @default false\n */\n right?: number | boolean | null;\n\n /**\n * Clamp bottom; true = viewport.worldHeight\n *\n * @default false\n */\n bottom?: number | boolean | null;\n\n /**\n * (all, x, or y) using clamps of [0, viewport.worldWidth/viewport.worldHeight]; replaces left/right/top/bottom if set\n *\n * @default null\n */\n direction?: 'all' | 'x' | 'y' | null;\n\n /**\n * Where to place world if too small for screen (e.g., top-right, center, none, bottomleft)\n *\n * @default 'center'\n */\n underflow?: 'center' | string;\n}\n\nconst DEFAULT_CLAMP_OPTIONS: Required = {\n left: false,\n right: false,\n top: false,\n bottom: false,\n direction: null,\n underflow: 'center'\n};\n\n/**\n * Plugin to clamp the viewport to a specific world bounding box.\n *\n * @public\n */\nexport class Clamp extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Required;\n\n /** Last state of viewport */\n protected last: {\n x: number | null;\n y: number | null;\n scaleX: number | null;\n scaleY: number | null;\n };\n\n protected noUnderflow!: boolean;\n protected underflowX!: -1 | 0 | 1;\n protected underflowY!: -1 | 0 | 1;\n\n /**\n * This is called by {@link Viewport.clamp}.\n */\n constructor(parent: Viewport, options : IClampOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_CLAMP_OPTIONS, options);\n\n if (this.options.direction)\n {\n this.options.left = this.options.direction === 'x' || this.options.direction === 'all' ? true : null;\n this.options.right = this.options.direction === 'x' || this.options.direction === 'all' ? true : null;\n this.options.top = this.options.direction === 'y' || this.options.direction === 'all' ? true : null;\n this.options.bottom = this.options.direction === 'y' || this.options.direction === 'all' ? true : null;\n }\n\n this.parseUnderflow();\n this.last = { x: null, y: null, scaleX: null, scaleY: null };\n this.update();\n }\n\n private parseUnderflow()\n {\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'none')\n {\n this.noUnderflow = true;\n }\n else if (clamp === 'center')\n {\n this.underflowX = this.underflowY = 0;\n this.noUnderflow = false;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n this.noUnderflow = false;\n }\n }\n\n public move(): boolean\n {\n this.update();\n\n return false;\n }\n\n public update(): void\n {\n if (this.paused)\n {\n return;\n }\n\n // only clamp on change\n if (this.parent.x === this.last.x\n && this.parent.y === this.last.y\n && this.parent.scale.x === this.last.scaleX\n && this.parent.scale.y === this.last.scaleY)\n {\n return;\n }\n const original = { x: this.parent.x, y: this.parent.y };\n // TODO: Fix\n const decelerate: any = (this.parent.plugins as any).decelerate || {};\n\n if (this.options.left !== null || this.options.right !== null)\n {\n let moved = false;\n\n if (!this.noUnderflow && this.parent.screenWorldWidth < this.parent.screenWidth)\n {\n switch (this.underflowX)\n {\n case -1:\n if (this.parent.x !== 0)\n {\n this.parent.x = 0;\n moved = true;\n }\n break;\n case 1:\n if (this.parent.x !== this.parent.screenWidth - this.parent.screenWorldWidth)\n {\n this.parent.x = this.parent.screenWidth - this.parent.screenWorldWidth;\n moved = true;\n }\n break;\n default:\n if (this.parent.x !== (this.parent.screenWidth - this.parent.screenWorldWidth) / 2)\n {\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n moved = true;\n }\n }\n }\n else\n {\n if (this.options.left !== null)\n {\n if (this.parent.left < (this.options.left === true ? 0 : this.options.left))\n {\n this.parent.x = -(this.options.left === true ? 0 : this.options.left) * this.parent.scale.x;\n decelerate.x = 0;\n moved = true;\n }\n }\n if (this.options.right !== null)\n {\n if (this.parent.right > (this.options.right === true ? this.parent.worldWidth : this.options.right))\n {\n this.parent.x = -(this.options.right === true ? this.parent.worldWidth : this.options.right) * this.parent.scale.x + this.parent.screenWidth;\n decelerate.x = 0;\n moved = true;\n }\n }\n }\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, original, type: 'clamp-x' });\n }\n }\n if (this.options.top !== null || this.options.bottom !== null)\n {\n let moved = false;\n\n if (!this.noUnderflow && this.parent.screenWorldHeight < this.parent.screenHeight)\n {\n switch (this.underflowY)\n {\n case -1:\n if (this.parent.y !== 0)\n {\n this.parent.y = 0;\n moved = true;\n }\n break;\n case 1:\n if (this.parent.y !== this.parent.screenHeight - this.parent.screenWorldHeight)\n {\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n moved = true;\n }\n break;\n default:\n if (this.parent.y !== (this.parent.screenHeight - this.parent.screenWorldHeight) / 2)\n {\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n moved = true;\n }\n }\n }\n else\n {\n if (this.options.top !== null)\n {\n if (this.parent.top < (this.options.top === true ? 0 : this.options.top))\n {\n this.parent.y = -(this.options.top === true ? 0 : this.options.top)\n * this.parent.scale.y;\n decelerate.y = 0;\n moved = true;\n }\n }\n if (this.options.bottom !== null)\n {\n if (this.parent.bottom > (this.options.bottom === true ? this.parent.worldHeight : this.options.bottom))\n {\n this.parent.y = -(this.options.bottom === true ? this.parent.worldHeight : this.options.bottom)\n * this.parent.scale.y + this.parent.screenHeight;\n decelerate.y = 0;\n moved = true;\n }\n }\n }\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, original, type: 'clamp-y' });\n }\n }\n this.last.x = this.parent.x;\n this.last.y = this.parent.y;\n this.last.scaleX = this.parent.scale.x;\n this.last.scaleY = this.parent.scale.y;\n }\n\n public reset(): void\n {\n this.update();\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\n\n/**\n * Options for {@link ClampZoom}.\n *\n * Use either minimum width/height or minimum scale\n */\nexport interface IClampZoomOptions\n{\n /** Minimum width */\n minWidth?: number | null;\n\n /** Minimum height */\n minHeight?: number | null;\n\n /** Maximum width */\n maxWidth?: number | null;\n\n /** Maximum height */\n maxHeight?: number | null;\n\n /** Minimum scale */\n minScale?: number | null | IScale;\n\n /** Maximum scale */\n maxScale?: number | null | IScale;\n}\n\nconst DEFAULT_CLAMP_ZOOM_OPTIONS: Required = {\n minWidth: null,\n minHeight: null,\n maxWidth: null,\n maxHeight: null,\n minScale: null,\n maxScale: null\n};\n\n/**\n * Plugin to clamp the viewport's zoom to a specific range.\n *\n * @public\n */\nexport class ClampZoom extends Plugin\n{\n public readonly options: Required;\n\n /**\n * This is called by {@link Viewport.clampZoom}.\n */\n constructor(parent: Viewport, options = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_CLAMP_ZOOM_OPTIONS, options);\n\n this.clamp();\n }\n\n public resize(): void\n {\n this.clamp();\n }\n\n /** Clamp the viewport scale zoom) */\n public clamp(): void\n {\n if (this.paused)\n {\n return;\n }\n\n if (this.options.minWidth || this.options.minHeight || this.options.maxWidth || this.options.maxHeight)\n {\n let width = this.parent.worldScreenWidth;\n let height = this.parent.worldScreenHeight;\n\n if (this.options.minWidth !== null && width < this.options.minWidth)\n {\n const original = this.parent.scale.x;\n\n this.parent.fitWidth(this.options.minWidth, false, false, true);\n this.parent.scale.y *= this.parent.scale.x / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.maxWidth !== null && width > this.options.maxWidth)\n {\n const original = this.parent.scale.x;\n\n this.parent.fitWidth(this.options.maxWidth, false, false, true);\n this.parent.scale.y *= this.parent.scale.x / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.minHeight !== null && height < this.options.minHeight)\n {\n const original = this.parent.scale.y;\n\n this.parent.fitHeight(this.options.minHeight, false, false, true);\n this.parent.scale.x *= this.parent.scale.y / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.maxHeight !== null && height > this.options.maxHeight)\n {\n const original = this.parent.scale.y;\n\n this.parent.fitHeight(this.options.maxHeight, false, false, true);\n this.parent.scale.x *= this.parent.scale.y / original;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n }\n else\n if (this.options.minScale || this.options.maxScale)\n {\n const minScale: IScale = { x: null, y: null };\n const maxScale: IScale = { x: null, y: null };\n\n if (typeof this.options.minScale === 'number')\n {\n minScale.x = this.options.minScale;\n minScale.y = this.options.minScale;\n }\n else if (this.options.minScale !== null)\n {\n const optsMinScale = this.options.minScale as IScale;\n\n minScale.x = typeof optsMinScale.x === 'undefined' ? null : optsMinScale.x;\n minScale.y = typeof optsMinScale.y === 'undefined' ? null : optsMinScale.y;\n }\n\n if (typeof this.options.maxScale === 'number')\n {\n maxScale.x = this.options.maxScale;\n maxScale.y = this.options.maxScale;\n }\n else if (this.options.maxScale !== null)\n {\n const optsMaxScale = this.options.maxScale as IScale;\n\n maxScale.x = typeof optsMaxScale.x === 'undefined' ? null : optsMaxScale.x;\n maxScale.y = typeof optsMaxScale.y === 'undefined' ? null : optsMaxScale.y;\n }\n\n let scaleX = this.parent.scale.x;\n let scaleY = this.parent.scale.y;\n\n if (minScale.x !== null && scaleX < minScale.x)\n {\n scaleX = minScale.x;\n }\n if (maxScale.x !== null && scaleX > maxScale.x)\n {\n scaleX = maxScale.x;\n }\n if (minScale.y !== null && scaleY < minScale.y)\n {\n scaleY = minScale.y;\n }\n if (maxScale.y !== null && scaleY > maxScale.y)\n {\n scaleY = maxScale.y;\n }\n if (scaleX !== this.parent.scale.x || scaleY !== this.parent.scale.y)\n {\n this.parent.scale.set(scaleX, scaleY);\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n }\n }\n\n public reset(): void\n {\n this.clamp();\n }\n}\n\n/** This allows independent x and y values for min/maxScale */\nexport interface IScale {\n x: null | number\n y: null | number\n}\n","import { Plugin } from './Plugin';\n\nimport type { Point } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\nexport interface IDecelerateOptions {\n /**\n * Percent to decelerate after movement. This should be between 0 and 1, exclusive.\n *\n * @default 0.95\n */\n friction?: number;\n\n /**\n * Percent to decelerate when past boundaries (only applicable when viewport.bounce() is active)\n *\n * @default 0.8\n */\n bounce?: number;\n\n /**\n * Minimum velocity before stopping/reversing acceleration\n *\n * @default 0.01\n */\n minSpeed?: number;\n}\n\n/** Viewport position snapshot that's saved by {@link DeceleratePlugin} to estimate panning velocity. */\nexport interface IDecelerateSnapshot {\n /** x-coordinate of the viewport. */\n x: number;\n\n /** y-coordinate of the viewport. */\n y: number;\n\n /** Time at which this snapshot was taken. */\n time: number;\n}\n\nconst DEFAULT_DECELERATE_OPTIONS: Required = {\n friction: 0.98,\n bounce: 0.8,\n minSpeed: 0.01\n};\n\n/**\n * Time period of decay (1 frame)\n *\n * @internal\n * @ignore\n */\nconst TP = 16;\n\n/**\n * Plugin to decelerate viewport velocity smoothly after panning ends.\n *\n * @public\n */\nexport class Decelerate extends Plugin\n{\n /** Options used to initialize this plugin. */\n public readonly options: Required;\n\n /**\n * x-component of the velocity of viewport provided by this plugin, at the current time.\n *\n * This is measured in px/frame, where a frame is normalized to 16 milliseconds.\n */\n public x!: number | null;\n\n /**\n * y-component of the velocity of the viewport provided by this plugin, at the current time.\n *\n * This is measured in px/frame, where a frame is normalized to 16 milliseconds.\n */\n public y!: number | null;\n\n /**\n * The decay factor for the x-component of the viewport.\n *\n * The viewport's velocity decreased by this amount each 16 milliseconds.\n */\n public percentChangeX!: number;\n\n /**\n * The decay factor for the y-component of the viewport.\n *\n * The viewport's velocity decreased by this amount each 16 milliseconds.\n */\n public percentChangeY!: number;\n\n /** Saved list of recent viewport position snapshots, to estimate velocity. */\n protected saved: Array;\n\n /** The time since the user released panning of the viewport. */\n protected timeSinceRelease: number;\n\n /**\n * This is called by {@link Viewport.decelerate}.\n */\n constructor(parent: Viewport, options: IDecelerateOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_DECELERATE_OPTIONS, options);\n this.saved = [];\n this.timeSinceRelease = 0;\n\n this.reset();\n this.parent.on('moved', (data) => this.moved(data));\n }\n\n public down(): boolean\n {\n this.saved = [];\n this.x = this.y = null;\n\n return false;\n }\n\n public isActive(): boolean\n {\n return !!(this.x || this.y);\n }\n\n public move(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n const count = this.parent.input.count();\n\n if (count === 1 || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n this.saved.push({ x: this.parent.x, y: this.parent.y, time: performance.now() });\n\n if (this.saved.length > 60)\n {\n this.saved.splice(0, 30);\n }\n }\n\n // Silently recording viewport positions\n return false;\n }\n\n /** Listener to viewport's \"moved\" event. */\n protected moved(data: { type: 'clamp-x' | 'clamp-y'; original: Point }): void\n {\n if (this.saved.length)\n {\n const last = this.saved[this.saved.length - 1];\n\n if (data.type === 'clamp-x')\n {\n if (last.x === data.original.x)\n {\n last.x = this.parent.x;\n }\n }\n else if (data.type === 'clamp-y')\n {\n if (last.y === data.original.y)\n {\n last.y = this.parent.y;\n }\n }\n }\n }\n\n public up(): boolean\n {\n if (this.parent.input.count() === 0 && this.saved.length)\n {\n const now = performance.now();\n\n for (const save of this.saved)\n {\n if (save.time >= now - 100)\n {\n const time = now - save.time;\n\n this.x = (this.parent.x - save.x) / time;\n this.y = (this.parent.y - save.y) / time;\n this.percentChangeX = this.percentChangeY = this.options.friction;\n this.timeSinceRelease = 0;\n break;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Manually activate deceleration, starting from the (x, y) velocity components passed in the options.\n *\n * @param {object} options\n * @param {number} [options.x] - Specify x-component of initial velocity.\n * @param {number} [options.y] - Specify y-component of initial velocity.\n */\n public activate(options: { x?: number; y?: number; }): void\n {\n options = options || {};\n\n if (typeof options.x !== 'undefined')\n {\n this.x = options.x;\n this.percentChangeX = this.options.friction;\n }\n if (typeof options.y !== 'undefined')\n {\n this.y = options.y;\n this.percentChangeY = this.options.friction;\n }\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n /*\n * See https://github.com/davidfig/pixi-viewport/issues/271 for math.\n *\n * The viewport velocity (this.x, this.y) decays exponentially by the the decay factor\n * (this.percentChangeX, this.percentChangeY) each frame. This velocity function is integrated\n * to calculate the displacement.\n */\n\n const moved = this.x || this.y;\n\n const ti = this.timeSinceRelease;\n const tf = this.timeSinceRelease + elapsed;\n\n if (this.x)\n {\n const k = this.percentChangeX;\n const lnk = Math.log(k);\n\n // Apply velocity delta on the viewport x-coordinate.\n this.parent.x += ((this.x * TP) / lnk) * (Math.pow(k, tf / TP) - Math.pow(k, ti / TP));\n\n // Apply decay on x-component of velocity\n this.x *= Math.pow(this.percentChangeX, elapsed / TP);\n }\n if (this.y)\n {\n const k = this.percentChangeY;\n const lnk = Math.log(k);\n\n // Apply velocity delta on the viewport y-coordinate.\n this.parent.y += ((this.y * TP) / lnk) * (Math.pow(k, tf / TP) - Math.pow(k, ti / TP));\n\n // Apply decay on y-component of velocity\n this.y *= Math.pow(this.percentChangeY, elapsed / TP);\n }\n\n this.timeSinceRelease += elapsed;\n\n // End decelerate velocity once it goes under a certain amount of precision.\n if (Math.abs(this.x || 0) < this.options.minSpeed)\n {\n this.x = 0;\n }\n if (Math.abs(this.y || 0) < this.options.minSpeed)\n {\n this.y = 0;\n }\n\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, type: 'decelerate' });\n }\n }\n\n public reset(): void\n {\n this.x = this.y = null;\n }\n}\n","import { Point } from '@pixi/math';\nimport { Plugin } from './Plugin';\n\nimport type { Decelerate } from './Decelerate';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { IPointData } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Drag}. */\nexport interface IDragOptions {\n /**\n * direction to drag\n *\n * @default \"all\"\n */\n direction?: string;\n\n /**\n * whether click to drag is active\n *\n * @default true\n */\n pressDrag?: boolean;\n\n /**\n * Use wheel to scroll in direction (unless wheel plugin is active)\n *\n * @default true\n */\n wheel?: boolean;\n\n /**\n * number of pixels to scroll with each wheel spin\n *\n * @default 1\n */\n wheelScroll?: number;\n\n /**\n * reverse the direction of the wheel scroll\n *\n * @default false\n */\n reverse?: boolean;\n\n /**\n * clamp wheel(to avoid weird bounce with mouse wheel). Can be 'x' or 'y' or `true`.\n *\n * @default false\n */\n clampWheel?: boolean | string;\n\n /**\n * where to place world if too small for screen\n *\n * @default \"center\"\n */\n underflow?: string;\n\n /**\n * factor to multiply drag to increase the speed of movement\n *\n * @default 1\n */\n factor?: number;\n\n /**\n * Changes which mouse buttons trigger drag.\n *\n * Use: 'all', 'left', right' 'middle', or some combination, like, 'middle-right'; you may want to set\n * `viewport.options.disableOnContextMenu` if you want to use right-click dragging.\n *\n * @default \"all\"\n */\n mouseButtons?: 'all' | string;\n\n /**\n * Array containing {@link key|https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code} codes of\n * keys that can be pressed for the drag to be triggered, e.g.: ['ShiftLeft', 'ShiftRight'}.\n *\n * @default null\n */\n keyToPress?: string[] | null;\n\n /**\n * Ignore keyToPress for touch events.\n *\n * @default false\n */\n ignoreKeyToPressOnTouch?: boolean;\n\n /**\n * Scaling factor for non-DOM_DELTA_PIXEL scrolling events.\n *\n * @default 20\n */\n lineHeight?: number;\n}\n\nconst DEFAULT_DRAG_OPTIONS: Required = {\n direction: 'all',\n pressDrag: true,\n wheel: true,\n wheelScroll: 1,\n reverse: false,\n clampWheel: false,\n underflow: 'center',\n factor: 1,\n mouseButtons: 'all',\n keyToPress: null,\n ignoreKeyToPressOnTouch: false,\n lineHeight: 20,\n};\n\n/**\n * Plugin to enable panning/dragging of the viewport to move around.\n *\n * @public\n */\nexport class Drag extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Readonly>;\n\n /** Flags when viewport is moving. */\n protected moved: boolean;\n\n /** Factor to apply from {@link IDecelerateOptions}'s reverse. */\n protected reverse: 1 | -1;\n\n /** Holds whether dragging is enabled along the x-axis. */\n protected xDirection: boolean\n\n /** Holds whether dragging is enabled along the y-axis. */\n protected yDirection: boolean;\n\n /** Flags whether the keys required to drag are pressed currently. */\n protected keyIsPressed: boolean;\n\n /** Holds whether the left, center, and right buttons are required to pan. */\n protected mouse!: [boolean, boolean, boolean]\n\n /** Underflow factor along x-axis */\n protected underflowX!: -1 | 0 | 1;\n\n /** Underflow factor along y-axis */\n protected underflowY!: -1 | 0 | 1;\n\n /** Last pointer position while panning. */\n protected last?: IPointData | null\n\n /** The ID of the pointer currently panning the viewport. */\n protected current?: number;\n\n /**\n * This is called by {@link Viewport.drag}.\n */\n constructor(parent: Viewport, options = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_DRAG_OPTIONS, options);\n this.moved = false;\n this.reverse = this.options.reverse ? 1 : -1;\n this.xDirection = !this.options.direction || this.options.direction === 'all' || this.options.direction === 'x';\n this.yDirection = !this.options.direction || this.options.direction === 'all' || this.options.direction === 'y';\n this.keyIsPressed = false;\n\n this.parseUnderflow();\n this.mouseButtons(this.options.mouseButtons);\n\n if (this.options.keyToPress)\n {\n this.handleKeyPresses(this.options.keyToPress);\n }\n }\n\n /**\n * Handles keypress events and set the keyIsPressed boolean accordingly\n *\n * @param {array} codes - key codes that can be used to trigger drag event\n */\n protected handleKeyPresses(codes: string[]): void\n {\n window.addEventListener('keydown', (e) =>\n {\n if (codes.includes(e.code))\n { this.keyIsPressed = true; }\n });\n\n window.addEventListener('keyup', (e) =>\n {\n if (codes.includes(e.code))\n { this.keyIsPressed = false; }\n });\n }\n\n /**\n * initialize mousebuttons array\n * @param {string} buttons\n */\n protected mouseButtons(buttons: string): void\n {\n if (!buttons || buttons === 'all')\n {\n this.mouse = [true, true, true];\n }\n else\n {\n this.mouse = [\n buttons.indexOf('left') !== -1,\n buttons.indexOf('middle') !== -1,\n buttons.indexOf('right') !== -1\n ];\n }\n }\n\n protected parseUnderflow(): void\n {\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'center')\n {\n this.underflowX = 0;\n this.underflowY = 0;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n }\n }\n\n /**\n * @param {PIXI.InteractionEvent} event\n * @returns {boolean}\n */\n protected checkButtons(event: InteractionEvent): boolean\n {\n const isMouse = event.data.pointerType === 'mouse';\n const count = this.parent.input.count();\n\n if ((count === 1) || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n if (!isMouse || this.mouse[event.data.button])\n {\n return true;\n }\n }\n\n return false;\n }\n\n /**\n * @param {PIXI.InteractionEvent} event\n * @returns {boolean}\n */\n protected checkKeyPress(event: InteractionEvent): boolean\n {\n return (!this.options.keyToPress\n || this.keyIsPressed\n || (this.options.ignoreKeyToPressOnTouch && event.data.pointerType === 'touch'));\n }\n\n public down(event: InteractionEvent): boolean\n {\n if (this.paused || !this.options.pressDrag)\n {\n return false;\n }\n if (this.checkButtons(event) && this.checkKeyPress(event))\n {\n this.last = { x: event.data.global.x, y: event.data.global.y };\n this.current = event.data.pointerId;\n\n return true;\n }\n this.last = null;\n\n return false;\n }\n\n get active(): boolean\n {\n return this.moved;\n }\n\n public move(event: InteractionEvent): boolean\n {\n if (this.paused || !this.options.pressDrag)\n {\n return false;\n }\n if (this.last && this.current === event.data.pointerId)\n {\n const x = event.data.global.x;\n const y = event.data.global.y;\n const count = this.parent.input.count();\n\n if (count === 1 || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n const distX = x - this.last.x;\n const distY = y - this.last.y;\n\n if (this.moved\n || ((this.xDirection && this.parent.input.checkThreshold(distX))\n || (this.yDirection && this.parent.input.checkThreshold(distY))))\n {\n const newPoint = { x, y };\n\n if (this.xDirection)\n {\n this.parent.x += (newPoint.x - this.last.x) * this.options.factor;\n }\n if (this.yDirection)\n {\n this.parent.y += (newPoint.y - this.last.y) * this.options.factor;\n }\n this.last = newPoint;\n if (!this.moved)\n {\n this.parent.emit('drag-start', {\n event,\n screen: new Point(this.last.x, this.last.y),\n world: this.parent.toWorld(new Point(this.last.x, this.last.y)),\n viewport: this.parent\n });\n }\n this.moved = true;\n this.parent.emit('moved', { viewport: this.parent, type: 'drag' });\n\n return true;\n }\n }\n else\n {\n this.moved = false;\n }\n }\n\n return false;\n }\n\n public up(event: InteractionEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n const touches = this.parent.input.touches;\n\n if (touches.length === 1)\n {\n const pointer = touches[0];\n\n if (pointer.last)\n {\n this.last = { x: pointer.last.x, y: pointer.last.y };\n this.current = pointer.id;\n }\n this.moved = false;\n\n return true;\n }\n else if (this.last)\n {\n if (this.moved)\n {\n const screen = new Point(this.last.x, this.last.y);\n\n this.parent.emit('drag-end', {\n event, screen,\n world: this.parent.toWorld(screen),\n viewport: this.parent,\n });\n this.last = null;\n this.moved = false;\n\n return true;\n }\n }\n\n return false;\n }\n\n public wheel(event: WheelEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n if (this.options.wheel)\n {\n const wheel = this.parent.plugins.get('wheel', true);\n\n if (!wheel)\n {\n const step = event.deltaMode ? this.options.lineHeight : 1;\n\n if (this.xDirection)\n {\n this.parent.x += event.deltaX * step * this.options.wheelScroll * this.reverse;\n }\n if (this.yDirection)\n {\n this.parent.y += event.deltaY * step * this.options.wheelScroll * this.reverse;\n }\n if (this.options.clampWheel)\n {\n this.clamp();\n }\n this.parent.emit('wheel-scroll', this.parent);\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n if (!this.parent.options.passiveWheel)\n {\n event.preventDefault();\n }\n\n return true;\n }\n }\n\n return false;\n }\n\n public resume()\n {\n this.last = null;\n this.paused = false;\n }\n\n public clamp()\n {\n const decelerate: Partial = this.parent.plugins.get('decelerate', true) || {};\n\n if (this.options.clampWheel !== 'y')\n {\n if (this.parent.screenWorldWidth < this.parent.screenWidth)\n {\n switch (this.underflowX)\n {\n case -1:\n this.parent.x = 0;\n break;\n case 1:\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth);\n break;\n default:\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n }\n }\n else\n if (this.parent.left < 0)\n {\n this.parent.x = 0;\n decelerate.x = 0;\n }\n else if (this.parent.right > this.parent.worldWidth)\n {\n this.parent.x = -this.parent.worldWidth * this.parent.scale.x + this.parent.screenWidth;\n decelerate.x = 0;\n }\n }\n if (this.options.clampWheel !== 'x')\n {\n if (this.parent.screenWorldHeight < this.parent.screenHeight)\n {\n switch (this.underflowY)\n {\n case -1:\n this.parent.y = 0;\n break;\n case 1:\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n break;\n default:\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n }\n }\n else\n {\n if (this.parent.top < 0)\n {\n this.parent.y = 0;\n decelerate.y = 0;\n }\n if (this.parent.bottom > this.parent.worldHeight)\n {\n this.parent.y = -this.parent.worldHeight * this.parent.scale.y + this.parent.screenHeight;\n decelerate.y = 0;\n }\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { DisplayObject } from '@pixi/display';\nimport type { IPointData } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Follow}. */\nexport interface IFollowOptions\n{\n /**\n * Speed to follow in px/frame (0 = teleport to location)\n *\n * @default 9\n */\n speed?: number;\n\n /**\n * Set acceleration to accelerate and decelerate at this rate; speed cannot be 0 to use acceleration\n *\n * @default null\n */\n acceleration?: number | null;\n\n /**\n * Radius (in world coordinates) of center circle where movement is allowed without moving the viewport\n *\n * @default null\n */\n radius?: number | null;\n}\n\nconst DEFAULT_FOLLOW_OPTIONS: Required = {\n speed: 0,\n acceleration: null,\n radius: null\n};\n\n/**\n * Plugin to follow a display-object.\n *\n * @see Viewport.follow\n * @public\n */\nexport class Follow extends Plugin\n{\n /** The options used to initialize this plugin. */\n public readonly options: Required;\n\n /** The target this plugin will make the viewport follow. */\n public target: DisplayObject;\n\n /** The velocity provided the viewport by following, at the current time. */\n protected velocity: IPointData;\n\n /**\n * This is called by {@link Viewport.follow}.\n *\n * @param parent\n * @param target - target to follow\n * @param options\n */\n constructor(parent: Viewport, target: DisplayObject, options: IFollowOptions = {})\n {\n super(parent);\n\n this.target = target;\n this.options = Object.assign({}, DEFAULT_FOLLOW_OPTIONS, options);\n this.velocity = { x: 0, y: 0 };\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n const center = this.parent.center;\n let toX = this.target.x;\n let toY = this.target.y;\n\n if (this.options.radius)\n {\n const distance = Math.sqrt(Math.pow(this.target.y - center.y, 2) + Math.pow(this.target.x - center.x, 2));\n\n if (distance > this.options.radius)\n {\n const angle = Math.atan2(this.target.y - center.y, this.target.x - center.x);\n\n toX = this.target.x - (Math.cos(angle) * this.options.radius);\n toY = this.target.y - (Math.sin(angle) * this.options.radius);\n }\n else\n {\n return;\n }\n }\n\n const deltaX = toX - center.x;\n const deltaY = toY - center.y;\n\n if (deltaX || deltaY)\n {\n if (this.options.speed)\n {\n if (this.options.acceleration)\n {\n const angle = Math.atan2(toY - center.y, toX - center.x);\n const distance = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));\n\n if (distance)\n {\n const decelerationDistance = (Math.pow(this.velocity.x, 2) + Math.pow(this.velocity.y, 2)) / (2 * this.options.acceleration);\n\n if (distance > decelerationDistance)\n {\n this.velocity = {\n x: Math.min(this.velocity.x + this.options.acceleration * elapsed, this.options.speed),\n y: Math.min(this.velocity.y + this.options.acceleration * elapsed, this.options.speed)\n };\n }\n else\n {\n this.velocity = {\n x: Math.max(this.velocity.x - this.options.acceleration * this.options.speed, 0),\n y: Math.max(this.velocity.y - this.options.acceleration * this.options.speed, 0)\n };\n }\n const changeX = Math.cos(angle) * this.velocity.x;\n const changeY = Math.sin(angle) * this.velocity.y;\n const x = Math.abs(changeX) > Math.abs(deltaX) ? toX : center.x + changeX;\n const y = Math.abs(changeY) > Math.abs(deltaY) ? toY : center.y + changeY;\n\n this.parent.moveCenter(x, y);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n else\n {\n const angle = Math.atan2(toY - center.y, toX - center.x);\n const changeX = Math.cos(angle) * this.options.speed;\n const changeY = Math.sin(angle) * this.options.speed;\n const x = Math.abs(changeX) > Math.abs(deltaX) ? toX : center.x + changeX;\n const y = Math.abs(changeY) > Math.abs(deltaY) ? toY : center.y + changeY;\n\n this.parent.moveCenter(x, y);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n else\n {\n this.parent.moveCenter(toX, toY);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\nimport type { InteractionEvent } from '@pixi/interaction';\n\n/** Insets for mouse edges scrolling regions */\nexport interface IMouseEdgesInsets {\n /** Distance from center of screen in screen pixels */\n radius?: number | null;\n\n /** Distance from all sides in screen pixels */\n distance?: number | null;\n\n /** Alternatively, set top distance (leave unset for no top scroll) */\n top?: number | null;\n\n /** Alternatively, set bottom distance (leave unset for no top scroll) */\n bottom?: number | null;\n\n /** Alternatively, set left distance (leave unset for no top scroll) */\n left?: number | null;\n\n /** Alternatively, set right distance (leave unset for no top scroll) */\n right?: number | null;\n}\n\n/** Options for {@link MouseEdges}. */\nexport interface IMouseEdgesOptions extends IMouseEdgesInsets {\n /** Speed in pixels/frame to scroll viewport */\n speed?: number;\n\n /** Reverse direction of scroll */\n reverse?: boolean;\n\n /** Don't use decelerate plugin even if it's installed */\n noDecelerate?: boolean;\n\n /**\n * If using radius, use linear movement (+/- 1, +/- 1) instead of angled movement.\n *\n * (Math.cos(angle from center), Math.sin(angle from center))\n */\n linear?: boolean;\n\n /** Allows plugin to continue working even when there's a `mousedown` event. */\n allowButtons?: boolean;\n}\n\nconst MOUSE_EDGES_OPTIONS: Required = {\n radius: null,\n distance: null,\n top: null,\n bottom: null,\n left: null,\n right: null,\n speed: 8,\n reverse: false,\n noDecelerate: false,\n linear: false,\n allowButtons: false,\n};\n\n/**\n * Scroll viewport when mouse hovers near one of the edges.\n *\n * @event mouse-edge-start(Viewport) emitted when mouse-edge starts\n * @event mouse-edge-end(Viewport) emitted when mouse-edge ends\n */\nexport class MouseEdges extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Readonly>;\n\n /** Factor from reverse option. */\n protected readonly reverse: -1 | 1;\n\n /** Radius squared */\n protected readonly radiusSquared: number | null;\n\n /** Scroll region size on the left side. */\n protected left!: number | null;\n\n /** Scroll region size on the top size. */\n protected top!: number | null;\n\n /** Scroll region size on the right side. */\n protected right!: number | null;\n\n /** Scroll region size on the bottom side. */\n protected bottom!: number | null;\n\n protected horizontal?: number | null;\n\n protected vertical?: number | null;\n\n /**\n * This is called by {@link Viewport.mouseEdges}.\n */\n constructor(parent: Viewport, options: IMouseEdgesOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, MOUSE_EDGES_OPTIONS, options);\n this.reverse = this.options.reverse ? 1 : -1;\n this.radiusSquared = typeof this.options.radius === 'number' ? Math.pow(this.options.radius, 2) : null;\n\n this.resize();\n }\n\n public resize(): void\n {\n const distance = this.options.distance;\n\n if (distance !== null)\n {\n this.left = distance;\n this.top = distance;\n this.right = this.parent.worldScreenWidth - distance;\n this.bottom = this.parent.worldScreenHeight - distance;\n }\n else if (!this.options.radius)\n {\n this.left = this.options.left;\n this.top = this.options.top;\n this.right = this.options.right === null ? null : this.parent.worldScreenWidth - this.options.right;\n this.bottom = this.options.bottom === null ? null : this.parent.worldScreenHeight - this.options.bottom;\n }\n }\n\n public down(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if (!this.options.allowButtons)\n {\n this.horizontal = this.vertical = null;\n }\n\n return false;\n }\n\n public move(event: InteractionEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if ((event.data.pointerType !== 'mouse' && event.data.identifier !== 1)\n || (!this.options.allowButtons && event.data.buttons !== 0))\n {\n return false;\n }\n\n const x = event.data.global.x;\n const y = event.data.global.y;\n\n if (this.radiusSquared)\n {\n const center = this.parent.toScreen(this.parent.center);\n const distance = Math.pow(center.x - x, 2) + Math.pow(center.y - y, 2);\n\n if (distance >= this.radiusSquared)\n {\n const angle = Math.atan2(center.y - y, center.x - x);\n\n if (this.options.linear)\n {\n this.horizontal = Math.round(Math.cos(angle)) * this.options.speed * this.reverse * (60 / 1000);\n this.vertical = Math.round(Math.sin(angle)) * this.options.speed * this.reverse * (60 / 1000);\n }\n else\n {\n this.horizontal = Math.cos(angle) * this.options.speed * this.reverse * (60 / 1000);\n this.vertical = Math.sin(angle) * this.options.speed * this.reverse * (60 / 1000);\n }\n }\n else\n {\n if (this.horizontal)\n {\n this.decelerateHorizontal();\n }\n if (this.vertical)\n {\n this.decelerateVertical();\n }\n\n this.horizontal = this.vertical = 0;\n }\n }\n else\n {\n if (this.left !== null && x < this.left)\n {\n this.horizontal = Number(this.reverse) * this.options.speed * (60 / 1000);\n }\n else if (this.right !== null && x > this.right)\n {\n this.horizontal = -1 * this.reverse * this.options.speed * (60 / 1000);\n }\n else\n {\n this.decelerateHorizontal();\n this.horizontal = 0;\n }\n if (this.top !== null && y < this.top)\n {\n this.vertical = Number(this.reverse) * this.options.speed * (60 / 1000);\n }\n else if (this.bottom !== null && y > this.bottom)\n {\n this.vertical = -1 * this.reverse * this.options.speed * (60 / 1000);\n }\n else\n {\n this.decelerateVertical();\n this.vertical = 0;\n }\n }\n\n return false;\n }\n\n private decelerateHorizontal(): void\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (this.horizontal && decelerate && !this.options.noDecelerate)\n {\n decelerate.activate({ x: (this.horizontal * this.options.speed * this.reverse) / (1000 / 60) });\n }\n }\n\n private decelerateVertical(): void\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (this.vertical && decelerate && !this.options.noDecelerate)\n {\n decelerate.activate({ y: (this.vertical * this.options.speed * this.reverse) / (1000 / 60) });\n }\n }\n\n public up(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if (this.horizontal)\n {\n this.decelerateHorizontal();\n }\n if (this.vertical)\n {\n this.decelerateVertical();\n }\n this.horizontal = this.vertical = null;\n\n return false;\n }\n\n public update(): void\n {\n if (this.paused)\n {\n return;\n }\n\n if (this.horizontal || this.vertical)\n {\n const center = this.parent.center;\n\n if (this.horizontal)\n {\n center.x += this.horizontal * this.options.speed;\n }\n if (this.vertical)\n {\n center.y += this.vertical * this.options.speed;\n }\n\n this.parent.moveCenter(center);\n this.parent.emit('moved', { viewport: this.parent, type: 'mouse-edges' });\n }\n }\n}\n","import { Plugin } from './Plugin';\nimport { Point } from '@pixi/math';\n\nimport type { IPointData } from '@pixi/math';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { IViewportTouch } from '../InputManager';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Pinch}. */\nexport interface IPinchOptions\n{\n /** Disable two-finger dragging. */\n noDrag?: boolean;\n\n /**\n * Percent to modify pinch speed.\n *\n * @default 1\n */\n percent?: number;\n\n /**\n * Factor to multiply two-finger drag to increase speed of movement\n *\n * @default 1\n */\n factor?: number;\n\n /** Place this point at center during zoom instead of center of two fingers */\n center?: Point | null;\n\n /** Axis to zoom */\n axis?: 'all' | 'x' | 'y';\n}\n\nconst DEFAULT_PINCH_OPTIONS: Required = {\n noDrag: false,\n percent: 1,\n center: null,\n factor: 1,\n axis: 'all',\n};\n\n/**\n * Plugin for enabling two-finger pinching (or dragging).\n *\n * @public\n */\nexport class Pinch extends Plugin\n{\n /** Options used to initialize this plugin. */\n public readonly options: Required;\n\n /** Flags whether this plugin is active, i.e. a pointer is down on the viewport. */\n public active = false;\n\n /** Flags whether the viewport is being pinched. */\n public pinching = false;\n\n protected moved = false;\n protected lastCenter?: IPointData | null;\n\n /**\n * This is called by {@link Viewport.pinch}.\n */\n constructor(parent: Viewport, options: IPinchOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_PINCH_OPTIONS, options);\n }\n\n public down(): boolean\n {\n if (this.parent.input.count() >= 2)\n {\n this.active = true;\n return true;\n }\n\n return false;\n }\n\n public isAxisX(): boolean\n {\n return ['all', 'x'].includes(this.options.axis);\n }\n\n public isAxisY(): boolean\n {\n return ['all', 'y'].includes(this.options.axis);\n }\n\n public move(e: InteractionEvent): boolean\n {\n if (this.paused || !this.active)\n {\n return false;\n }\n\n const x = e.data.global.x;\n const y = e.data.global.y;\n\n const pointers = this.parent.input.touches;\n\n if (pointers.length >= 2)\n {\n const first = pointers[0] as IViewportTouch;\n const second = pointers[1] as IViewportTouch;\n const last = (first.last && second.last)\n ? Math.sqrt(Math.pow(second.last.x - first.last.x, 2) + Math.pow(second.last.y - first.last.y, 2))\n : null;\n\n if (first.id === e.data.pointerId)\n {\n first.last = { x, y, data: e.data } as IPointData;\n }\n else if (second.id === e.data.pointerId)\n {\n second.last = { x, y, data: e.data } as IPointData;\n }\n if (last)\n {\n let oldPoint: IPointData | undefined;\n\n const point = {\n x: (first.last as IPointData).x +\n ((second.last as IPointData).x - (first.last as IPointData).x) / 2,\n y: (first.last as IPointData).y +\n ((second.last as IPointData).y - (first.last as IPointData).y) / 2,\n };\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point);\n }\n let dist = Math.sqrt(Math.pow(\n (second.last as IPointData).x - (first.last as IPointData).x, 2) +\n Math.pow((second.last as IPointData).y - (first.last as IPointData).y, 2));\n\n dist = dist === 0 ? dist = 0.0000000001 : dist;\n\n const change = (1 - last / dist) * this.options.percent\n * (this.isAxisX() ? this.parent.scale.x : this.parent.scale.y);\n\n if (this.isAxisX())\n {\n this.parent.scale.x += change;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y += change;\n }\n\n this.parent.emit('zoomed', { viewport: this.parent, type: 'pinch', center: point });\n\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += (point.x - newPoint.x) * this.options.factor;\n this.parent.y += (point.y - newPoint.y) * this.options.factor;\n this.parent.emit('moved', { viewport: this.parent, type: 'pinch' });\n }\n if (!this.options.noDrag && this.lastCenter)\n {\n this.parent.x += (point.x - this.lastCenter.x) * this.options.factor;\n this.parent.y += (point.y - this.lastCenter.y) * this.options.factor;\n this.parent.emit('moved', { viewport: this.parent, type: 'pinch' });\n }\n\n this.lastCenter = point;\n this.moved = true;\n }\n else if (!this.pinching)\n {\n this.parent.emit('pinch-start', this.parent);\n this.pinching = true;\n }\n\n return true;\n }\n\n return false;\n }\n\n public up(): boolean\n {\n if (this.pinching)\n {\n if (this.parent.input.touches.length <= 1)\n {\n this.active = false;\n this.lastCenter = null;\n this.pinching = false;\n this.moved = false;\n this.parent.emit('pinch-end', this.parent);\n\n return true;\n }\n }\n\n return false;\n }\n}\n","import { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Viewport } from '../Viewport';\n\nexport interface ISnapOptions\n{\n /** snap to the top-left of viewport instead of center */\n topLeft?: boolean;\n\n /**\n * Friction/frame to apply if decelerate is active\n *\n * @default 0.8\n */\n friction?: number;\n\n /**\n * @default 1000\n */\n time?: number;\n\n /** Easing function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /**\n * Pause snapping with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Removes this plugin after snapping is complete\n *\n * @default false\n */\n removeOnComplete?: boolean;\n\n /**\n * Removes this plugin if interrupted by any user input\n *\n * @default false\n */\n removeOnInterrupt?: boolean;\n\n /**\n * Starts the snap immediately regardless of whether the viewport is at the desired location\n *\n * @default false\n */\n forceStart?: boolean;\n}\n\nconst DEFAULT_SNAP_OPTIONS: Required = {\n topLeft: false,\n friction: 0.8,\n time: 1000,\n ease: 'easeInOutSine',\n interrupt: true,\n removeOnComplete: false,\n removeOnInterrupt: false,\n forceStart: false\n};\n\n/**\n * @event snap-start(Viewport) emitted each time a snap animation starts\n * @event snap-restart(Viewport) emitted each time a snap resets because of a change in viewport size\n * @event snap-end(Viewport) emitted each time snap reaches its target\n * @event snap-remove(Viewport) emitted if snap plugin is removed\n */\nexport class Snap extends Plugin\n{\n public readonly options: Required;\n public ease?: any;\n public x: number;\n public y: number;\n\n protected percent?: number;\n protected snapping?: { time: number } | null;\n protected deltaX?: number;\n protected deltaY?: number;\n protected startX?: number;\n protected startY?: number;\n\n /**\n * This is called by {@link Viewport.snap}.\n */\n constructor(parent: Viewport, x: number, y: number, options: ISnapOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_SNAP_OPTIONS, options);\n this.ease = ease(options.ease, 'easeInOutSine');\n this.x = x;\n this.y = y;\n\n if (this.options.forceStart)\n {\n this.snapStart();\n }\n }\n\n public snapStart(): void\n {\n this.percent = 0;\n this.snapping = { time: 0 };\n const current = this.options.topLeft ? this.parent.corner : this.parent.center;\n\n this.deltaX = this.x - current.x;\n this.deltaY = this.y - current.y;\n this.startX = current.x;\n this.startY = current.y;\n this.parent.emit('snap-start', this.parent);\n }\n\n public wheel(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap');\n }\n\n return false;\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap');\n }\n else if (this.options.interrupt)\n {\n this.snapping = null;\n }\n\n return false;\n }\n\n public up(): boolean\n {\n if (this.parent.input.count() === 0)\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (decelerate && (decelerate.x || decelerate.y))\n {\n decelerate.percentChangeX = decelerate.percentChangeY = this.options.friction;\n }\n }\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n if (this.options.interrupt && this.parent.input.count() !== 0)\n {\n return;\n }\n if (!this.snapping)\n {\n const current = this.options.topLeft ? this.parent.corner : this.parent.center;\n\n if (current.x !== this.x || current.y !== this.y)\n {\n this.snapStart();\n }\n }\n else\n {\n const snapping = this.snapping;\n\n snapping.time += elapsed;\n let finished;\n let x;\n let y;\n\n const startX = this.startX as number;\n const startY = this.startY as number;\n const deltaX = this.deltaX as number;\n const deltaY = this.deltaY as number;\n\n if (snapping.time > this.options.time)\n {\n finished = true;\n x = startX + deltaX;\n y = startY + deltaY;\n }\n else\n {\n const percent = this.ease(snapping.time, 0, 1, this.options.time);\n\n x = startX + (deltaX * percent);\n y = startY + (deltaY * percent);\n }\n if (this.options.topLeft)\n {\n this.parent.moveCorner(x, y);\n }\n else\n {\n this.parent.moveCenter(x, y);\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'snap' });\n\n if (finished)\n {\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap');\n }\n this.parent.emit('snap-end', this.parent);\n this.snapping = null;\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Point } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link SnapZoom}. */\nexport interface ISnapZoomOptions\n{\n /** the desired width to snap (to maintain aspect ratio, choose only width or height) */\n width?: number;\n\n /** the desired height to snap (to maintain aspect ratio, choose only width or height) */\n height?: number;\n\n /**\n * time for snapping in ms\n *\n * @default 1000\n */\n time?: number;\n\n /** ease function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /** Place this point at center during zoom instead of center of the viewport */\n center?: Point | null;\n\n /**\n * Pause snapping with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Removes this plugin after snapping is complete\n *\n * @default false\n */\n removeOnComplete?: boolean;\n\n /**\n * Removes this plugin if interrupted by any user input\n *\n * @default false\n */\n removeOnInterrupt?: boolean;\n\n /**\n * Starts the snap immediately regardless of whether the viewport is at the desired zoom\n *\n * @default false\n */\n forceStart?: boolean;\n\n /**\n * Zoom but do not move\n *\n * @default false\n */\n noMove?: boolean;\n}\n\nconst DEFAULT_SNAP_ZOOM_OPTIONS: Required = {\n width: 0,\n height: 0,\n time: 1000,\n ease: 'easeInOutSine',\n center: null,\n interrupt: true,\n removeOnComplete: false,\n removeOnInterrupt: false,\n forceStart: false,\n noMove: false\n};\n\n/**\n * @event snap-zoom-start(Viewport) emitted each time a fit animation starts\n * @event snap-zoom-end(Viewport) emitted each time fit reaches its target\n * @event snap-zoom-end(Viewport) emitted each time fit reaches its target\n */\nexport class SnapZoom extends Plugin\n{\n public readonly options: Required;\n\n protected ease: any;\n protected xScale: number;\n protected yScale: number;\n protected xIndependent: boolean;\n protected yIndependent: boolean;\n protected snapping?: {\n time: number;\n startX: number;\n startY: number;\n deltaX: number;\n deltaY: number;\n } | null;\n\n /**\n * This is called by {@link Viewport.snapZoom}.\n */\n constructor(parent: Viewport, options: ISnapZoomOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_SNAP_ZOOM_OPTIONS, options);\n this.ease = ease(this.options.ease);\n\n // Assign defaults for typescript.\n this.xIndependent = false;\n this.yIndependent = false;\n this.xScale = 0;\n this.yScale = 0;\n\n if (this.options.width > 0)\n {\n this.xScale = parent.screenWidth / this.options.width;\n this.xIndependent = true;\n }\n if (this.options.height > 0)\n {\n this.yScale = parent.screenHeight / this.options.height;\n this.yIndependent = true;\n }\n\n this.xScale = this.xIndependent ? (this.xScale as number) : (this.yScale as number);\n this.yScale = this.yIndependent ? (this.yScale as number) : this.xScale;\n\n if (this.options.time === 0)\n {\n // TODO: Fix this\n // @ts-expect-error todo\n parent.container.scale.x = this.xScale;\n\n // TODO: Fix this\n // @ts-expect-error todo\n parent.container.scale.y = this.yScale;\n\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n }\n else if (options.forceStart)\n {\n this.createSnapping();\n }\n }\n\n private createSnapping(): void\n {\n const startWorldScreenWidth = this.parent.worldScreenWidth;\n const startWorldScreenHeight = this.parent.worldScreenHeight;\n const endWorldScreenWidth = this.parent.screenWidth / this.xScale;\n const endWorldScreenHeight = this.parent.screenHeight / this.yScale;\n\n this.snapping = {\n time: 0,\n startX: startWorldScreenWidth,\n startY: startWorldScreenHeight,\n deltaX: endWorldScreenWidth - startWorldScreenWidth,\n deltaY: endWorldScreenHeight - startWorldScreenHeight\n };\n\n this.parent.emit('snap-zoom-start', this.parent);\n }\n\n public resize(): void\n {\n this.snapping = null;\n\n if (this.options.width > 0)\n {\n this.xScale = this.parent.screenWidth / this.options.width;\n }\n if (this.options.height > 0)\n {\n this.yScale = this.parent.screenHeight / this.options.height;\n }\n this.xScale = this.xIndependent ? this.xScale : this.yScale;\n this.yScale = this.yIndependent ? this.yScale : this.xScale;\n }\n\n public wheel(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n\n return false;\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n else if (this.options.interrupt)\n {\n this.snapping = null;\n }\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n if (this.options.interrupt && this.parent.input.count() !== 0)\n {\n return;\n }\n\n let oldCenter: Point | undefined;\n\n if (!this.options.center && !this.options.noMove)\n {\n oldCenter = this.parent.center;\n }\n if (!this.snapping)\n {\n if (this.parent.scale.x !== this.xScale || this.parent.scale.y !== this.yScale)\n {\n this.createSnapping();\n }\n }\n else if (this.snapping)\n {\n const snapping = this.snapping;\n\n snapping.time += elapsed;\n\n if (snapping.time >= this.options.time)\n {\n this.parent.scale.set(this.xScale, this.yScale);\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n this.parent.emit('snap-zoom-end', this.parent);\n this.snapping = null;\n }\n else\n {\n const snapping = this.snapping;\n const worldScreenWidth = this.ease(snapping.time, snapping.startX, snapping.deltaX, this.options.time);\n const worldScreenHeight = this.ease(snapping.time, snapping.startY, snapping.deltaY, this.options.time);\n\n this.parent.scale.x = this.parent.screenWidth / worldScreenWidth;\n this.parent.scale.y = this.parent.screenHeight / worldScreenHeight;\n }\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (!this.options.noMove)\n {\n if (!this.options.center)\n {\n this.parent.moveCenter(oldCenter as Point);\n }\n else\n {\n this.parent.moveCenter(this.options.center);\n }\n }\n }\n }\n\n public resume(): void\n {\n this.snapping = null;\n super.resume();\n }\n}\n","import { Plugin } from './Plugin';\nimport { IPointData, Point } from '@pixi/math';\n\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Wheel}. */\nexport interface IWheelOptions\n{\n /**\n * Percent to scroll with each spin\n *\n * @default 0.1\n */\n percent?: number;\n\n /**\n * smooth the zooming by providing the number of frames to zoom between wheel spins\n *\n * @default false\n */\n smooth?: false | number;\n\n /**\n * Stop smoothing with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Reverse the direction of the scroll\n *\n * @default false\n */\n reverse?: boolean;\n\n /**\n * Place this point at center during zoom instead of current mouse position\n *\n * @default null\n */\n center?: Point | null;\n\n /**\n * Scaling factor for non-DOM_DELTA_PIXEL scrolling events\n *\n * @default 20\n */\n lineHeight?: number;\n\n /**\n * Axis to zoom\n *\n * @default 'all'\n */\n axis?: 'all' | 'x' | 'y';\n}\n\nconst DEFAULT_WHEEL_OPTIONS: Required = {\n percent: 0.1,\n smooth: false,\n interrupt: true,\n reverse: false,\n center: null,\n lineHeight: 20,\n axis: 'all',\n};\n\n/**\n * Plugin for handling wheel scrolling for viewport zoom.\n *\n * @event wheel({wheel: {dx, dy, dz}, event, viewport})\n */\nexport class Wheel extends Plugin\n{\n public readonly options: Required;\n\n protected smoothing?: IPointData | null;\n protected smoothingCenter?: Point | null;\n protected smoothingCount?: number;\n\n /**\n * This is called by {@link Viewport.wheel}.\n */\n constructor(parent: Viewport, options: IWheelOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_WHEEL_OPTIONS, options);\n }\n\n public down(): boolean\n {\n if (this.options.interrupt)\n {\n this.smoothing = null;\n }\n\n return false;\n }\n\n protected isAxisX(): boolean\n {\n return ['all', 'x'].includes(this.options.axis);\n }\n\n protected isAxisY(): boolean\n {\n return ['all', 'y'].includes(this.options.axis);\n }\n\n public update(): void\n {\n if (this.smoothing)\n {\n const point = this.smoothingCenter;\n const change = this.smoothing;\n let oldPoint;\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point as IPointData);\n }\n if (this.isAxisX())\n {\n this.parent.scale.x += change.x;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y += change.y;\n }\n\n this.parent.emit('zoomed', { viewport: this.parent, type: 'wheel' });\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += (point as IPointData).x - newPoint.x;\n this.parent.y += (point as IPointData).y - newPoint.y;\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n (this.smoothingCount as number)++;\n\n if (this.smoothingCount as number >= this.options.smooth)\n {\n this.smoothing = null;\n }\n }\n }\n\n public wheel(e: WheelEvent): boolean | undefined\n {\n if (this.paused)\n {\n return;\n }\n\n const point = this.parent.input.getPointerPosition(e);\n const sign = this.options.reverse ? -1 : 1;\n const step = sign * -e.deltaY * (e.deltaMode ? this.options.lineHeight : 1) / 500;\n const change = Math.pow(2, (1 + this.options.percent) * step);\n\n if (this.options.smooth)\n {\n const original = {\n x: this.smoothing ? this.smoothing.x * (this.options.smooth - (this.smoothingCount as number)) : 0,\n y: this.smoothing ? this.smoothing.y * (this.options.smooth - (this.smoothingCount as number)) : 0\n };\n\n this.smoothing = {\n x: ((this.parent.scale.x + original.x) * change - this.parent.scale.x) / this.options.smooth,\n y: ((this.parent.scale.y + original.y) * change - this.parent.scale.y) / this.options.smooth,\n };\n this.smoothingCount = 0;\n this.smoothingCenter = point;\n }\n else\n {\n let oldPoint: IPointData | undefined;\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point);\n }\n if (this.isAxisX())\n {\n this.parent.scale.x *= change;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y *= change;\n }\n this.parent.emit('zoomed', { viewport: this.parent, type: 'wheel' });\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += point.x - newPoint.x;\n this.parent.y += point.y - newPoint.y;\n }\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n this.parent.emit('wheel', { wheel: { dx: e.deltaX, dy: e.deltaY, dz: e.deltaZ }, event: e, viewport: this.parent });\n\n if (!this.parent.options.passiveWheel)\n {\n return true;\n }\n }\n}\n","import { Point, Rectangle } from '@pixi/math';\n\nimport type { IPointData } from '@pixi/math';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { Viewport } from './Viewport';\n\nexport interface IViewportTouch {\n id: number;\n last: IPointData | null;\n}\n\n/**\n * Handles all input for Viewport\n *\n * @internal\n * @ignore\n * @private\n */\nexport class InputManager\n{\n public readonly viewport: Viewport;\n\n public clickedAvailable?: boolean;\n public isMouseDown?: boolean;\n public last?: Point | null;\n public wheelFunction?: (e: WheelEvent) => void;\n /** List of active touches on viewport */\n public touches: IViewportTouch[];\n\n constructor(viewport: Viewport)\n {\n this.viewport = viewport;\n this.touches = [];\n\n this.addListeners();\n }\n\n /** Add input listeners */\n private addListeners()\n {\n this.viewport.interactive = true;\n if (!this.viewport.forceHitArea)\n {\n this.viewport.hitArea = new Rectangle(0, 0, this.viewport.worldWidth, this.viewport.worldHeight);\n }\n this.viewport.on('pointerdown', this.down, this);\n this.viewport.on('pointermove', this.move, this);\n this.viewport.on('pointerup', this.up, this);\n this.viewport.on('pointerupoutside', this.up, this);\n this.viewport.on('pointercancel', this.up, this);\n this.viewport.on('pointerout', this.up, this);\n this.wheelFunction = (e) => this.handleWheel(e);\n this.viewport.options.divWheel.addEventListener(\n 'wheel',\n this.wheelFunction as any,\n { passive: this.viewport.options.passiveWheel });\n this.isMouseDown = false;\n }\n\n /**\n * Removes all event listeners from viewport\n * (useful for cleanup of wheel when removing viewport)\n */\n public destroy()\n {\n this.viewport.options.divWheel.removeEventListener('wheel', this.wheelFunction as any);\n }\n\n /**\n * handle down events for viewport\n *\n * @param {PIXI.InteractionEvent} event\n */\n public down(event: InteractionEvent)\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n if (event.data.pointerType === 'mouse')\n {\n this.isMouseDown = true;\n }\n else if (!this.get(event.data.pointerId))\n {\n this.touches.push({ id: event.data.pointerId, last: null });\n }\n if (this.count() === 1)\n {\n this.last = event.data.global.clone();\n\n // clicked event does not fire if viewport is decelerating or bouncing\n const decelerate = this.viewport.plugins.get('decelerate', true);\n const bounce = this.viewport.plugins.get('bounce', true);\n\n if ((!decelerate || !decelerate.isActive()) && (!bounce || !bounce.isActive()))\n {\n this.clickedAvailable = true;\n }\n else\n {\n this.clickedAvailable = false;\n }\n }\n else\n {\n this.clickedAvailable = false;\n }\n\n const stop = this.viewport.plugins.down(event);\n\n if (stop && this.viewport.options.stopPropagation)\n {\n event.stopPropagation();\n }\n }\n\n /** Clears all pointer events */\n public clear(): void\n {\n this.isMouseDown = false;\n this.touches = [];\n this.last = null;\n }\n\n /**\n * @param {number} change\n * @returns whether change exceeds threshold\n */\n public checkThreshold(change: number): boolean\n {\n if (Math.abs(change) >= this.viewport.threshold)\n {\n return true;\n }\n\n return false;\n }\n\n /** Handle move events for viewport */\n public move(event: InteractionEvent): void\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n\n const stop = this.viewport.plugins.move(event);\n\n if (this.clickedAvailable && this.last)\n {\n const distX = event.data.global.x - this.last.x;\n const distY = event.data.global.y - this.last.y;\n\n if (this.checkThreshold(distX) || this.checkThreshold(distY))\n {\n this.clickedAvailable = false;\n }\n }\n\n if (stop && this.viewport.options.stopPropagation)\n {\n event.stopPropagation();\n }\n }\n\n /** Handle up events for viewport */\n public up(event: InteractionEvent): void\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n\n if (event.data.pointerType === 'mouse')\n {\n this.isMouseDown = false;\n }\n\n if (event.data.pointerType !== 'mouse')\n {\n this.remove(event.data.pointerId);\n }\n\n const stop = this.viewport.plugins.up(event);\n\n if (this.clickedAvailable && this.count() === 0 && this.last)\n {\n this.viewport.emit('clicked', {\n event,\n screen: this.last,\n world: this.viewport.toWorld(this.last),\n viewport: this\n });\n this.clickedAvailable = false;\n }\n\n if (stop && this.viewport.options.stopPropagation)\n {\n event.stopPropagation();\n }\n }\n\n /** Gets pointer position if this.interaction is set */\n public getPointerPosition(event: WheelEvent): Point\n {\n const point = new Point();\n\n if (this.viewport.options.interaction)\n {\n this.viewport.options.interaction.mapPositionToPoint(point, event.clientX, event.clientY);\n }\n else\n {\n point.x = event.clientX;\n point.y = event.clientY;\n }\n\n return point;\n }\n\n /** Handle wheel events */\n public handleWheel(event: WheelEvent): void\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n\n // do not handle events coming from other elements\n if (this.viewport.options.interaction\n && (this.viewport.options.interaction as any).interactionDOMElement !== event.target)\n {\n return;\n }\n\n // only handle wheel events where the mouse is over the viewport\n const point = this.viewport.toLocal(this.getPointerPosition(event));\n\n if (this.viewport.left <= point.x\n && point.x <= this.viewport.right\n && this.viewport.top <= point.y\n && point.y <= this.viewport.bottom)\n {\n const stop = this.viewport.plugins.wheel(event);\n\n if (stop && !this.viewport.options.passiveWheel)\n {\n event.preventDefault();\n }\n }\n }\n\n public pause(): void\n {\n this.touches = [];\n this.isMouseDown = false;\n }\n\n /** Get touch by id */\n public get(id: number): IViewportTouch | null\n {\n for (const touch of this.touches)\n {\n if (touch.id === id)\n {\n return touch;\n }\n }\n\n return null;\n }\n\n /** Remove touch by number */\n remove(id: number): void\n {\n for (let i = 0; i < this.touches.length; i++)\n {\n if (this.touches[i].id === id)\n {\n this.touches.splice(i, 1);\n\n return;\n }\n }\n }\n\n /**\n * @returns {number} count of mouse/touch pointers that are down on the viewport\n */\n count(): number\n {\n return (this.isMouseDown ? 1 : 0) + this.touches.length;\n }\n}\n","import type {\n Animate,\n Bounce,\n Clamp,\n ClampZoom,\n Decelerate,\n Drag,\n Follow,\n MouseEdges,\n Pinch,\n Plugin,\n Snap,\n SnapZoom,\n Wheel,\n} from './plugins';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { Viewport } from './Viewport';\n\nconst PLUGIN_ORDER = [\n 'drag',\n 'pinch',\n 'wheel',\n 'follow',\n 'mouse-edges',\n 'decelerate',\n 'aniamte',\n 'bounce',\n 'snap-zoom',\n 'clamp-zoom',\n 'snap',\n 'clamp',\n];\n\n/**\n * Use this to access current plugins or add user-defined plugins\n *\n * @public\n */\nexport class PluginManager\n{\n /** Maps mounted plugins by their type */\n public plugins: Partial>;\n\n /**\n * List of plugins mounted\n *\n * This list is kept sorted by the internal priority of plugins (hard-coded).\n */\n public list: Array;\n\n /** The viewport using the plugins managed by `this`. */\n public readonly viewport: Viewport;\n\n /** This is called by {@link Viewport} to initialize the {@link Viewport.plugins plugins}. */\n constructor(viewport: Viewport)\n {\n this.viewport = viewport;\n this.list = [];\n this.plugins = {};\n }\n\n /**\n * Inserts a named plugin or a user plugin into the viewport\n * default plugin order: 'drag', 'pinch', 'wheel', 'follow', 'mouse-edges', 'decelerate', 'bounce',\n * 'snap-zoom', 'clamp-zoom', 'snap', 'clamp'\n *\n * @param {string} name of plugin\n * @param {Plugin} plugin - instantiated Plugin class\n * @param {number} index to insert userPlugin (otherwise inserts it at the end)\n */\n public add(name: string, plugin: Plugin, index: number = PLUGIN_ORDER.length)\n {\n this.plugins[name] = plugin;\n\n const current = PLUGIN_ORDER.indexOf(name);\n\n if (current !== -1)\n {\n PLUGIN_ORDER.splice(current, 1);\n }\n\n PLUGIN_ORDER.splice(index, 0, name);\n this.sort();\n }\n\n public get(name: 'animate', ignorePaused?: boolean): Animate | undefined | null;\n public get(name: 'bounce', ignorePaused?: boolean): Bounce | undefined | null;\n public get(name: 'clamp', ignorePaused?: boolean): Clamp | undefined | null;\n public get(name: 'clamp-zoom', ignorePaused?: boolean): ClampZoom | undefined | null;\n public get(name: 'decelerate', ignorePaused?: boolean): Decelerate | undefined | null;\n public get(name: 'drag', ignorePaused?: boolean): Drag | undefined | null;\n public get(name: 'follow', ignorePaused?: boolean): Follow | undefined | null;\n public get(name: 'mouse-edges', ignorePaused?: boolean): MouseEdges | undefined | null;\n public get(name: 'pinch', ignorePaused?: boolean): Pinch | undefined | null;\n public get(name: 'snap', ignorePaused?: boolean): Snap | undefined | null;\n public get(name: 'snap-zoom', ignorePaused?: boolean): SnapZoom | undefined | null;\n public get(name: 'wheel', ignorePaused?: boolean): Wheel | undefined | null;\n public get(name: string, ignorePaused?: boolean): T | undefined | null;\n\n /**\n * Get plugin\n *\n * @param {string} name of plugin\n * @param {boolean} [ignorePaused] return null if plugin is paused\n */\n public get(name: string, ignorePaused?: boolean): T | undefined | null\n {\n if (ignorePaused)\n {\n if (this.plugins[name]?.paused)\n {\n return null;\n }\n }\n\n return this.plugins[name] as T;\n }\n\n /**\n * Update all active plugins\n *\n * @internal\n * @ignore\n * @param {number} elapsed type in milliseconds since last update\n */\n public update(elapsed: number): void\n {\n for (const plugin of this.list)\n {\n plugin.update(elapsed);\n }\n }\n\n /**\n * Resize all active plugins\n *\n * @internal\n * @ignore\n */\n public resize(): void\n {\n for (const plugin of this.list)\n {\n plugin.resize();\n }\n }\n\n /** Clamps and resets bounce and decelerate (as needed) after manually moving viewport */\n public reset(): void\n {\n for (const plugin of this.list)\n {\n plugin.reset();\n }\n }\n\n /** removes all installed plugins */\n public removeAll(): void\n {\n this.plugins = {};\n this.sort();\n }\n\n /**\n * Removes installed plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public remove(name: string): void\n {\n if (this.plugins[name])\n {\n delete this.plugins[name];\n this.viewport.emit(`${name}-remove`);\n this.sort();\n }\n }\n\n /**\n * Pause plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public pause(name: string): void\n {\n this.plugins[name]?.pause();\n }\n\n /**\n * Resume plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public resume(name: string): void\n {\n this.plugins[name]?.resume();\n }\n\n /**\n * Sort plugins according to PLUGIN_ORDER\n *\n * @internal\n * @ignore\n */\n public sort()\n {\n this.list = [];\n\n for (const plugin of PLUGIN_ORDER)\n {\n if (this.plugins[plugin])\n {\n this.list.push(this.plugins[plugin] as Plugin);\n }\n }\n }\n\n /**\n * Handle down for all plugins\n *\n * @internal\n * @ignore\n */\n public down(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.list)\n {\n if (plugin.down(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle move for all plugins\n *\n * @internal\n * @ignore\n */\n public move(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.viewport.plugins.list)\n {\n if (plugin.move(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle up for all plugins\n *\n * @internal\n * @ignore\n */\n public up(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.list)\n {\n if (plugin.up(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle wheel event for all plugins\n *\n * @internal\n * @ignore\n */\n public wheel(e: WheelEvent): boolean\n {\n let result = false;\n\n for (const plugin of this.list)\n {\n if (plugin.wheel(e))\n {\n result = true;\n }\n }\n\n return result;\n }\n}\n","import { Container } from '@pixi/display';\nimport { Point, Rectangle } from '@pixi/math';\nimport { Ticker } from '@pixi/ticker';\n\nimport { InputManager } from './InputManager';\nimport { PluginManager } from './PluginManager';\nimport {\n Animate,\n Bounce,\n Clamp,\n ClampZoom,\n Decelerate,\n Drag,\n Follow,\n MouseEdges,\n Pinch,\n Snap,\n SnapZoom,\n Wheel\n} from './plugins';\n\nimport type { DisplayObject, IDestroyOptions } from '@pixi/display';\nimport type { IHitArea, InteractionManager } from '@pixi/interaction';\n\n/** Options for {@link Viewport}. */\nexport interface IViewportOptions {\n /** @default window.innerWidth */\n screenWidth?: number;\n\n /** @default window.innerHeight */\n screenHeight?: number;\n\n /** @default this.width */\n worldWidth?: number | null;\n\n /** @default this.height */\n worldHeight?: number | null;\n\n /**\n * Number of pixels to move to trigger an input event (e.g., drag, pinch) or disable a clicked event\n *\n * @default 5\n */\n threshold?: number;\n\n /**\n * Whether the 'wheel' event is set to passive (note: if false, e.preventDefault() will be called when wheel is used over the viewport)\n *\n * @default true\n */\n passiveWheel?: boolean;\n\n /**\n * Whether to stopPropagation of events that impact the viewport (except wheel events, see options.passiveWheel)\n */\n stopPropagation?: boolean;\n\n /**\n * Change the default hitArea from world size to a new value\n */\n forceHitArea?: Rectangle | null;\n\n /**\n * Set this if you want to manually call update() function on each frame\n *\n * @default false\n */\n noTicker?: boolean;\n\n /**\n * InteractionManager, available from instantiated `WebGLRenderer/CanvasRenderer.plugins.interaction`\n *\n * It's used to calculate pointer postion relative to canvas location on screen\n */\n interaction?: InteractionManager | null;\n\n /**\n * Remove oncontextmenu=() => {} from the divWheel element\n */\n disableOnContextMenu?: boolean;\n\n /**\n * div to attach the wheel event\n *\n * @default document.body\n */\n divWheel?: HTMLElement;\n\n /**\n * Use this PIXI.ticker for updates\n *\n * @default PIXI.Ticker.shared\n */\n ticker?: Ticker;\n}\n\nexport interface ICompleteViewportOptions extends IViewportOptions {\n screenWidth: number;\n screenHeight: number;\n threshold: number;\n passiveWheel: boolean;\n stopPropagation: boolean;\n noTicker: boolean;\n ticker: Ticker;\n}\n\nexport interface IViewportTransformState {\n x: number;\n y: number;\n scaleX: number;\n scaleY: number;\n}\n\nconst DEFAULT_VIEWPORT_OPTIONS: ICompleteViewportOptions = {\n screenWidth: window.innerWidth,\n screenHeight: window.innerHeight,\n worldWidth: null,\n worldHeight: null,\n threshold: 5,\n passiveWheel: true,\n stopPropagation: false,\n forceHitArea: null,\n noTicker: false,\n interaction: null,\n disableOnContextMenu: false,\n ticker: Ticker.shared,\n};\n\n/**\n * Main class to use when creating a Viewport\n *\n * @public\n * @fires clicked\n * @fires drag-start\n * @fires drag-end\n * @fires drag-remove\n * @fires pinch-start\n * @fires pinch-end\n * @fires pinch-remove\n * @fires snap-start\n * @fires snap-end\n * @fires snap-remove\n * @fires snap-zoom-start\n * @fires snap-zoom-end\n * @fires snap-zoom-remove\n * @fires bounce-x-start\n * @fires bounce-x-end\n * @fires bounce-y-start\n * @fires bounce-y-end\n * @fires bounce-remove\n * @fires wheel\n * @fires wheel-remove\n * @fires wheel-scroll\n * @fires wheel-scroll-remove\n * @fires mouse-edge-start\n * @fires mouse-edge-end\n * @fires mouse-edge-remove\n * @fires moved\n * @fires moved-end\n * @fires zoomed\n * @fires zoomed-end\n * @fires frame-end\n */\nexport class Viewport extends Container\n{\n /** Flags whether the viewport is being panned */\n public moving?: boolean;\n\n public screenWidth: number;\n public screenHeight: number;\n\n /** Number of pixels to move to trigger an input event (e.g., drag, pinch) or disable a clicked event */\n public threshold: number;\n\n public readonly input: InputManager;\n\n /** Use this to add user plugins or access existing plugins (e.g., to pause, resume, or remove them) */\n public readonly plugins: PluginManager;\n\n /** Flags whether the viewport zoom is being changed. */\n public zooming?: boolean;\n\n public lastViewport?: IViewportTransformState | null;\n\n /** The options passed when creating this viewport, merged with the default values */\n public readonly options: ICompleteViewportOptions & { divWheel: HTMLElement };\n\n private _dirty?: boolean;\n private _forceHitArea?: IHitArea | null;\n private _hitAreaDefault?: Rectangle;\n private _pause?: boolean;\n private readonly tickerFunction?: () => void;\n private _worldWidth?: number | null;\n private _worldHeight?: number | null;\n\n /**\n * @param options\n */\n constructor(options: IViewportOptions = {})\n {\n super();\n this.options = Object.assign(\n {},\n { divWheel: document.body },\n DEFAULT_VIEWPORT_OPTIONS,\n options\n );\n\n this.screenWidth = this.options.screenWidth;\n this.screenHeight = this.options.screenHeight;\n\n this._worldWidth = this.options.worldWidth;\n this._worldHeight = this.options.worldHeight;\n this.forceHitArea = this.options.forceHitArea;\n this.threshold = this.options.threshold;\n\n this.options.divWheel = this.options.divWheel || document.body;\n\n if (this.options.disableOnContextMenu)\n {\n this.options.divWheel.oncontextmenu = (e) => e.preventDefault();\n }\n if (!this.options.noTicker)\n {\n this.tickerFunction = () => this.update(this.options.ticker.elapsedMS);\n this.options.ticker.add(this.tickerFunction);\n }\n\n this.input = new InputManager(this);\n this.plugins = new PluginManager(this);\n }\n\n /** Overrides PIXI.Container's destroy to also remove the 'wheel' and PIXI.Ticker listeners */\n destroy(options: IDestroyOptions)\n {\n if (!this.options.noTicker && this.tickerFunction)\n {\n this.options.ticker.remove(this.tickerFunction);\n }\n\n this.input.destroy();\n super.destroy(options);\n }\n\n /**\n * Update viewport on each frame.\n *\n * By default, you do not need to call this unless you set `options.noTicker=true`.\n *\n * @param {number} elapsed time in milliseconds since last update\n */\n update(elapsed: number): void\n {\n if (!this.pause)\n {\n this.plugins.update(elapsed);\n\n if (this.lastViewport)\n {\n // Check for moved-end event\n if (this.lastViewport.x !== this.x || this.lastViewport.y !== this.y)\n {\n this.moving = true;\n }\n else if (this.moving)\n {\n this.emit('moved-end', this);\n this.moving = false;\n }\n\n // Check for zoomed-end event\n if (this.lastViewport.scaleX !== this.scale.x || this.lastViewport.scaleY !== this.scale.y)\n {\n this.zooming = true;\n }\n else if (this.zooming)\n {\n this.emit('zoomed-end', this);\n this.zooming = false;\n }\n }\n\n if (!this.forceHitArea)\n {\n this._hitAreaDefault = new Rectangle(this.left, this.top, this.worldScreenWidth, this.worldScreenHeight);\n this.hitArea = this._hitAreaDefault;\n }\n\n this._dirty = this._dirty || !this.lastViewport\n || this.lastViewport.x !== this.x || this.lastViewport.y !== this.y\n || this.lastViewport.scaleX !== this.scale.x || this.lastViewport.scaleY !== this.scale.y;\n\n this.lastViewport = {\n x: this.x,\n y: this.y,\n scaleX: this.scale.x,\n scaleY: this.scale.y\n };\n this.emit('frame-end', this);\n }\n }\n\n /** Use this to set screen and world sizes, needed for pinch/wheel/clamp/bounce. */\n resize(\n screenWidth = window.innerWidth,\n screenHeight = window.innerHeight,\n worldWidth: number,\n worldHeight: number\n ): void\n {\n this.screenWidth = screenWidth;\n this.screenHeight = screenHeight;\n\n if (typeof worldWidth !== 'undefined')\n {\n this._worldWidth = worldWidth;\n }\n if (typeof worldHeight !== 'undefined')\n {\n this._worldHeight = worldHeight;\n }\n\n this.plugins.resize();\n this.dirty = true;\n }\n\n /** World width, in pixels */\n get worldWidth(): number\n {\n if (this._worldWidth)\n {\n return this._worldWidth;\n }\n\n return this.width / this.scale.x;\n }\n set worldWidth(value)\n {\n this._worldWidth = value;\n this.plugins.resize();\n }\n\n /** World height, in pixels */\n get worldHeight(): number\n {\n if (this._worldHeight)\n {\n return this._worldHeight;\n }\n\n return this.height / this.scale.y;\n }\n set worldHeight(value)\n {\n this._worldHeight = value;\n this.plugins.resize();\n }\n\n /** Get visible world bounds of viewport */\n public getVisibleBounds(): Rectangle\n {\n return new Rectangle(this.left, this.top, this.worldScreenWidth, this.worldScreenHeight);\n }\n\n /** Change coordinates from screen to world */\n public toWorld(x: number, y: number): Point;\n /** Change coordinates from screen to world */\n public toWorld(screenPoint: Point): Point;\n\n public toWorld(x: number | Point, y?: number): Point\n {\n if (arguments.length === 2)\n {\n return this.toLocal(new Point(x as number, y));\n }\n\n return this.toLocal(x as Point);\n }\n\n /** Change coordinates from world to screen */\n public toScreen(x: number, y: number): Point;\n /** Change coordinates from world to screen */\n public toScreen(worldPoint: Point): Point;\n\n public toScreen(x: number | Point, y?: number): Point\n {\n if (arguments.length === 2)\n {\n return this.toGlobal(new Point(x as number, y));\n }\n\n return this.toGlobal(x as Point);\n }\n\n /** Screen width in world coordinates */\n get worldScreenWidth(): number\n {\n return this.screenWidth / this.scale.x;\n }\n\n /** Screen height in world coordinates */\n get worldScreenHeight(): number\n {\n return this.screenHeight / this.scale.y;\n }\n\n /** World width in screen coordinates */\n get screenWorldWidth(): number\n {\n return this.worldWidth * this.scale.x;\n }\n\n /** World height in screen coordinates */\n get screenWorldHeight()\n {\n return this.worldHeight * this.scale.y;\n }\n\n /** Center of screen in world coordinates */\n get center(): Point\n {\n return new Point(\n this.worldScreenWidth / 2 - this.x / this.scale.x,\n this.worldScreenHeight / 2 - this.y / this.scale.y,\n );\n }\n set center(value: Point)\n {\n this.moveCenter(value);\n }\n\n /** Move center of viewport to (x, y) */\n public moveCenter(x: number, y: number): this;\n\n /** Move center of viewport to {@code center}. */\n public moveCenter(center: Point): this;\n\n public moveCenter(...args: [number, number] | [Point]): this\n {\n let x: number;\n let y: number;\n\n if (typeof args[0] === 'number')\n {\n x = args[0];\n y = args[1] as number;\n }\n else\n {\n x = args[0].x;\n y = args[0].y;\n }\n\n const newX = (this.worldScreenWidth / 2 - x) * this.scale.x;\n const newY = (this.worldScreenHeight / 2 - y) * this.scale.y;\n\n if (this.x !== newX || this.y !== newY)\n {\n this.position.set(newX, newY);\n this.plugins.reset();\n this.dirty = true;\n }\n\n return this;\n }\n\n /** Top-left corner of Viewport */\n get corner(): Point\n {\n return new Point(-this.x / this.scale.x, -this.y / this.scale.y);\n }\n set corner(value: Point)\n {\n this.moveCorner(value);\n }\n\n /** Move viewport's top-left corner; also clamps and resets decelerate and bounce (as needed) */\n public moveCorner(x: number, y: number): this;\n\n /** move viewport's top-left corner; also clamps and resets decelerate and bounce (as needed) */\n public moveCorner(center: Point): this;\n\n public moveCorner(...args: [number, number] | [Point])\n {\n let x;\n let y;\n\n if (args.length === 1)\n {\n x = -args[0].x * this.scale.x;\n y = -args[0].y * this.scale.y;\n }\n else\n {\n x = -args[0] * this.scale.x;\n y = -args[1] * this.scale.y;\n }\n\n if (x !== this.x || y !== this.y)\n {\n this.position.set(x, y);\n this.plugins.reset();\n this.dirty = true;\n }\n\n return this;\n }\n\n /** Get how many world pixels fit in screen's width */\n get screenWidthInWorldPixels()\n {\n return this.screenWidth / this.scale.x;\n }\n\n /** Get how many world pixels fit on screen's height */\n get screenHeightInWorldPixels()\n {\n return this.screenHeight / this.scale.y;\n }\n\n /**\n * Find the scale value that fits a world width on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param width - Width in world pixels\n * @return - scale\n */\n findFitWidth(width: number): number\n {\n return this.screenWidth / width;\n }\n\n /**\n * Finds the scale value that fits a world height on the screens\n * does not change the viewport (use fit... to change)\n *\n * @param height - Height in world pixels\n * @return - scale\n */\n findFitHeight(height: number): number\n {\n return this.screenHeight / height;\n }\n\n /**\n * Finds the scale value that fits the smaller of a world width and world height on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param {number} width in world pixels\n * @param {number} height in world pixels\n * @returns {number} scale\n */\n findFit(width: number, height: number): number\n {\n const scaleX = this.screenWidth / width;\n const scaleY = this.screenHeight / height;\n\n return Math.min(scaleX, scaleY);\n }\n\n /**\n * Finds the scale value that fits the larger of a world width and world height on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param {number} width in world pixels\n * @param {number} height in world pixels\n * @returns {number} scale\n */\n findCover(width: number, height: number): number\n {\n const scaleX = this.screenWidth / width;\n const scaleY = this.screenHeight / height;\n\n return Math.max(scaleX, scaleY);\n }\n\n /**\n * Change zoom so the width fits in the viewport\n *\n * @param width - width in world coordinates\n * @param center - maintain the same center\n * @param scaleY - whether to set scaleY=scaleX\n * @param noClamp - whether to disable clamp-zoom\n * @returns {Viewport} this\n */\n fitWidth(width = this.worldWidth, center?: boolean, scaleY = true, noClamp?: boolean): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.x = this.screenWidth / width;\n\n if (scaleY)\n {\n this.scale.y = this.scale.x;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (!noClamp && clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so the height fits in the viewport\n *\n * @param {number} [height=this.worldHeight] in world coordinates\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @param {boolean} [scaleX=true] whether to set scaleX = scaleY\n * @param {boolean} [noClamp] whether to disable clamp-zoom\n * @returns {Viewport} this\n */\n fitHeight(height = this.worldHeight, center?: boolean, scaleX = true, noClamp?: boolean)\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.y = this.screenHeight / height;\n\n if (scaleX)\n {\n this.scale.x = this.scale.y;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (!noClamp && clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so it fits the entire world in the viewport\n *\n * @param {boolean} center maintain the same center of the screen after zoom\n * @returns {Viewport} this\n */\n fitWorld(center?: boolean): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n\n this.scale.x = this.screenWidth / this.worldWidth;\n this.scale.y = this.screenHeight / this.worldHeight;\n\n if (this.scale.x < this.scale.y)\n {\n this.scale.y = this.scale.x;\n }\n else\n {\n this.scale.x = this.scale.y;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so it fits the size or the entire world in the viewport\n *\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @param {number} [width=this.worldWidth] desired width\n * @param {number} [height=this.worldHeight] desired height\n * @returns {Viewport} this\n */\n fit(center?: boolean, width = this.worldWidth, height = this.worldHeight): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n\n this.scale.x = this.screenWidth / width;\n this.scale.y = this.screenHeight / height;\n\n if (this.scale.x < this.scale.y)\n {\n this.scale.y = this.scale.x;\n }\n else\n {\n this.scale.x = this.scale.y;\n }\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n // eslint-disable-next-line\n // @ts-ignore\n set visible(value: boolean)\n {\n if (!value)\n {\n this.input.clear();\n }\n\n super.visible = value;\n }\n\n /**\n * Zoom viewport to specific value.\n *\n * @param {number} scale value (e.g., 1 would be 100%, 0.25 would be 25%)\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n setZoom(scale: number, center?: boolean): this\n {\n let save;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.set(scale);\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Zoom viewport by a certain percent (in both x and y direction).\n *\n * @param {number} percent change (e.g., 0.25 would increase a starting scale of 1.0 to 1.25)\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n zoomPercent(percent: number, center?: boolean): this\n {\n return this.setZoom(this.scale.x + this.scale.x * percent, center);\n }\n\n /**\n * Zoom viewport by increasing/decreasing width by a certain number of pixels.\n *\n * @param {number} change in pixels\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n zoom(change: number, center?: boolean)\n {\n this.fitWidth(change + this.worldScreenWidth, center);\n\n return this;\n }\n\n /** Changes scale of viewport and maintains center of viewport */\n get scaled(): number\n {\n return this.scale.x;\n }\n set scaled(scale: number)\n {\n this.setZoom(scale, true);\n }\n\n /**\n * @param {SnapZoomOptions} options\n */\n snapZoom(options: any): this\n {\n this.plugins.add('snap-zoom', new SnapZoom(this, options));\n\n return this;\n }\n\n /** Is container out of world bounds */\n OOB(): {\n left: boolean;\n right: boolean;\n top: boolean;\n bottom: boolean;\n cornerPoint: Point;\n }\n {\n return {\n left: this.left < 0,\n right: this.right > this.worldWidth,\n top: this.top < 0,\n bottom: this.bottom > this.worldHeight,\n cornerPoint: new Point(\n this.worldWidth * this.scale.x - this.screenWidth,\n this.worldHeight * this.scale.y - this.screenHeight\n )\n };\n }\n\n /** World coordinates of the right edge of the screen */\n get right(): number\n {\n return -this.x / this.scale.x + this.worldScreenWidth;\n }\n set right(value: number)\n {\n this.x = -value * this.scale.x + this.screenWidth;\n this.plugins.reset();\n }\n\n /** World coordinates of the left edge of the screen */\n get left(): number\n {\n return -this.x / this.scale.x;\n }\n set left(value: number)\n {\n this.x = -value * this.scale.x;\n this.plugins.reset();\n }\n\n /** World coordinates of the top edge of the screen */\n get top(): number\n {\n return -this.y / this.scale.y;\n }\n set top(value: number)\n {\n this.y = -value * this.scale.y;\n this.plugins.reset();\n }\n\n /** World coordinates of the bottom edge of the screen */\n get bottom(): number\n {\n return -this.y / this.scale.y + this.worldScreenHeight;\n }\n set bottom(value: number)\n {\n this.y = -value * this.scale.y + this.screenHeight;\n this.plugins.reset();\n }\n\n /**\n * Determines whether the viewport is dirty (i.e., needs to be renderered to the screen because of a change)\n */\n get dirty(): boolean\n {\n return !!this._dirty;\n }\n set dirty(value: boolean)\n {\n this._dirty = value;\n }\n\n /**\n * Permanently changes the Viewport's hitArea\n *\n * NOTE: if not set then hitArea = PIXI.Rectangle(Viewport.left, Viewport.top, Viewport.worldScreenWidth, Viewport.worldScreenHeight)\n */\n get forceHitArea(): IHitArea | null | undefined\n {\n return this._forceHitArea;\n }\n set forceHitArea(value: IHitArea | null | undefined)\n {\n if (value)\n {\n this._forceHitArea = value;\n this.hitArea = value;\n }\n else\n {\n this._forceHitArea = null;\n this.hitArea = new Rectangle(0, 0, this.worldWidth, this.worldHeight);\n }\n }\n\n /**\n * Enable one-finger touch to drag\n *\n * NOTE: if you expect users to use right-click dragging, you should enable `viewport.options.disableOnContextMenu`\n * to avoid the context menu popping up on each right-click drag.\n *\n * @param {DragOptions} [options]\n * @returns {Viewport} this\n */\n public drag(options: any): this\n {\n this.plugins.add('drag', new Drag(this, options));\n\n return this;\n }\n\n /**\n * Clamp to world boundaries or other provided boundaries\n *\n * NOTES:\n * clamp is disabled if called with no options; use { direction: 'all' } for all edge clamping\n * screenWidth, screenHeight, worldWidth, and worldHeight needs to be set for this to work properly\n *\n * @param {ClampOptions} [options]\n * @returns {Viewport} this\n */\n public clamp(options: any): this\n {\n this.plugins.add('clamp', new Clamp(this, options));\n\n return this;\n }\n\n /**\n * Decelerate after a move\n *\n * NOTE: this fires 'moved' event during deceleration\n *\n * @param {DecelerateOptions} [options]\n * @return {Viewport} this\n */\n public decelerate(options: any): this\n {\n this.plugins.add('decelerate', new Decelerate(this, options));\n\n return this;\n }\n\n /**\n * Bounce on borders\n * NOTES:\n * screenWidth, screenHeight, worldWidth, and worldHeight needs to be set for this to work properly\n * fires 'moved', 'bounce-x-start', 'bounce-y-start', 'bounce-x-end', and 'bounce-y-end' events\n * @param {object} [options]\n * @param {string} [options.sides=all] all, horizontal, vertical, or combination of top, bottom, right, left (e.g., 'top-bottom-right')\n * @param {number} [options.friction=0.5] friction to apply to decelerate if active\n * @param {number} [options.time=150] time in ms to finish bounce\n * @param {object} [options.bounceBox] use this bounceBox instead of (0, 0, viewport.worldWidth, viewport.worldHeight)\n * @param {number} [options.bounceBox.x=0]\n * @param {number} [options.bounceBox.y=0]\n * @param {number} [options.bounceBox.width=viewport.worldWidth]\n * @param {number} [options.bounceBox.height=viewport.worldHeight]\n * @param {string|function} [options.ease=easeInOutSine] ease function or name (see http://easings.net/ for supported names)\n * @param {string} [options.underflow=center] (top/bottom/center and left/right/center, or center) where to place world if too small for screen\n * @return {Viewport} this\n */\n public bounce(options: any): this\n {\n this.plugins.add('bounce', new Bounce(this, options));\n\n return this;\n }\n\n /**\n * Enable pinch to zoom and two-finger touch to drag\n *\n * @param {PinchOptions} [options]\n * @return {Viewport} this\n */\n public pinch(options: any): this\n {\n this.plugins.add('pinch', new Pinch(this, options));\n\n return this;\n }\n\n /**\n * Snap to a point\n *\n * @param {number} x\n * @param {number} y\n * @param {SnapOptions} [options]\n * @return {Viewport} this\n */\n public snap(x: number, y: number, options: any): this\n {\n this.plugins.add('snap', new Snap(this, x, y, options));\n\n return this;\n }\n\n /**\n * Follow a target\n *\n * NOTES:\n * uses the (x, y) as the center to follow; for PIXI.Sprite to work properly, use sprite.anchor.set(0.5)\n * options.acceleration is not perfect as it doesn't know the velocity of the target\n * it adds acceleration to the start of movement and deceleration to the end of movement when the target is stopped\n * fires 'moved' event\n * @param {PIXI.DisplayObject} target to follow\n * @param {FollowOptions} [options]\n * @returns {Viewport} this\n */\n public follow(target: DisplayObject, options: any): this\n {\n this.plugins.add('follow', new Follow(this, target, options));\n\n return this;\n }\n\n /**\n * Zoom using mouse wheel\n *\n * @param {WheelOptions} [options]\n * @return {Viewport} this\n */\n public wheel(options: any): this\n {\n this.plugins.add('wheel', new Wheel(this, options));\n\n return this;\n }\n\n /**\n * Animate the position and/or scale of the viewport\n *\n * @param {AnimateOptions} options\n * @returns {Viewport} this\n */\n public animate(options: any): this\n {\n this.plugins.add('animate', new Animate(this, options));\n\n return this;\n }\n\n /**\n * Enable clamping of zoom to constraints\n *\n * The minWidth/Height settings are how small the world can get (as it would appear on the screen)\n * before clamping. The maxWidth/maxHeight is how larger the world can scale (as it would appear on\n * the screen) before clamping.\n *\n * For example, if you have a world size of 1000 x 1000 and a screen size of 100 x 100, if you set\n * minWidth/Height = 100 then the world will not be able to zoom smaller than the screen size (ie,\n * zooming out so it appears smaller than the screen). Similarly, if you set maxWidth/Height = 100\n * the world will not be able to zoom larger than the screen size (ie, zooming in so it appears\n * larger than the screen).\n *\n * @param {ClampZoomOptions} [options]\n * @return {Viewport} this\n */\n public clampZoom(options: any): this\n {\n this.plugins.add('clamp-zoom', new ClampZoom(this, options));\n\n return this;\n }\n\n /**\n * Scroll viewport when mouse hovers near one of the edges or radius-distance from center of screen.\n *\n * NOTE: fires 'moved' event\n *\n * @param {MouseEdgesOptions} [options]\n */\n public mouseEdges(options: any): this\n {\n this.plugins.add('mouse-edges', new MouseEdges(this, options));\n\n return this;\n }\n\n /** Pause viewport (including animation updates such as decelerate) */\n get pause(): boolean\n {\n return !!this._pause;\n }\n set pause(value: boolean)\n {\n this._pause = value;\n\n this.lastViewport = null;\n this.moving = false;\n this.zooming = false;\n\n if (value)\n {\n this.input.pause();\n }\n }\n\n /**\n * Move the viewport so the bounding box is visible\n *\n * @param x - left\n * @param y - top\n * @param width\n * @param height\n * @param resizeToFit - Resize the viewport so the box fits within the viewport\n */\n public ensureVisible(x: number, y: number, width: number, height: number, resizeToFit?: boolean)\n {\n if (resizeToFit && (width > this.worldScreenWidth || height > this.worldScreenHeight))\n {\n this.fit(true, width, height);\n this.emit('zoomed', { viewport: this, type: 'ensureVisible' });\n }\n let moved = false;\n\n if (x < this.left)\n {\n this.left = x;\n moved = true;\n }\n else if (x + width > this.right)\n {\n this.right = x + width;\n moved = true;\n }\n if (y < this.top)\n {\n this.top = y;\n moved = true;\n }\n else if (y + height > this.bottom)\n {\n this.bottom = y + height;\n moved = true;\n }\n if (moved)\n {\n this.emit('moved', { viewport: this, type: 'ensureVisible' });\n }\n }\n}\n\n/**\n * Fires after a mouse or touch click\n * @event Viewport#clicked\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a drag starts\n * @event Viewport#drag-start\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a drag ends\n * @event Viewport#drag-end\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a pinch starts\n * @event Viewport#pinch-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a pinch end\n * @event Viewport#pinch-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap starts\n * @event Viewport#snap-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap ends\n * @event Viewport#snap-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap-zoom starts\n * @event Viewport#snap-zoom-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap-zoom ends\n * @event Viewport#snap-zoom-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce starts in the x direction\n * @event Viewport#bounce-x-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce ends in the x direction\n * @event Viewport#bounce-x-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce starts in the y direction\n * @event Viewport#bounce-y-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce ends in the y direction\n * @event Viewport#bounce-y-end\n * @type {Viewport}\n */\n\n/**\n * Fires when for a mouse wheel event\n * @event Viewport#wheel\n * @type {object}\n * @property {object} wheel\n * @property {number} wheel.dx\n * @property {number} wheel.dy\n * @property {number} wheel.dz\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a wheel-scroll occurs\n * @event Viewport#wheel-scroll\n * @type {Viewport}\n */\n\n/**\n * Fires when a mouse-edge starts to scroll\n * @event Viewport#mouse-edge-start\n * @type {Viewport}\n */\n\n/**\n * Fires when the mouse-edge scrolling ends\n * @event Viewport#mouse-edge-end\n * @type {Viewport}\n */\n\n/**\n * Fires when viewport moves through UI interaction, deceleration, ensureVisible, or follow\n * @event Viewport#moved\n * @type {object}\n * @property {Viewport} viewport\n * @property {string} type - (drag, snap, pinch, follow, bounce-x, bounce-y,\n * clamp-x, clamp-y, decelerate, mouse-edges, wheel, ensureVisible)\n */\n\n/**\n * Fires when viewport moves through UI interaction, deceleration, ensureVisible, or follow\n * @event Viewport#zoomed\n * @type {object}\n * @property {Viewport} viewport\n * @property {string} type (drag-zoom, pinch, wheel, clamp-zoom, ensureVisible)\n */\n\n/**\n * Fires when viewport stops moving\n * @event Viewport#moved-end\n * @type {Viewport}\n */\n\n/**\n * Fires when viewport stops zooming\n * @event Viewport#zoomed-end\n * @type {Viewport}\n */\n\n/**\n* Fires at the end of an update frame\n* @event Viewport#frame-end\n* @type {Viewport}\n*/\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAGA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC1FA;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACpBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wRACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;qgBC1RA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC3XA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC1RA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC7IA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACrRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC1eA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACvJA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACxRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,8HACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC/JA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACxNA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACrRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC3NA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;ACrSA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACpRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;"} \ No newline at end of file diff --git a/dist/esm/viewport.es.js b/dist/esm/viewport.es.js index c113878b..d7a5a893 100644 --- a/dist/esm/viewport.es.js +++ b/dist/esm/viewport.es.js @@ -2,7 +2,7 @@ /*! * pixi-viewport - v4.30.0 - * Compiled Thu, 22 Apr 2021 09:16:32 UTC + * Compiled Thu, 22 Apr 2021 10:41:51 UTC * * pixi-viewport is licensed under the MIT License. * http://www.opensource.org/licenses/mit-license @@ -1135,7 +1135,7 @@ class ClampZoom extends Plugin this.clamp(); } - /** Clamp the viewport's zoom immediately. */ + /** Clamp the viewport scale zoom) */ clamp() { if (this.paused) @@ -1188,20 +1188,59 @@ class ClampZoom extends Plugin } } else + if (this.options.minScale || this.options.maxScale) { - let scale = this.parent.scale.x; + const minScale = { x: null, y: null }; + const maxScale = { x: null, y: null }; - if (this.options.minScale !== null && scale < this.options.minScale) + if (typeof this.options.minScale === 'number') { - scale = this.options.minScale; + minScale.x = this.options.minScale; + minScale.y = this.options.minScale; } - if (this.options.maxScale !== null && scale > this.options.maxScale) + else if (this.options.minScale !== null) { - scale = this.options.maxScale; + const optsMinScale = this.options.minScale ; + + minScale.x = typeof optsMinScale.x === 'undefined' ? null : optsMinScale.x; + minScale.y = typeof optsMinScale.y === 'undefined' ? null : optsMinScale.y; + } + + if (typeof this.options.maxScale === 'number') + { + maxScale.x = this.options.maxScale; + maxScale.y = this.options.maxScale; + } + else if (this.options.maxScale !== null) + { + const optsMaxScale = this.options.maxScale ; + + maxScale.x = typeof optsMaxScale.x === 'undefined' ? null : optsMaxScale.x; + maxScale.y = typeof optsMaxScale.y === 'undefined' ? null : optsMaxScale.y; + } + + let scaleX = this.parent.scale.x; + let scaleY = this.parent.scale.y; + + if (minScale.x !== null && scaleX < minScale.x) + { + scaleX = minScale.x; } - if (scale !== this.parent.scale.x) + if (maxScale.x !== null && scaleX > maxScale.x) { - this.parent.scale.set(scale); + scaleX = maxScale.x; + } + if (minScale.y !== null && scaleY < minScale.y) + { + scaleY = minScale.y; + } + if (maxScale.y !== null && scaleY > maxScale.y) + { + scaleY = maxScale.y; + } + if (scaleX !== this.parent.scale.x || scaleY !== this.parent.scale.y) + { + this.parent.scale.set(scaleX, scaleY); this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' }); } } @@ -1213,6 +1252,8 @@ class ClampZoom extends Plugin } } +/** This allows independent x and y values for min/maxScale */ + const DEFAULT_DECELERATE_OPTIONS = { friction: 0.98, bounce: 0.8, diff --git a/dist/esm/viewport.es.js.map b/dist/esm/viewport.es.js.map index 6b9f6170..6fab9862 100644 --- a/dist/esm/viewport.es.js.map +++ b/dist/esm/viewport.es.js.map @@ -1 +1 @@ -{"version":3,"file":"viewport.es.js","sources":["../../src/plugins/Plugin.ts","../../src/ease.ts","../../src/plugins/Animate.ts","../../src/plugins/Bounce.ts","../../src/plugins/Clamp.ts","../../src/plugins/ClampZoom.ts","../../src/plugins/Decelerate.ts","../../src/plugins/Drag.ts","../../src/plugins/Follow.ts","../../src/plugins/MouseEdges.ts","../../src/plugins/Pinch.ts","../../src/plugins/Snap.ts","../../src/plugins/SnapZoom.ts","../../src/plugins/Wheel.ts","../../src/InputManager.ts","../../src/PluginManager.ts","../../src/Viewport.ts"],"sourcesContent":["import type { InteractionEvent } from '@pixi/interaction';\nimport type { Viewport } from '../Viewport';\n\n/**\n * Derive this class to create user-defined plugins\n *\n * @public\n */\nexport class Plugin\n{\n /** The viewport to which this plugin is attached. */\n public readonly parent: Viewport;\n\n /**\n * Flags whether this plugin has been \"paused\".\n *\n * @see Plugin#pause\n * @see Plugin#resume\n */\n public paused: boolean;\n\n /** @param {Viewport} parent */\n constructor(parent: Viewport)\n {\n this.parent = parent;\n this.paused = false;\n }\n\n /** Called when plugin is removed */\n public destroy()\n {\n // Override for implementation\n }\n\n /** Handler for pointerdown PIXI event */\n public down(_e: InteractionEvent): boolean\n {\n return false;\n }\n\n /** Handler for pointermove PIXI event */\n public move(_e: InteractionEvent): boolean\n {\n return false;\n }\n\n /** Handler for pointerup PIXI event */\n public up(_e: InteractionEvent): boolean\n {\n return false;\n }\n\n /** Handler for wheel event on div */\n public wheel(_e: WheelEvent): boolean | undefined\n {\n return false;\n }\n\n /**\n * Called on each tick\n * @param {number} elapsed time in millisecond since last update\n */\n public update(_delta: number): void\n {\n // Override for implementation\n }\n\n /** Called when the viewport is resized */\n public resize()\n {\n // Override for implementation\n }\n\n /** Called when the viewport is manually moved */\n public reset(): void\n {\n // Override for implementation\n }\n\n /** Pause the plugin */\n public pause(): void\n {\n this.paused = true;\n }\n\n /** Un-pause the plugin */\n public resume(): void\n {\n this.paused = false;\n }\n}\n","// eslint-disable-next-line\n// @ts-expect-error Penner seems to have no typings.\nimport Penner from 'penner';\n\n/**\n * Returns correct Penner equation using string or Function.\n *\n * @internal\n * @ignore\n * @param {(function|string)} [ease]\n * @param {defaults} default penner equation to use if none is provided\n */\nexport default function ease(ease: any, defaults?: any): any\n{\n if (!ease)\n {\n return Penner[defaults]\n }\n else if (typeof ease === 'function')\n {\n return ease\n }\n else if (typeof ease === 'string')\n {\n return Penner[ease]\n }\n}","import { Point } from '@pixi/math';\nimport { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Animate}. */\nexport interface IAnimateOptions {\n /** Time to animate */\n time?: number;\n\n /** Position to move the viewport to */\n position?: Point;\n\n /**\n * Desired viewport width in world pixels\n *\n * (use instead of scale; aspect ratio is maintained if height is not provided)\n */\n width?: number;\n\n /**\n * Desired viewport height in world pixels\n *\n * (use instead of scale; aspect ratio is maintained if width is not provided)\n */\n height?: number;\n\n /** Scale to change zoom (scale.x = scale.y) */\n scale?: number;\n\n /** Independently change zoom in x-direction */\n scaleX?: number;\n\n /** Independently change zoom in y-direction */\n scaleY?: number;\n\n /** Easing function to use */\n ease?: any;\n\n /** Callback to invoke when the animation completes */\n callbackOnComplete?: (viewport: Viewport) => void;\n\n /** Removes this plugin if interrupted by any user input */\n removeOnInterrupt?: boolean;\n}\n\nconst DEFAULT_ANIMATE_OPTIONS = {\n removeOnInterrupt: false,\n ease: 'linear',\n time: 1000,\n};\n\n/**\n * Animation plugin.\n *\n * @see Viewport#animate\n * @fires animate-end\n */\nexport class Animate extends Plugin\n{\n public readonly options: IAnimateOptions & { ease: any; time: number };\n\n /** The starting x-coordinate of the viewport. */\n protected startX?: number;\n\n /** The starting y-coordinate of the viewport. */\n protected startY?: number;\n\n /** The change in the x-coordinate of the viewport through the animation.*/\n protected deltaX?: number;\n\n /** The change in the y-coordinate of the viewport through the animation. */\n protected deltaY?: number;\n\n /** Marks whether the center of the viewport is preserved in the animation. */\n protected keepCenter!: boolean;\n\n /** The starting viewport width. */\n protected startWidth: number | null = null;\n\n /** The starting viewport height. */\n protected startHeight: number | null = null;\n\n /** The change in the viewport's width through the animation. */\n protected deltaWidth: number | null = null;\n\n /** The change in the viewport's height through the animation. */\n protected deltaHeight: number | null = null;\n\n /** The viewport's width post-animation. */\n protected width: number | null = null;\n\n /** The viewport's height post-animation. */\n protected height: number | null = null;\n\n /** The time since the animation started. */\n protected time = 0;\n\n /**\n * This is called by {@link Viewport.animate}.\n *\n * @param parent\n * @param options\n */\n constructor(parent: Viewport, options: IAnimateOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_ANIMATE_OPTIONS, options);\n this.options.ease = ease(this.options.ease);\n\n this.setupPosition();\n this.setupZoom();\n\n this.time = 0;\n }\n\n /**\n * Setup `startX`, `startY`, `deltaX`, `deltaY`, `keepCenter`.\n *\n * This is called during construction.\n */\n protected setupPosition(): void\n {\n if (typeof this.options.position !== 'undefined')\n {\n this.startX = this.parent.center.x;\n this.startY = this.parent.center.y;\n this.deltaX = this.options.position.x - this.parent.center.x;\n this.deltaY = this.options.position.y - this.parent.center.y;\n this.keepCenter = false;\n }\n else\n {\n this.keepCenter = true;\n }\n }\n\n /**\n * Setup `startWidth, `startHeight`, `deltaWidth, `deltaHeight, `width`, `height`.\n *\n * This is called during construction.\n */\n protected setupZoom(): void\n {\n this.width = null;\n this.height = null;\n\n if (typeof this.options.scale !== 'undefined')\n {\n this.width = this.parent.screenWidth / this.options.scale;\n }\n else if (typeof this.options.scaleX !== 'undefined' || typeof this.options.scaleY !== 'undefined')\n {\n if (typeof this.options.scaleX !== 'undefined')\n {\n // screenSizeInWorldPixels = screenWidth / scale\n this.width = this.parent.screenWidth / this.options.scaleX;\n }\n if (typeof this.options.scaleY !== 'undefined')\n {\n this.height = this.parent.screenHeight / this.options.scaleY;\n }\n }\n else\n {\n if (typeof this.options.width !== 'undefined')\n {\n this.width = this.options.width;\n }\n if (typeof this.options.height !== 'undefined')\n {\n this.height = this.options.height;\n }\n }\n\n if (this.width !== null)\n {\n this.startWidth = this.parent.screenWidthInWorldPixels;\n this.deltaWidth = this.width - this.startWidth;\n }\n if (this.height !== null)\n {\n this.startHeight = this.parent.screenHeightInWorldPixels;\n this.deltaHeight = this.height - this.startHeight;\n }\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('animate');\n }\n\n return false;\n }\n\n public complete(): void\n {\n this.parent.plugins.remove('animate');\n if (this.width !== null)\n {\n this.parent.fitWidth(this.width, this.keepCenter, this.height === null);\n }\n if (this.height !== null)\n {\n this.parent.fitHeight(this.height, this.keepCenter, this.width === null);\n }\n if (!this.keepCenter)\n {\n this.parent.moveCenter(this.options.position as Point);\n }\n\n this.parent.emit('animate-end', this.parent);\n\n if (this.options.callbackOnComplete)\n {\n this.options.callbackOnComplete(this.parent);\n }\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n this.time += elapsed;\n\n if (this.time >= this.options.time)\n {\n this.complete();\n }\n else\n {\n const originalZoom = new Point(this.parent.scale.x, this.parent.scale.y);\n const percent = this.options.ease(this.time, 0, 1, this.options.time);\n\n if (this.width !== null)\n {\n const startWidth = this.startWidth as number;\n const deltaWidth = this.deltaWidth as number;\n\n this.parent.fitWidth(\n startWidth + (deltaWidth * percent),\n this.keepCenter,\n this.height === null);\n }\n if (this.height !== null)\n {\n const startHeight = this.startHeight as number;\n const deltaHeight = this.deltaHeight as number;\n\n this.parent.fitHeight(\n startHeight + (deltaHeight * percent),\n this.keepCenter,\n this.width === null);\n }\n if (this.width === null)\n {\n this.parent.scale.x = this.parent.scale.y;\n }\n else if (this.height === null)\n {\n this.parent.scale.y = this.parent.scale.x;\n }\n if (!this.keepCenter)\n {\n const startX = this.startX as number;\n const startY = this.startY as number;\n const deltaX = this.deltaX as number;\n const deltaY = this.deltaY as number;\n const original = new Point(this.parent.x, this.parent.y);\n\n this.parent.moveCenter(startX + (deltaX * percent), startY + (deltaY * percent));\n this.parent.emit('moved', { viewport: this.parent, original, type: 'animate' });\n }\n if (this.width || this.height)\n {\n this.parent.emit('zoomed', { viewport: this.parent, original: originalZoom, type: 'animate' });\n }\n }\n }\n}\n","import { Point, Rectangle } from '@pixi/math';\nimport { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Drag } from './Drag';\nimport type { IDecelerateOptions } from './Decelerate';\nimport type { Pinch } from './Pinch';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Bounce}. */\nexport interface IBounceOptions {\n /** \"all\", \"horizontal\", \"vertical\", or combination of \"top\", \"bottom\", \"right\", \"left\" (e.g., 'top-bottom-right') */\n sides?:\n 'all'\n | 'horizontal'\n | 'vertical'\n | string;\n\n /** Friction to apply to decelerate if active */\n friction?: number;\n\n /** Time in ms to finish bounce */\n time?: number;\n\n /** Use this bounceBox instead of (0, 0, viewport.worldWidth, viewport.worldHeight) */\n bounceBox?: Rectangle | null;\n\n /** Ease function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /** (top/bottom/center and left/right/center, or center) where to place world if too small for screen */\n underflow?: 'center' | string;\n}\n\n/** Bounce state along an axis */\nexport interface IBounceState {\n /** Elapsed time since bounce started */\n time: number;\n\n /** Starting coordinate */\n start: number;\n\n /** Change in coordinate through bounce */\n delta: number;\n\n /** Ending coordinate */\n end: number;\n}\n\nconst DEFAULT_BOUNCE_OPTIONS: Required = {\n sides: 'all',\n friction: 0.5,\n time: 150,\n ease: 'easeInOutSine',\n underflow: 'center',\n bounceBox: null\n};\n\n/**\n * @fires bounce-start-x\n * @fires bounce.end-x\n * @fires bounce-start-y\n * @fires bounce-end-y\n * @public\n */\nexport class Bounce extends Plugin\n{\n /** The options passed to initialize this plugin, cannot be modified again. */\n public readonly options: Readonly>;\n\n /** Holds whether to bounce from left side. */\n public readonly left: boolean ;\n\n /** Holds whether to bounce from top side. */\n public readonly top: boolean;\n\n /** Holds whether to bounce from right side. */\n public readonly right: boolean;\n\n /** Holds whether to bounce from bottom side. */\n public readonly bottom: boolean;\n\n /** Direction of underflow along x-axis. */\n public readonly underflowX: -1 | 0 | 1;\n\n /** Direction of underflow along y-axis. */\n public readonly underflowY: -1 | 0 | 1;\n\n /** Easing */\n protected ease: any;\n\n /** Bounce state along x-axis */\n protected toX!: IBounceState | null;\n\n /** Bounce state along y-axis */\n protected toY!: IBounceState | null;\n\n /**\n * This is called by {@link Viewport.bounce}.\n */\n constructor(parent: Viewport, options: IBounceOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_BOUNCE_OPTIONS, options);\n this.ease = ease(this.options.ease, 'easeInOutSine');\n\n if (this.options.sides)\n {\n if (this.options.sides === 'all')\n {\n this.top = this.bottom = this.left = this.right = true;\n }\n else if (this.options.sides === 'horizontal')\n {\n this.right = this.left = true;\n this.top = this.bottom = false;\n }\n else if (this.options.sides === 'vertical')\n {\n this.left = this.right = false;\n this.top = this.bottom = true;\n }\n else\n {\n this.top = this.options.sides.indexOf('top') !== -1;\n this.bottom = this.options.sides.indexOf('bottom') !== -1;\n this.left = this.options.sides.indexOf('left') !== -1;\n this.right = this.options.sides.indexOf('right') !== -1;\n }\n } else {\n this.left = this.top = this.right = this.bottom = false;\n }\n\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'center')\n {\n this.underflowX = 0;\n this.underflowY = 0;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n }\n\n this.reset();\n }\n\n public isActive(): boolean\n {\n return this.toX !== null || this.toY !== null;\n }\n\n public down(): boolean\n {\n this.toX = this.toY = null;\n\n return false;\n }\n\n public up(): boolean\n {\n this.bounce();\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n this.bounce();\n\n if (this.toX)\n {\n const toX = this.toX;\n\n toX.time += elapsed;\n this.parent.emit('moved', { viewport: this.parent, type: 'bounce-x' });\n\n if (toX.time >= this.options.time)\n {\n this.parent.x = toX.end;\n this.toX = null;\n this.parent.emit('bounce-x-end', this.parent);\n }\n else\n {\n this.parent.x = this.ease(toX.time, toX.start, toX.delta, this.options.time);\n }\n }\n\n if (this.toY)\n {\n const toY = this.toY;\n\n toY.time += elapsed;\n this.parent.emit('moved', { viewport: this.parent, type: 'bounce-y' });\n\n if (toY.time >= this.options.time)\n {\n this.parent.y = toY.end;\n this.toY = null;\n this.parent.emit('bounce-y-end', this.parent);\n }\n else\n {\n this.parent.y = this.ease(toY.time, toY.start, toY.delta, this.options.time);\n }\n }\n }\n\n /** @internal */\n protected calcUnderflowX(): number\n {\n let x: number;\n\n switch (this.underflowX)\n {\n case -1:\n x = 0;\n break;\n case 1:\n x = (this.parent.screenWidth - this.parent.screenWorldWidth);\n break;\n default:\n x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n }\n\n return x;\n }\n\n /** @internal */\n protected calcUnderflowY(): number\n {\n let y: number;\n\n switch (this.underflowY)\n {\n case -1:\n y = 0;\n break;\n case 1:\n y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n break;\n default:\n y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n }\n\n return y;\n }\n\n private oob(): Record<'left' | 'right' | 'top' | 'bottom', boolean> & Record<'topLeft' | 'bottomRight', Point>\n {\n const box = this.options.bounceBox;\n\n if (box)\n {\n const x1 = typeof box.x === 'undefined' ? 0 : box.x;\n const y1 = typeof box.y === 'undefined' ? 0 : box.y;\n const width = typeof box.width === 'undefined' ? this.parent.worldWidth : box.width;\n const height = typeof box.height === 'undefined' ? this.parent.worldHeight : box.height;\n\n return {\n left: this.parent.left < x1,\n right: this.parent.right > width,\n top: this.parent.top < y1,\n bottom: this.parent.bottom > height,\n topLeft: new Point(\n x1 * this.parent.scale.x,\n y1 * this.parent.scale.y\n ),\n bottomRight: new Point(\n width * this.parent.scale.x - this.parent.screenWidth,\n height * this.parent.scale.y - this.parent.screenHeight\n )\n };\n }\n\n return {\n left: this.parent.left < 0,\n right: this.parent.right > this.parent.worldWidth,\n top: this.parent.top < 0,\n bottom: this.parent.bottom > this.parent.worldHeight,\n topLeft: new Point(0, 0),\n bottomRight: new Point(\n this.parent.worldWidth * this.parent.scale.x - this.parent.screenWidth,\n this.parent.worldHeight * this.parent.scale.y - this.parent.screenHeight\n )\n };\n }\n\n public bounce(): void\n {\n if (this.paused)\n {\n return;\n }\n\n let oob;\n let decelerate: undefined | null | {\n percentChangeX?: number;\n percentChangeY?: number;\n x?: number;\n y?: number;\n options?: IDecelerateOptions\n } = this.parent.plugins.get('decelerate', true) as any;\n\n if (decelerate && (decelerate.x || decelerate.y))\n {\n if ((decelerate.x && decelerate.percentChangeX === decelerate.options?.friction) || (decelerate.y && decelerate.percentChangeY === decelerate.options?.friction))\n {\n oob = this.oob();\n if ((oob.left && this.left) || (oob.right && this.right))\n {\n decelerate.percentChangeX = this.options.friction;\n }\n if ((oob.top && this.top) || (oob.bottom && this.bottom))\n {\n decelerate.percentChangeY = this.options.friction;\n }\n }\n }\n const drag: Partial = this.parent.plugins.get('drag', true) || {};\n const pinch: Partial = this.parent.plugins.get('pinch', true) || {};\n\n decelerate = decelerate || {};\n\n if (!drag?.active && !pinch?.active && ((!this.toX || !this.toY) && (!decelerate.x || !decelerate.y)))\n {\n oob = oob || this.oob();\n const topLeft = oob.topLeft;\n const bottomRight = oob.bottomRight;\n\n if (!this.toX && !decelerate.x)\n {\n let x = null;\n\n if (oob.left && this.left)\n {\n x = (this.parent.screenWorldWidth < this.parent.screenWidth) ? this.calcUnderflowX() : -topLeft.x;\n }\n else if (oob.right && this.right)\n {\n x = (this.parent.screenWorldWidth < this.parent.screenWidth) ? this.calcUnderflowX() : -bottomRight.x;\n }\n if (x !== null && this.parent.x !== x)\n {\n this.toX = { time: 0, start: this.parent.x, delta: x - this.parent.x, end: x };\n this.parent.emit('bounce-x-start', this.parent);\n }\n }\n if (!this.toY && !decelerate.y)\n {\n let y = null;\n\n if (oob.top && this.top)\n {\n y = (this.parent.screenWorldHeight < this.parent.screenHeight) ? this.calcUnderflowY() : -topLeft.y;\n }\n else if (oob.bottom && this.bottom)\n {\n y = (this.parent.screenWorldHeight < this.parent.screenHeight) ? this.calcUnderflowY() : -bottomRight.y;\n }\n if (y !== null && this.parent.y !== y)\n {\n this.toY = { time: 0, start: this.parent.y, delta: y - this.parent.y, end: y };\n this.parent.emit('bounce-y-start', this.parent);\n }\n }\n }\n }\n\n public reset(): void\n {\n this.toX = this.toY = null;\n this.bounce();\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\n\n/**\n * There are three ways to clamp:\n * 1. direction: 'all' = the world is clamped to its world boundaries, ie, you cannot drag any part of the world offscreen\n * direction: 'x' | 'y' = only the x or y direction is clamped to its world boundary\n * 2. left, right, top, bottom = true | number = the world is clamped to the world's pixel location for each side;\n * if any of these are set to true, then the location is set to the boundary [0, viewport.worldWidth/viewport.worldHeight]\n * eg: to allow the world to be completely dragged offscreen, set [-viewport.worldWidth, -viewport.worldHeight, viewport.worldWidth * 2, viewport.worldHeight * 2]\n *\n * Underflow determines what happens when the world is smaller than the viewport\n * 1. none = the world is clamped but there is no special behavior\n * 2. center = the world is centered on the viewport\n * 3. combination of top/bottom/center and left/right/center (case insensitive) = the world is stuck to the appropriate boundaries\n *\n */\nexport interface IClampOptions\n{\n /**\n * Clamp left; true = 0\n *\n * @default false\n */\n left?: number | boolean | null;\n\n /**\n * Clamp top; true = 0\n *\n * @default false\n */\n top?: number | boolean | null;\n\n /**\n * Clamp right; true = viewport.worldWidth\n *\n * @default false\n */\n right?: number | boolean | null;\n\n /**\n * Clamp bottom; true = viewport.worldHeight\n *\n * @default false\n */\n bottom?: number | boolean | null;\n\n /**\n * (all, x, or y) using clamps of [0, viewport.worldWidth/viewport.worldHeight]; replaces left/right/top/bottom if set\n *\n * @default null\n */\n direction?: 'all' | 'x' | 'y' | null;\n\n /**\n * Where to place world if too small for screen (e.g., top-right, center, none, bottomleft)\n *\n * @default 'center'\n */\n underflow?: 'center' | string;\n}\n\nconst DEFAULT_CLAMP_OPTIONS: Required = {\n left: false,\n right: false,\n top: false,\n bottom: false,\n direction: null,\n underflow: 'center'\n};\n\n/**\n * Plugin to clamp the viewport to a specific world bounding box.\n *\n * @public\n */\nexport class Clamp extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Required;\n\n /** Last state of viewport */\n protected last: {\n x: number | null;\n y: number | null;\n scaleX: number | null;\n scaleY: number | null;\n };\n\n protected noUnderflow!: boolean;\n protected underflowX!: -1 | 0 | 1;\n protected underflowY!: -1 | 0 | 1;\n\n /**\n * This is called by {@link Viewport.clamp}.\n */\n constructor(parent: Viewport, options : IClampOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_CLAMP_OPTIONS, options);\n\n if (this.options.direction)\n {\n this.options.left = this.options.direction === 'x' || this.options.direction === 'all' ? true : null;\n this.options.right = this.options.direction === 'x' || this.options.direction === 'all' ? true : null;\n this.options.top = this.options.direction === 'y' || this.options.direction === 'all' ? true : null;\n this.options.bottom = this.options.direction === 'y' || this.options.direction === 'all' ? true : null;\n }\n\n this.parseUnderflow();\n this.last = { x: null, y: null, scaleX: null, scaleY: null };\n this.update();\n }\n\n private parseUnderflow()\n {\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'none')\n {\n this.noUnderflow = true;\n }\n else if (clamp === 'center')\n {\n this.underflowX = this.underflowY = 0;\n this.noUnderflow = false;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n this.noUnderflow = false;\n }\n }\n\n public move(): boolean\n {\n this.update();\n\n return false;\n }\n\n public update(): void\n {\n if (this.paused)\n {\n return;\n }\n\n // only clamp on change\n if (this.parent.x === this.last.x\n && this.parent.y === this.last.y\n && this.parent.scale.x === this.last.scaleX\n && this.parent.scale.y === this.last.scaleY)\n {\n return;\n }\n const original = { x: this.parent.x, y: this.parent.y };\n // TODO: Fix\n const decelerate: any = (this.parent.plugins as any).decelerate || {};\n\n if (this.options.left !== null || this.options.right !== null)\n {\n let moved = false;\n\n if (!this.noUnderflow && this.parent.screenWorldWidth < this.parent.screenWidth)\n {\n switch (this.underflowX)\n {\n case -1:\n if (this.parent.x !== 0)\n {\n this.parent.x = 0;\n moved = true;\n }\n break;\n case 1:\n if (this.parent.x !== this.parent.screenWidth - this.parent.screenWorldWidth)\n {\n this.parent.x = this.parent.screenWidth - this.parent.screenWorldWidth;\n moved = true;\n }\n break;\n default:\n if (this.parent.x !== (this.parent.screenWidth - this.parent.screenWorldWidth) / 2)\n {\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n moved = true;\n }\n }\n }\n else\n {\n if (this.options.left !== null)\n {\n if (this.parent.left < (this.options.left === true ? 0 : this.options.left))\n {\n this.parent.x = -(this.options.left === true ? 0 : this.options.left) * this.parent.scale.x;\n decelerate.x = 0;\n moved = true;\n }\n }\n if (this.options.right !== null)\n {\n if (this.parent.right > (this.options.right === true ? this.parent.worldWidth : this.options.right))\n {\n this.parent.x = -(this.options.right === true ? this.parent.worldWidth : this.options.right) * this.parent.scale.x + this.parent.screenWidth;\n decelerate.x = 0;\n moved = true;\n }\n }\n }\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, original, type: 'clamp-x' });\n }\n }\n if (this.options.top !== null || this.options.bottom !== null)\n {\n let moved = false;\n\n if (!this.noUnderflow && this.parent.screenWorldHeight < this.parent.screenHeight)\n {\n switch (this.underflowY)\n {\n case -1:\n if (this.parent.y !== 0)\n {\n this.parent.y = 0;\n moved = true;\n }\n break;\n case 1:\n if (this.parent.y !== this.parent.screenHeight - this.parent.screenWorldHeight)\n {\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n moved = true;\n }\n break;\n default:\n if (this.parent.y !== (this.parent.screenHeight - this.parent.screenWorldHeight) / 2)\n {\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n moved = true;\n }\n }\n }\n else\n {\n if (this.options.top !== null)\n {\n if (this.parent.top < (this.options.top === true ? 0 : this.options.top))\n {\n this.parent.y = -(this.options.top === true ? 0 : this.options.top)\n * this.parent.scale.y;\n decelerate.y = 0;\n moved = true;\n }\n }\n if (this.options.bottom !== null)\n {\n if (this.parent.bottom > (this.options.bottom === true ? this.parent.worldHeight : this.options.bottom))\n {\n this.parent.y = -(this.options.bottom === true ? this.parent.worldHeight : this.options.bottom)\n * this.parent.scale.y + this.parent.screenHeight;\n decelerate.y = 0;\n moved = true;\n }\n }\n }\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, original, type: 'clamp-y' });\n }\n }\n this.last.x = this.parent.x;\n this.last.y = this.parent.y;\n this.last.scaleX = this.parent.scale.x;\n this.last.scaleY = this.parent.scale.y;\n }\n\n public reset(): void\n {\n this.update();\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\n\n/**\n * Options for {@link ClampZoom}.\n *\n * Use either minimum width/height or minimum scale\n */\nexport interface IClampZoomOptions\n{\n /** Minimum width */\n minWidth?: number | null;\n\n /** Minimum height */\n minHeight?: number | null;\n\n /** Maximum width */\n maxWidth?: number | null;\n\n /** Maximum height */\n maxHeight?: number | null;\n\n /** Minimum scale */\n minScale?: number | null;\n\n /** Maximum scale */\n maxScale?: number | null;\n}\n\nconst DEFAULT_CLAMP_ZOOM_OPTIONS: Required = {\n minWidth: null,\n minHeight: null,\n maxWidth: null,\n maxHeight: null,\n minScale: null,\n maxScale: null\n};\n\n/**\n * Plugin to clamp the viewport's zoom to a specific range.\n *\n * @public\n */\nexport class ClampZoom extends Plugin\n{\n public readonly options: Required;\n\n /**\n * This is called by {@link Viewport.clampZoom}.\n */\n constructor(parent: Viewport, options = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_CLAMP_ZOOM_OPTIONS, options);\n\n this.clamp();\n }\n\n public resize(): void\n {\n this.clamp();\n }\n\n /** Clamp the viewport's zoom immediately. */\n public clamp(): void\n {\n if (this.paused)\n {\n return;\n }\n\n if (this.options.minWidth || this.options.minHeight || this.options.maxWidth || this.options.maxHeight)\n {\n let width = this.parent.worldScreenWidth;\n let height = this.parent.worldScreenHeight;\n\n if (this.options.minWidth !== null && width < this.options.minWidth)\n {\n const original = this.parent.scale.x;\n\n this.parent.fitWidth(this.options.minWidth, false, false, true);\n this.parent.scale.y *= this.parent.scale.x / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.maxWidth !== null && width > this.options.maxWidth)\n {\n const original = this.parent.scale.x;\n\n this.parent.fitWidth(this.options.maxWidth, false, false, true);\n this.parent.scale.y *= this.parent.scale.x / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.minHeight !== null && height < this.options.minHeight)\n {\n const original = this.parent.scale.y;\n\n this.parent.fitHeight(this.options.minHeight, false, false, true);\n this.parent.scale.x *= this.parent.scale.y / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.maxHeight !== null && height > this.options.maxHeight)\n {\n const original = this.parent.scale.y;\n\n this.parent.fitHeight(this.options.maxHeight, false, false, true);\n this.parent.scale.x *= this.parent.scale.y / original;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n }\n else\n {\n let scale = this.parent.scale.x;\n\n if (this.options.minScale !== null && scale < this.options.minScale)\n {\n scale = this.options.minScale;\n }\n if (this.options.maxScale !== null && scale > this.options.maxScale)\n {\n scale = this.options.maxScale;\n }\n if (scale !== this.parent.scale.x)\n {\n this.parent.scale.set(scale);\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n }\n }\n\n public reset(): void\n {\n this.clamp();\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Point } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\nexport interface IDecelerateOptions {\n /**\n * Percent to decelerate after movement. This should be between 0 and 1, exclusive.\n *\n * @default 0.95\n */\n friction?: number;\n\n /**\n * Percent to decelerate when past boundaries (only applicable when viewport.bounce() is active)\n *\n * @default 0.8\n */\n bounce?: number;\n\n /**\n * Minimum velocity before stopping/reversing acceleration\n *\n * @default 0.01\n */\n minSpeed?: number;\n}\n\n/** Viewport position snapshot that's saved by {@link DeceleratePlugin} to estimate panning velocity. */\nexport interface IDecelerateSnapshot {\n /** x-coordinate of the viewport. */\n x: number;\n\n /** y-coordinate of the viewport. */\n y: number;\n\n /** Time at which this snapshot was taken. */\n time: number;\n}\n\nconst DEFAULT_DECELERATE_OPTIONS: Required = {\n friction: 0.98,\n bounce: 0.8,\n minSpeed: 0.01\n};\n\n/**\n * Time period of decay (1 frame)\n *\n * @internal\n * @ignore\n */\nconst TP = 16;\n\n/**\n * Plugin to decelerate viewport velocity smoothly after panning ends.\n *\n * @public\n */\nexport class Decelerate extends Plugin\n{\n /** Options used to initialize this plugin. */\n public readonly options: Required;\n\n /**\n * x-component of the velocity of viewport provided by this plugin, at the current time.\n *\n * This is measured in px/frame, where a frame is normalized to 16 milliseconds.\n */\n public x!: number | null;\n\n /**\n * y-component of the velocity of the viewport provided by this plugin, at the current time.\n *\n * This is measured in px/frame, where a frame is normalized to 16 milliseconds.\n */\n public y!: number | null;\n\n /**\n * The decay factor for the x-component of the viewport.\n *\n * The viewport's velocity decreased by this amount each 16 milliseconds.\n */\n public percentChangeX!: number;\n\n /**\n * The decay factor for the y-component of the viewport.\n *\n * The viewport's velocity decreased by this amount each 16 milliseconds.\n */\n public percentChangeY!: number;\n\n /** Saved list of recent viewport position snapshots, to estimate velocity. */\n protected saved: Array;\n\n /** The time since the user released panning of the viewport. */\n protected timeSinceRelease: number;\n\n /**\n * This is called by {@link Viewport.decelerate}.\n */\n constructor(parent: Viewport, options: IDecelerateOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_DECELERATE_OPTIONS, options);\n this.saved = [];\n this.timeSinceRelease = 0;\n\n this.reset();\n this.parent.on('moved', (data) => this.moved(data));\n }\n\n public down(): boolean\n {\n this.saved = [];\n this.x = this.y = null;\n\n return false;\n }\n\n public isActive(): boolean\n {\n return !!(this.x || this.y);\n }\n\n public move(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n const count = this.parent.input.count();\n\n if (count === 1 || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n this.saved.push({ x: this.parent.x, y: this.parent.y, time: performance.now() });\n\n if (this.saved.length > 60)\n {\n this.saved.splice(0, 30);\n }\n }\n\n // Silently recording viewport positions\n return false;\n }\n\n /** Listener to viewport's \"moved\" event. */\n protected moved(data: { type: 'clamp-x' | 'clamp-y'; original: Point }): void\n {\n if (this.saved.length)\n {\n const last = this.saved[this.saved.length - 1];\n\n if (data.type === 'clamp-x')\n {\n if (last.x === data.original.x)\n {\n last.x = this.parent.x;\n }\n }\n else if (data.type === 'clamp-y')\n {\n if (last.y === data.original.y)\n {\n last.y = this.parent.y;\n }\n }\n }\n }\n\n public up(): boolean\n {\n if (this.parent.input.count() === 0 && this.saved.length)\n {\n const now = performance.now();\n\n for (const save of this.saved)\n {\n if (save.time >= now - 100)\n {\n const time = now - save.time;\n\n this.x = (this.parent.x - save.x) / time;\n this.y = (this.parent.y - save.y) / time;\n this.percentChangeX = this.percentChangeY = this.options.friction;\n this.timeSinceRelease = 0;\n break;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Manually activate deceleration, starting from the (x, y) velocity components passed in the options.\n *\n * @param {object} options\n * @param {number} [options.x] - Specify x-component of initial velocity.\n * @param {number} [options.y] - Specify y-component of initial velocity.\n */\n public activate(options: { x?: number; y?: number; }): void\n {\n options = options || {};\n\n if (typeof options.x !== 'undefined')\n {\n this.x = options.x;\n this.percentChangeX = this.options.friction;\n }\n if (typeof options.y !== 'undefined')\n {\n this.y = options.y;\n this.percentChangeY = this.options.friction;\n }\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n /*\n * See https://github.com/davidfig/pixi-viewport/issues/271 for math.\n *\n * The viewport velocity (this.x, this.y) decays exponentially by the the decay factor\n * (this.percentChangeX, this.percentChangeY) each frame. This velocity function is integrated\n * to calculate the displacement.\n */\n\n const moved = this.x || this.y;\n\n const ti = this.timeSinceRelease;\n const tf = this.timeSinceRelease + elapsed;\n\n if (this.x)\n {\n const k = this.percentChangeX;\n const lnk = Math.log(k);\n\n // Apply velocity delta on the viewport x-coordinate.\n this.parent.x += ((this.x * TP) / lnk) * (Math.pow(k, tf / TP) - Math.pow(k, ti / TP));\n\n // Apply decay on x-component of velocity\n this.x *= Math.pow(this.percentChangeX, elapsed / TP);\n }\n if (this.y)\n {\n const k = this.percentChangeY;\n const lnk = Math.log(k);\n\n // Apply velocity delta on the viewport y-coordinate.\n this.parent.y += ((this.y * TP) / lnk) * (Math.pow(k, tf / TP) - Math.pow(k, ti / TP));\n\n // Apply decay on y-component of velocity\n this.y *= Math.pow(this.percentChangeY, elapsed / TP);\n }\n\n this.timeSinceRelease += elapsed;\n\n // End decelerate velocity once it goes under a certain amount of precision.\n if (Math.abs(this.x || 0) < this.options.minSpeed)\n {\n this.x = 0;\n }\n if (Math.abs(this.y || 0) < this.options.minSpeed)\n {\n this.y = 0;\n }\n\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, type: 'decelerate' });\n }\n }\n\n public reset(): void\n {\n this.x = this.y = null;\n }\n}\n","import { Point } from '@pixi/math';\nimport { Plugin } from './Plugin';\n\nimport type { Decelerate } from './Decelerate';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { IPointData } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Drag}. */\nexport interface IDragOptions {\n /**\n * direction to drag\n *\n * @default \"all\"\n */\n direction?: string;\n\n /**\n * whether click to drag is active\n *\n * @default true\n */\n pressDrag?: boolean;\n\n /**\n * Use wheel to scroll in direction (unless wheel plugin is active)\n *\n * @default true\n */\n wheel?: boolean;\n\n /**\n * number of pixels to scroll with each wheel spin\n *\n * @default 1\n */\n wheelScroll?: number;\n\n /**\n * reverse the direction of the wheel scroll\n *\n * @default false\n */\n reverse?: boolean;\n\n /**\n * clamp wheel(to avoid weird bounce with mouse wheel). Can be 'x' or 'y' or `true`.\n *\n * @default false\n */\n clampWheel?: boolean | string;\n\n /**\n * where to place world if too small for screen\n *\n * @default \"center\"\n */\n underflow?: string;\n\n /**\n * factor to multiply drag to increase the speed of movement\n *\n * @default 1\n */\n factor?: number;\n\n /**\n * Changes which mouse buttons trigger drag.\n *\n * Use: 'all', 'left', right' 'middle', or some combination, like, 'middle-right'; you may want to set\n * `viewport.options.disableOnContextMenu` if you want to use right-click dragging.\n *\n * @default \"all\"\n */\n mouseButtons?: 'all' | string;\n\n /**\n * Array containing {@link key|https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code} codes of\n * keys that can be pressed for the drag to be triggered, e.g.: ['ShiftLeft', 'ShiftRight'}.\n *\n * @default null\n */\n keyToPress?: string[] | null;\n\n /**\n * Ignore keyToPress for touch events.\n *\n * @default false\n */\n ignoreKeyToPressOnTouch?: boolean;\n\n /**\n * Scaling factor for non-DOM_DELTA_PIXEL scrolling events.\n *\n * @default 20\n */\n lineHeight?: number;\n}\n\nconst DEFAULT_DRAG_OPTIONS: Required = {\n direction: 'all',\n pressDrag: true,\n wheel: true,\n wheelScroll: 1,\n reverse: false,\n clampWheel: false,\n underflow: 'center',\n factor: 1,\n mouseButtons: 'all',\n keyToPress: null,\n ignoreKeyToPressOnTouch: false,\n lineHeight: 20,\n};\n\n/**\n * Plugin to enable panning/dragging of the viewport to move around.\n *\n * @public\n */\nexport class Drag extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Readonly>;\n\n /** Flags when viewport is moving. */\n protected moved: boolean;\n\n /** Factor to apply from {@link IDecelerateOptions}'s reverse. */\n protected reverse: 1 | -1;\n\n /** Holds whether dragging is enabled along the x-axis. */\n protected xDirection: boolean\n\n /** Holds whether dragging is enabled along the y-axis. */\n protected yDirection: boolean;\n\n /** Flags whether the keys required to drag are pressed currently. */\n protected keyIsPressed: boolean;\n\n /** Holds whether the left, center, and right buttons are required to pan. */\n protected mouse!: [boolean, boolean, boolean]\n\n /** Underflow factor along x-axis */\n protected underflowX!: -1 | 0 | 1;\n\n /** Underflow factor along y-axis */\n protected underflowY!: -1 | 0 | 1;\n\n /** Last pointer position while panning. */\n protected last?: IPointData | null\n\n /** The ID of the pointer currently panning the viewport. */\n protected current?: number;\n\n /**\n * This is called by {@link Viewport.drag}.\n */\n constructor(parent: Viewport, options = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_DRAG_OPTIONS, options);\n this.moved = false;\n this.reverse = this.options.reverse ? 1 : -1;\n this.xDirection = !this.options.direction || this.options.direction === 'all' || this.options.direction === 'x';\n this.yDirection = !this.options.direction || this.options.direction === 'all' || this.options.direction === 'y';\n this.keyIsPressed = false;\n\n this.parseUnderflow();\n this.mouseButtons(this.options.mouseButtons);\n\n if (this.options.keyToPress)\n {\n this.handleKeyPresses(this.options.keyToPress);\n }\n }\n\n /**\n * Handles keypress events and set the keyIsPressed boolean accordingly\n *\n * @param {array} codes - key codes that can be used to trigger drag event\n */\n protected handleKeyPresses(codes: string[]): void\n {\n window.addEventListener('keydown', (e) =>\n {\n if (codes.includes(e.code))\n { this.keyIsPressed = true; }\n });\n\n window.addEventListener('keyup', (e) =>\n {\n if (codes.includes(e.code))\n { this.keyIsPressed = false; }\n });\n }\n\n /**\n * initialize mousebuttons array\n * @param {string} buttons\n */\n protected mouseButtons(buttons: string): void\n {\n if (!buttons || buttons === 'all')\n {\n this.mouse = [true, true, true];\n }\n else\n {\n this.mouse = [\n buttons.indexOf('left') !== -1,\n buttons.indexOf('middle') !== -1,\n buttons.indexOf('right') !== -1\n ];\n }\n }\n\n protected parseUnderflow(): void\n {\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'center')\n {\n this.underflowX = 0;\n this.underflowY = 0;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n }\n }\n\n /**\n * @param {PIXI.InteractionEvent} event\n * @returns {boolean}\n */\n protected checkButtons(event: InteractionEvent): boolean\n {\n const isMouse = event.data.pointerType === 'mouse';\n const count = this.parent.input.count();\n\n if ((count === 1) || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n if (!isMouse || this.mouse[event.data.button])\n {\n return true;\n }\n }\n\n return false;\n }\n\n /**\n * @param {PIXI.InteractionEvent} event\n * @returns {boolean}\n */\n protected checkKeyPress(event: InteractionEvent): boolean\n {\n return (!this.options.keyToPress\n || this.keyIsPressed\n || (this.options.ignoreKeyToPressOnTouch && event.data.pointerType === 'touch'));\n }\n\n public down(event: InteractionEvent): boolean\n {\n if (this.paused || !this.options.pressDrag)\n {\n return false;\n }\n if (this.checkButtons(event) && this.checkKeyPress(event))\n {\n this.last = { x: event.data.global.x, y: event.data.global.y };\n this.current = event.data.pointerId;\n\n return true;\n }\n this.last = null;\n\n return false;\n }\n\n get active(): boolean\n {\n return this.moved;\n }\n\n public move(event: InteractionEvent): boolean\n {\n if (this.paused || !this.options.pressDrag)\n {\n return false;\n }\n if (this.last && this.current === event.data.pointerId)\n {\n const x = event.data.global.x;\n const y = event.data.global.y;\n const count = this.parent.input.count();\n\n if (count === 1 || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n const distX = x - this.last.x;\n const distY = y - this.last.y;\n\n if (this.moved\n || ((this.xDirection && this.parent.input.checkThreshold(distX))\n || (this.yDirection && this.parent.input.checkThreshold(distY))))\n {\n const newPoint = { x, y };\n\n if (this.xDirection)\n {\n this.parent.x += (newPoint.x - this.last.x) * this.options.factor;\n }\n if (this.yDirection)\n {\n this.parent.y += (newPoint.y - this.last.y) * this.options.factor;\n }\n this.last = newPoint;\n if (!this.moved)\n {\n this.parent.emit('drag-start', {\n event,\n screen: new Point(this.last.x, this.last.y),\n world: this.parent.toWorld(new Point(this.last.x, this.last.y)),\n viewport: this.parent\n });\n }\n this.moved = true;\n this.parent.emit('moved', { viewport: this.parent, type: 'drag' });\n\n return true;\n }\n }\n else\n {\n this.moved = false;\n }\n }\n\n return false;\n }\n\n public up(event: InteractionEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n const touches = this.parent.input.touches;\n\n if (touches.length === 1)\n {\n const pointer = touches[0];\n\n if (pointer.last)\n {\n this.last = { x: pointer.last.x, y: pointer.last.y };\n this.current = pointer.id;\n }\n this.moved = false;\n\n return true;\n }\n else if (this.last)\n {\n if (this.moved)\n {\n const screen = new Point(this.last.x, this.last.y);\n\n this.parent.emit('drag-end', {\n event, screen,\n world: this.parent.toWorld(screen),\n viewport: this.parent,\n });\n this.last = null;\n this.moved = false;\n\n return true;\n }\n }\n\n return false;\n }\n\n public wheel(event: WheelEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n if (this.options.wheel)\n {\n const wheel = this.parent.plugins.get('wheel', true);\n\n if (!wheel)\n {\n const step = event.deltaMode ? this.options.lineHeight : 1;\n\n if (this.xDirection)\n {\n this.parent.x += event.deltaX * step * this.options.wheelScroll * this.reverse;\n }\n if (this.yDirection)\n {\n this.parent.y += event.deltaY * step * this.options.wheelScroll * this.reverse;\n }\n if (this.options.clampWheel)\n {\n this.clamp();\n }\n this.parent.emit('wheel-scroll', this.parent);\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n if (!this.parent.options.passiveWheel)\n {\n event.preventDefault();\n }\n\n return true;\n }\n }\n\n return false;\n }\n\n public resume()\n {\n this.last = null;\n this.paused = false;\n }\n\n public clamp()\n {\n const decelerate: Partial = this.parent.plugins.get('decelerate', true) || {};\n\n if (this.options.clampWheel !== 'y')\n {\n if (this.parent.screenWorldWidth < this.parent.screenWidth)\n {\n switch (this.underflowX)\n {\n case -1:\n this.parent.x = 0;\n break;\n case 1:\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth);\n break;\n default:\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n }\n }\n else\n if (this.parent.left < 0)\n {\n this.parent.x = 0;\n decelerate.x = 0;\n }\n else if (this.parent.right > this.parent.worldWidth)\n {\n this.parent.x = -this.parent.worldWidth * this.parent.scale.x + this.parent.screenWidth;\n decelerate.x = 0;\n }\n }\n if (this.options.clampWheel !== 'x')\n {\n if (this.parent.screenWorldHeight < this.parent.screenHeight)\n {\n switch (this.underflowY)\n {\n case -1:\n this.parent.y = 0;\n break;\n case 1:\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n break;\n default:\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n }\n }\n else\n {\n if (this.parent.top < 0)\n {\n this.parent.y = 0;\n decelerate.y = 0;\n }\n if (this.parent.bottom > this.parent.worldHeight)\n {\n this.parent.y = -this.parent.worldHeight * this.parent.scale.y + this.parent.screenHeight;\n decelerate.y = 0;\n }\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { DisplayObject } from '@pixi/display';\nimport type { IPointData } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Follow}. */\nexport interface IFollowOptions\n{\n /**\n * Speed to follow in px/frame (0 = teleport to location)\n *\n * @default 9\n */\n speed?: number;\n\n /**\n * Set acceleration to accelerate and decelerate at this rate; speed cannot be 0 to use acceleration\n *\n * @default null\n */\n acceleration?: number | null;\n\n /**\n * Radius (in world coordinates) of center circle where movement is allowed without moving the viewport\n *\n * @default null\n */\n radius?: number | null;\n}\n\nconst DEFAULT_FOLLOW_OPTIONS: Required = {\n speed: 0,\n acceleration: null,\n radius: null\n};\n\n/**\n * Plugin to follow a display-object.\n *\n * @see Viewport.follow\n * @public\n */\nexport class Follow extends Plugin\n{\n /** The options used to initialize this plugin. */\n public readonly options: Required;\n\n /** The target this plugin will make the viewport follow. */\n public target: DisplayObject;\n\n /** The velocity provided the viewport by following, at the current time. */\n protected velocity: IPointData;\n\n /**\n * This is called by {@link Viewport.follow}.\n *\n * @param parent\n * @param target - target to follow\n * @param options\n */\n constructor(parent: Viewport, target: DisplayObject, options: IFollowOptions = {})\n {\n super(parent);\n\n this.target = target;\n this.options = Object.assign({}, DEFAULT_FOLLOW_OPTIONS, options);\n this.velocity = { x: 0, y: 0 };\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n const center = this.parent.center;\n let toX = this.target.x;\n let toY = this.target.y;\n\n if (this.options.radius)\n {\n const distance = Math.sqrt(Math.pow(this.target.y - center.y, 2) + Math.pow(this.target.x - center.x, 2));\n\n if (distance > this.options.radius)\n {\n const angle = Math.atan2(this.target.y - center.y, this.target.x - center.x);\n\n toX = this.target.x - (Math.cos(angle) * this.options.radius);\n toY = this.target.y - (Math.sin(angle) * this.options.radius);\n }\n else\n {\n return;\n }\n }\n\n const deltaX = toX - center.x;\n const deltaY = toY - center.y;\n\n if (deltaX || deltaY)\n {\n if (this.options.speed)\n {\n if (this.options.acceleration)\n {\n const angle = Math.atan2(toY - center.y, toX - center.x);\n const distance = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));\n\n if (distance)\n {\n const decelerationDistance = (Math.pow(this.velocity.x, 2) + Math.pow(this.velocity.y, 2)) / (2 * this.options.acceleration);\n\n if (distance > decelerationDistance)\n {\n this.velocity = {\n x: Math.min(this.velocity.x + this.options.acceleration * elapsed, this.options.speed),\n y: Math.min(this.velocity.y + this.options.acceleration * elapsed, this.options.speed)\n };\n }\n else\n {\n this.velocity = {\n x: Math.max(this.velocity.x - this.options.acceleration * this.options.speed, 0),\n y: Math.max(this.velocity.y - this.options.acceleration * this.options.speed, 0)\n };\n }\n const changeX = Math.cos(angle) * this.velocity.x;\n const changeY = Math.sin(angle) * this.velocity.y;\n const x = Math.abs(changeX) > Math.abs(deltaX) ? toX : center.x + changeX;\n const y = Math.abs(changeY) > Math.abs(deltaY) ? toY : center.y + changeY;\n\n this.parent.moveCenter(x, y);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n else\n {\n const angle = Math.atan2(toY - center.y, toX - center.x);\n const changeX = Math.cos(angle) * this.options.speed;\n const changeY = Math.sin(angle) * this.options.speed;\n const x = Math.abs(changeX) > Math.abs(deltaX) ? toX : center.x + changeX;\n const y = Math.abs(changeY) > Math.abs(deltaY) ? toY : center.y + changeY;\n\n this.parent.moveCenter(x, y);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n else\n {\n this.parent.moveCenter(toX, toY);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\nimport type { InteractionEvent } from '@pixi/interaction';\n\n/** Insets for mouse edges scrolling regions */\nexport interface IMouseEdgesInsets {\n /** Distance from center of screen in screen pixels */\n radius?: number | null;\n\n /** Distance from all sides in screen pixels */\n distance?: number | null;\n\n /** Alternatively, set top distance (leave unset for no top scroll) */\n top?: number | null;\n\n /** Alternatively, set bottom distance (leave unset for no top scroll) */\n bottom?: number | null;\n\n /** Alternatively, set left distance (leave unset for no top scroll) */\n left?: number | null;\n\n /** Alternatively, set right distance (leave unset for no top scroll) */\n right?: number | null;\n}\n\n/** Options for {@link MouseEdges}. */\nexport interface IMouseEdgesOptions extends IMouseEdgesInsets {\n /** Speed in pixels/frame to scroll viewport */\n speed?: number;\n\n /** Reverse direction of scroll */\n reverse?: boolean;\n\n /** Don't use decelerate plugin even if it's installed */\n noDecelerate?: boolean;\n\n /**\n * If using radius, use linear movement (+/- 1, +/- 1) instead of angled movement.\n *\n * (Math.cos(angle from center), Math.sin(angle from center))\n */\n linear?: boolean;\n\n /** Allows plugin to continue working even when there's a `mousedown` event. */\n allowButtons?: boolean;\n}\n\nconst MOUSE_EDGES_OPTIONS: Required = {\n radius: null,\n distance: null,\n top: null,\n bottom: null,\n left: null,\n right: null,\n speed: 8,\n reverse: false,\n noDecelerate: false,\n linear: false,\n allowButtons: false,\n};\n\n/**\n * Scroll viewport when mouse hovers near one of the edges.\n *\n * @event mouse-edge-start(Viewport) emitted when mouse-edge starts\n * @event mouse-edge-end(Viewport) emitted when mouse-edge ends\n */\nexport class MouseEdges extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Readonly>;\n\n /** Factor from reverse option. */\n protected readonly reverse: -1 | 1;\n\n /** Radius squared */\n protected readonly radiusSquared: number | null;\n\n /** Scroll region size on the left side. */\n protected left!: number | null;\n\n /** Scroll region size on the top size. */\n protected top!: number | null;\n\n /** Scroll region size on the right side. */\n protected right!: number | null;\n\n /** Scroll region size on the bottom side. */\n protected bottom!: number | null;\n\n protected horizontal?: number | null;\n\n protected vertical?: number | null;\n\n /**\n * This is called by {@link Viewport.mouseEdges}.\n */\n constructor(parent: Viewport, options: IMouseEdgesOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, MOUSE_EDGES_OPTIONS, options);\n this.reverse = this.options.reverse ? 1 : -1;\n this.radiusSquared = typeof this.options.radius === 'number' ? Math.pow(this.options.radius, 2) : null;\n\n this.resize();\n }\n\n public resize(): void\n {\n const distance = this.options.distance;\n\n if (distance !== null)\n {\n this.left = distance;\n this.top = distance;\n this.right = this.parent.worldScreenWidth - distance;\n this.bottom = this.parent.worldScreenHeight - distance;\n }\n else if (!this.options.radius)\n {\n this.left = this.options.left;\n this.top = this.options.top;\n this.right = this.options.right === null ? null : this.parent.worldScreenWidth - this.options.right;\n this.bottom = this.options.bottom === null ? null : this.parent.worldScreenHeight - this.options.bottom;\n }\n }\n\n public down(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if (!this.options.allowButtons)\n {\n this.horizontal = this.vertical = null;\n }\n\n return false;\n }\n\n public move(event: InteractionEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if ((event.data.pointerType !== 'mouse' && event.data.identifier !== 1)\n || (!this.options.allowButtons && event.data.buttons !== 0))\n {\n return false;\n }\n\n const x = event.data.global.x;\n const y = event.data.global.y;\n\n if (this.radiusSquared)\n {\n const center = this.parent.toScreen(this.parent.center);\n const distance = Math.pow(center.x - x, 2) + Math.pow(center.y - y, 2);\n\n if (distance >= this.radiusSquared)\n {\n const angle = Math.atan2(center.y - y, center.x - x);\n\n if (this.options.linear)\n {\n this.horizontal = Math.round(Math.cos(angle)) * this.options.speed * this.reverse * (60 / 1000);\n this.vertical = Math.round(Math.sin(angle)) * this.options.speed * this.reverse * (60 / 1000);\n }\n else\n {\n this.horizontal = Math.cos(angle) * this.options.speed * this.reverse * (60 / 1000);\n this.vertical = Math.sin(angle) * this.options.speed * this.reverse * (60 / 1000);\n }\n }\n else\n {\n if (this.horizontal)\n {\n this.decelerateHorizontal();\n }\n if (this.vertical)\n {\n this.decelerateVertical();\n }\n\n this.horizontal = this.vertical = 0;\n }\n }\n else\n {\n if (this.left !== null && x < this.left)\n {\n this.horizontal = Number(this.reverse) * this.options.speed * (60 / 1000);\n }\n else if (this.right !== null && x > this.right)\n {\n this.horizontal = -1 * this.reverse * this.options.speed * (60 / 1000);\n }\n else\n {\n this.decelerateHorizontal();\n this.horizontal = 0;\n }\n if (this.top !== null && y < this.top)\n {\n this.vertical = Number(this.reverse) * this.options.speed * (60 / 1000);\n }\n else if (this.bottom !== null && y > this.bottom)\n {\n this.vertical = -1 * this.reverse * this.options.speed * (60 / 1000);\n }\n else\n {\n this.decelerateVertical();\n this.vertical = 0;\n }\n }\n\n return false;\n }\n\n private decelerateHorizontal(): void\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (this.horizontal && decelerate && !this.options.noDecelerate)\n {\n decelerate.activate({ x: (this.horizontal * this.options.speed * this.reverse) / (1000 / 60) });\n }\n }\n\n private decelerateVertical(): void\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (this.vertical && decelerate && !this.options.noDecelerate)\n {\n decelerate.activate({ y: (this.vertical * this.options.speed * this.reverse) / (1000 / 60) });\n }\n }\n\n public up(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if (this.horizontal)\n {\n this.decelerateHorizontal();\n }\n if (this.vertical)\n {\n this.decelerateVertical();\n }\n this.horizontal = this.vertical = null;\n\n return false;\n }\n\n public update(): void\n {\n if (this.paused)\n {\n return;\n }\n\n if (this.horizontal || this.vertical)\n {\n const center = this.parent.center;\n\n if (this.horizontal)\n {\n center.x += this.horizontal * this.options.speed;\n }\n if (this.vertical)\n {\n center.y += this.vertical * this.options.speed;\n }\n\n this.parent.moveCenter(center);\n this.parent.emit('moved', { viewport: this.parent, type: 'mouse-edges' });\n }\n }\n}\n","import { Plugin } from './Plugin';\nimport { Point } from '@pixi/math';\n\nimport type { IPointData } from '@pixi/math';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { IViewportTouch } from '../InputManager';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Pinch}. */\nexport interface IPinchOptions\n{\n /** Disable two-finger dragging. */\n noDrag?: boolean;\n\n /**\n * Percent to modify pinch speed.\n *\n * @default 1\n */\n percent?: number;\n\n /**\n * Factor to multiply two-finger drag to increase speed of movement\n *\n * @default 1\n */\n factor?: number;\n\n /** Place this point at center during zoom instead of center of two fingers */\n center?: Point | null;\n\n /** Axis to zoom */\n axis?: 'all' | 'x' | 'y';\n}\n\nconst DEFAULT_PINCH_OPTIONS: Required = {\n noDrag: false,\n percent: 1,\n center: null,\n factor: 1,\n axis: 'all',\n};\n\n/**\n * Plugin for enabling two-finger pinching (or dragging).\n *\n * @public\n */\nexport class Pinch extends Plugin\n{\n /** Options used to initialize this plugin. */\n public readonly options: Required;\n\n /** Flags whether this plugin is active, i.e. a pointer is down on the viewport. */\n public active = false;\n\n /** Flags whether the viewport is being pinched. */\n public pinching = false;\n\n protected moved = false;\n protected lastCenter?: IPointData | null;\n\n /**\n * This is called by {@link Viewport.pinch}.\n */\n constructor(parent: Viewport, options: IPinchOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_PINCH_OPTIONS, options);\n }\n\n public down(): boolean\n {\n if (this.parent.input.count() >= 2)\n {\n this.active = true;\n return true;\n }\n\n return false;\n }\n\n public isAxisX(): boolean\n {\n return ['all', 'x'].includes(this.options.axis);\n }\n\n public isAxisY(): boolean\n {\n return ['all', 'y'].includes(this.options.axis);\n }\n\n public move(e: InteractionEvent): boolean\n {\n if (this.paused || !this.active)\n {\n return false;\n }\n\n const x = e.data.global.x;\n const y = e.data.global.y;\n\n const pointers = this.parent.input.touches;\n\n if (pointers.length >= 2)\n {\n const first = pointers[0] as IViewportTouch;\n const second = pointers[1] as IViewportTouch;\n const last = (first.last && second.last)\n ? Math.sqrt(Math.pow(second.last.x - first.last.x, 2) + Math.pow(second.last.y - first.last.y, 2))\n : null;\n\n if (first.id === e.data.pointerId)\n {\n first.last = { x, y, data: e.data } as IPointData;\n }\n else if (second.id === e.data.pointerId)\n {\n second.last = { x, y, data: e.data } as IPointData;\n }\n if (last)\n {\n let oldPoint: IPointData | undefined;\n\n const point = {\n x: (first.last as IPointData).x +\n ((second.last as IPointData).x - (first.last as IPointData).x) / 2,\n y: (first.last as IPointData).y +\n ((second.last as IPointData).y - (first.last as IPointData).y) / 2,\n };\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point);\n }\n let dist = Math.sqrt(Math.pow(\n (second.last as IPointData).x - (first.last as IPointData).x, 2) +\n Math.pow((second.last as IPointData).y - (first.last as IPointData).y, 2));\n\n dist = dist === 0 ? dist = 0.0000000001 : dist;\n\n const change = (1 - last / dist) * this.options.percent\n * (this.isAxisX() ? this.parent.scale.x : this.parent.scale.y);\n\n if (this.isAxisX())\n {\n this.parent.scale.x += change;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y += change;\n }\n\n this.parent.emit('zoomed', { viewport: this.parent, type: 'pinch', center: point });\n\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += (point.x - newPoint.x) * this.options.factor;\n this.parent.y += (point.y - newPoint.y) * this.options.factor;\n this.parent.emit('moved', { viewport: this.parent, type: 'pinch' });\n }\n if (!this.options.noDrag && this.lastCenter)\n {\n this.parent.x += (point.x - this.lastCenter.x) * this.options.factor;\n this.parent.y += (point.y - this.lastCenter.y) * this.options.factor;\n this.parent.emit('moved', { viewport: this.parent, type: 'pinch' });\n }\n\n this.lastCenter = point;\n this.moved = true;\n }\n else if (!this.pinching)\n {\n this.parent.emit('pinch-start', this.parent);\n this.pinching = true;\n }\n\n return true;\n }\n\n return false;\n }\n\n public up(): boolean\n {\n if (this.pinching)\n {\n if (this.parent.input.touches.length <= 1)\n {\n this.active = false;\n this.lastCenter = null;\n this.pinching = false;\n this.moved = false;\n this.parent.emit('pinch-end', this.parent);\n\n return true;\n }\n }\n\n return false;\n }\n}\n","import { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Viewport } from '../Viewport';\n\nexport interface ISnapOptions\n{\n /** snap to the top-left of viewport instead of center */\n topLeft?: boolean;\n\n /**\n * Friction/frame to apply if decelerate is active\n *\n * @default 0.8\n */\n friction?: number;\n\n /**\n * @default 1000\n */\n time?: number;\n\n /** Easing function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /**\n * Pause snapping with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Removes this plugin after snapping is complete\n *\n * @default false\n */\n removeOnComplete?: boolean;\n\n /**\n * Removes this plugin if interrupted by any user input\n *\n * @default false\n */\n removeOnInterrupt?: boolean;\n\n /**\n * Starts the snap immediately regardless of whether the viewport is at the desired location\n *\n * @default false\n */\n forceStart?: boolean;\n}\n\nconst DEFAULT_SNAP_OPTIONS: Required = {\n topLeft: false,\n friction: 0.8,\n time: 1000,\n ease: 'easeInOutSine',\n interrupt: true,\n removeOnComplete: false,\n removeOnInterrupt: false,\n forceStart: false\n};\n\n/**\n * @event snap-start(Viewport) emitted each time a snap animation starts\n * @event snap-restart(Viewport) emitted each time a snap resets because of a change in viewport size\n * @event snap-end(Viewport) emitted each time snap reaches its target\n * @event snap-remove(Viewport) emitted if snap plugin is removed\n */\nexport class Snap extends Plugin\n{\n public readonly options: Required;\n public ease?: any;\n public x: number;\n public y: number;\n\n protected percent?: number;\n protected snapping?: { time: number } | null;\n protected deltaX?: number;\n protected deltaY?: number;\n protected startX?: number;\n protected startY?: number;\n\n /**\n * This is called by {@link Viewport.snap}.\n */\n constructor(parent: Viewport, x: number, y: number, options: ISnapOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_SNAP_OPTIONS, options);\n this.ease = ease(options.ease, 'easeInOutSine');\n this.x = x;\n this.y = y;\n\n if (this.options.forceStart)\n {\n this.snapStart();\n }\n }\n\n public snapStart(): void\n {\n this.percent = 0;\n this.snapping = { time: 0 };\n const current = this.options.topLeft ? this.parent.corner : this.parent.center;\n\n this.deltaX = this.x - current.x;\n this.deltaY = this.y - current.y;\n this.startX = current.x;\n this.startY = current.y;\n this.parent.emit('snap-start', this.parent);\n }\n\n public wheel(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap');\n }\n\n return false;\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap');\n }\n else if (this.options.interrupt)\n {\n this.snapping = null;\n }\n\n return false;\n }\n\n public up(): boolean\n {\n if (this.parent.input.count() === 0)\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (decelerate && (decelerate.x || decelerate.y))\n {\n decelerate.percentChangeX = decelerate.percentChangeY = this.options.friction;\n }\n }\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n if (this.options.interrupt && this.parent.input.count() !== 0)\n {\n return;\n }\n if (!this.snapping)\n {\n const current = this.options.topLeft ? this.parent.corner : this.parent.center;\n\n if (current.x !== this.x || current.y !== this.y)\n {\n this.snapStart();\n }\n }\n else\n {\n const snapping = this.snapping;\n\n snapping.time += elapsed;\n let finished;\n let x;\n let y;\n\n const startX = this.startX as number;\n const startY = this.startY as number;\n const deltaX = this.deltaX as number;\n const deltaY = this.deltaY as number;\n\n if (snapping.time > this.options.time)\n {\n finished = true;\n x = startX + deltaX;\n y = startY + deltaY;\n }\n else\n {\n const percent = this.ease(snapping.time, 0, 1, this.options.time);\n\n x = startX + (deltaX * percent);\n y = startY + (deltaY * percent);\n }\n if (this.options.topLeft)\n {\n this.parent.moveCorner(x, y);\n }\n else\n {\n this.parent.moveCenter(x, y);\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'snap' });\n\n if (finished)\n {\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap');\n }\n this.parent.emit('snap-end', this.parent);\n this.snapping = null;\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Point } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link SnapZoom}. */\nexport interface ISnapZoomOptions\n{\n /** the desired width to snap (to maintain aspect ratio, choose only width or height) */\n width?: number;\n\n /** the desired height to snap (to maintain aspect ratio, choose only width or height) */\n height?: number;\n\n /**\n * time for snapping in ms\n *\n * @default 1000\n */\n time?: number;\n\n /** ease function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /** Place this point at center during zoom instead of center of the viewport */\n center?: Point | null;\n\n /**\n * Pause snapping with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Removes this plugin after snapping is complete\n *\n * @default false\n */\n removeOnComplete?: boolean;\n\n /**\n * Removes this plugin if interrupted by any user input\n *\n * @default false\n */\n removeOnInterrupt?: boolean;\n\n /**\n * Starts the snap immediately regardless of whether the viewport is at the desired zoom\n *\n * @default false\n */\n forceStart?: boolean;\n\n /**\n * Zoom but do not move\n *\n * @default false\n */\n noMove?: boolean;\n}\n\nconst DEFAULT_SNAP_ZOOM_OPTIONS: Required = {\n width: 0,\n height: 0,\n time: 1000,\n ease: 'easeInOutSine',\n center: null,\n interrupt: true,\n removeOnComplete: false,\n removeOnInterrupt: false,\n forceStart: false,\n noMove: false\n};\n\n/**\n * @event snap-zoom-start(Viewport) emitted each time a fit animation starts\n * @event snap-zoom-end(Viewport) emitted each time fit reaches its target\n * @event snap-zoom-end(Viewport) emitted each time fit reaches its target\n */\nexport class SnapZoom extends Plugin\n{\n public readonly options: Required;\n\n protected ease: any;\n protected xScale: number;\n protected yScale: number;\n protected xIndependent: boolean;\n protected yIndependent: boolean;\n protected snapping?: {\n time: number;\n startX: number;\n startY: number;\n deltaX: number;\n deltaY: number;\n } | null;\n\n /**\n * This is called by {@link Viewport.snapZoom}.\n */\n constructor(parent: Viewport, options: ISnapZoomOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_SNAP_ZOOM_OPTIONS, options);\n this.ease = ease(this.options.ease);\n\n // Assign defaults for typescript.\n this.xIndependent = false;\n this.yIndependent = false;\n this.xScale = 0;\n this.yScale = 0;\n\n if (this.options.width > 0)\n {\n this.xScale = parent.screenWidth / this.options.width;\n this.xIndependent = true;\n }\n if (this.options.height > 0)\n {\n this.yScale = parent.screenHeight / this.options.height;\n this.yIndependent = true;\n }\n\n this.xScale = this.xIndependent ? (this.xScale as number) : (this.yScale as number);\n this.yScale = this.yIndependent ? (this.yScale as number) : this.xScale;\n\n if (this.options.time === 0)\n {\n // TODO: Fix this\n // @ts-expect-error todo\n parent.container.scale.x = this.xScale;\n\n // TODO: Fix this\n // @ts-expect-error todo\n parent.container.scale.y = this.yScale;\n\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n }\n else if (options.forceStart)\n {\n this.createSnapping();\n }\n }\n\n private createSnapping(): void\n {\n const startWorldScreenWidth = this.parent.worldScreenWidth;\n const startWorldScreenHeight = this.parent.worldScreenHeight;\n const endWorldScreenWidth = this.parent.screenWidth / this.xScale;\n const endWorldScreenHeight = this.parent.screenHeight / this.yScale;\n\n this.snapping = {\n time: 0,\n startX: startWorldScreenWidth,\n startY: startWorldScreenHeight,\n deltaX: endWorldScreenWidth - startWorldScreenWidth,\n deltaY: endWorldScreenHeight - startWorldScreenHeight\n };\n\n this.parent.emit('snap-zoom-start', this.parent);\n }\n\n public resize(): void\n {\n this.snapping = null;\n\n if (this.options.width > 0)\n {\n this.xScale = this.parent.screenWidth / this.options.width;\n }\n if (this.options.height > 0)\n {\n this.yScale = this.parent.screenHeight / this.options.height;\n }\n this.xScale = this.xIndependent ? this.xScale : this.yScale;\n this.yScale = this.yIndependent ? this.yScale : this.xScale;\n }\n\n public wheel(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n\n return false;\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n else if (this.options.interrupt)\n {\n this.snapping = null;\n }\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n if (this.options.interrupt && this.parent.input.count() !== 0)\n {\n return;\n }\n\n let oldCenter: Point | undefined;\n\n if (!this.options.center && !this.options.noMove)\n {\n oldCenter = this.parent.center;\n }\n if (!this.snapping)\n {\n if (this.parent.scale.x !== this.xScale || this.parent.scale.y !== this.yScale)\n {\n this.createSnapping();\n }\n }\n else if (this.snapping)\n {\n const snapping = this.snapping;\n\n snapping.time += elapsed;\n\n if (snapping.time >= this.options.time)\n {\n this.parent.scale.set(this.xScale, this.yScale);\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n this.parent.emit('snap-zoom-end', this.parent);\n this.snapping = null;\n }\n else\n {\n const snapping = this.snapping;\n const worldScreenWidth = this.ease(snapping.time, snapping.startX, snapping.deltaX, this.options.time);\n const worldScreenHeight = this.ease(snapping.time, snapping.startY, snapping.deltaY, this.options.time);\n\n this.parent.scale.x = this.parent.screenWidth / worldScreenWidth;\n this.parent.scale.y = this.parent.screenHeight / worldScreenHeight;\n }\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (!this.options.noMove)\n {\n if (!this.options.center)\n {\n this.parent.moveCenter(oldCenter as Point);\n }\n else\n {\n this.parent.moveCenter(this.options.center);\n }\n }\n }\n }\n\n public resume(): void\n {\n this.snapping = null;\n super.resume();\n }\n}\n","import { Plugin } from './Plugin';\nimport { IPointData, Point } from '@pixi/math';\n\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Wheel}. */\nexport interface IWheelOptions\n{\n /**\n * Percent to scroll with each spin\n *\n * @default 0.1\n */\n percent?: number;\n\n /**\n * smooth the zooming by providing the number of frames to zoom between wheel spins\n *\n * @default false\n */\n smooth?: false | number;\n\n /**\n * Stop smoothing with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Reverse the direction of the scroll\n *\n * @default false\n */\n reverse?: boolean;\n\n /**\n * Place this point at center during zoom instead of current mouse position\n *\n * @default null\n */\n center?: Point | null;\n\n /**\n * Scaling factor for non-DOM_DELTA_PIXEL scrolling events\n *\n * @default 20\n */\n lineHeight?: number;\n\n /**\n * Axis to zoom\n *\n * @default 'all'\n */\n axis?: 'all' | 'x' | 'y';\n}\n\nconst DEFAULT_WHEEL_OPTIONS: Required = {\n percent: 0.1,\n smooth: false,\n interrupt: true,\n reverse: false,\n center: null,\n lineHeight: 20,\n axis: 'all',\n};\n\n/**\n * Plugin for handling wheel scrolling for viewport zoom.\n *\n * @event wheel({wheel: {dx, dy, dz}, event, viewport})\n */\nexport class Wheel extends Plugin\n{\n public readonly options: Required;\n\n protected smoothing?: IPointData | null;\n protected smoothingCenter?: Point | null;\n protected smoothingCount?: number;\n\n /**\n * This is called by {@link Viewport.wheel}.\n */\n constructor(parent: Viewport, options: IWheelOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_WHEEL_OPTIONS, options);\n }\n\n public down(): boolean\n {\n if (this.options.interrupt)\n {\n this.smoothing = null;\n }\n\n return false;\n }\n\n protected isAxisX(): boolean\n {\n return ['all', 'x'].includes(this.options.axis);\n }\n\n protected isAxisY(): boolean\n {\n return ['all', 'y'].includes(this.options.axis);\n }\n\n public update(): void\n {\n if (this.smoothing)\n {\n const point = this.smoothingCenter;\n const change = this.smoothing;\n let oldPoint;\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point as IPointData);\n }\n if (this.isAxisX())\n {\n this.parent.scale.x += change.x;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y += change.y;\n }\n\n this.parent.emit('zoomed', { viewport: this.parent, type: 'wheel' });\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += (point as IPointData).x - newPoint.x;\n this.parent.y += (point as IPointData).y - newPoint.y;\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n (this.smoothingCount as number)++;\n\n if (this.smoothingCount as number >= this.options.smooth)\n {\n this.smoothing = null;\n }\n }\n }\n\n public wheel(e: WheelEvent): boolean | undefined\n {\n if (this.paused)\n {\n return;\n }\n\n const point = this.parent.input.getPointerPosition(e);\n const sign = this.options.reverse ? -1 : 1;\n const step = sign * -e.deltaY * (e.deltaMode ? this.options.lineHeight : 1) / 500;\n const change = Math.pow(2, (1 + this.options.percent) * step);\n\n if (this.options.smooth)\n {\n const original = {\n x: this.smoothing ? this.smoothing.x * (this.options.smooth - (this.smoothingCount as number)) : 0,\n y: this.smoothing ? this.smoothing.y * (this.options.smooth - (this.smoothingCount as number)) : 0\n };\n\n this.smoothing = {\n x: ((this.parent.scale.x + original.x) * change - this.parent.scale.x) / this.options.smooth,\n y: ((this.parent.scale.y + original.y) * change - this.parent.scale.y) / this.options.smooth,\n };\n this.smoothingCount = 0;\n this.smoothingCenter = point;\n }\n else\n {\n let oldPoint: IPointData | undefined;\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point);\n }\n if (this.isAxisX())\n {\n this.parent.scale.x *= change;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y *= change;\n }\n this.parent.emit('zoomed', { viewport: this.parent, type: 'wheel' });\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += point.x - newPoint.x;\n this.parent.y += point.y - newPoint.y;\n }\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n this.parent.emit('wheel', { wheel: { dx: e.deltaX, dy: e.deltaY, dz: e.deltaZ }, event: e, viewport: this.parent });\n\n if (!this.parent.options.passiveWheel)\n {\n return true;\n }\n }\n}\n","import { Point, Rectangle } from '@pixi/math';\n\nimport type { IPointData } from '@pixi/math';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { Viewport } from './Viewport';\n\nexport interface IViewportTouch {\n id: number;\n last: IPointData | null;\n}\n\n/**\n * Handles all input for Viewport\n *\n * @internal\n * @ignore\n * @private\n */\nexport class InputManager\n{\n public readonly viewport: Viewport;\n\n public clickedAvailable?: boolean;\n public isMouseDown?: boolean;\n public last?: Point | null;\n public wheelFunction?: (e: WheelEvent) => void;\n /** List of active touches on viewport */\n public touches: IViewportTouch[];\n\n constructor(viewport: Viewport)\n {\n this.viewport = viewport;\n this.touches = [];\n\n this.addListeners();\n }\n\n /** Add input listeners */\n private addListeners()\n {\n this.viewport.interactive = true;\n if (!this.viewport.forceHitArea)\n {\n this.viewport.hitArea = new Rectangle(0, 0, this.viewport.worldWidth, this.viewport.worldHeight);\n }\n this.viewport.on('pointerdown', this.down, this);\n this.viewport.on('pointermove', this.move, this);\n this.viewport.on('pointerup', this.up, this);\n this.viewport.on('pointerupoutside', this.up, this);\n this.viewport.on('pointercancel', this.up, this);\n this.viewport.on('pointerout', this.up, this);\n this.wheelFunction = (e) => this.handleWheel(e);\n this.viewport.options.divWheel.addEventListener(\n 'wheel',\n this.wheelFunction as any,\n { passive: this.viewport.options.passiveWheel });\n this.isMouseDown = false;\n }\n\n /**\n * Removes all event listeners from viewport\n * (useful for cleanup of wheel when removing viewport)\n */\n public destroy()\n {\n this.viewport.options.divWheel.removeEventListener('wheel', this.wheelFunction as any);\n }\n\n /**\n * handle down events for viewport\n *\n * @param {PIXI.InteractionEvent} event\n */\n public down(event: InteractionEvent)\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n if (event.data.pointerType === 'mouse')\n {\n this.isMouseDown = true;\n }\n else if (!this.get(event.data.pointerId))\n {\n this.touches.push({ id: event.data.pointerId, last: null });\n }\n if (this.count() === 1)\n {\n this.last = event.data.global.clone();\n\n // clicked event does not fire if viewport is decelerating or bouncing\n const decelerate = this.viewport.plugins.get('decelerate', true);\n const bounce = this.viewport.plugins.get('bounce', true);\n\n if ((!decelerate || !decelerate.isActive()) && (!bounce || !bounce.isActive()))\n {\n this.clickedAvailable = true;\n }\n else\n {\n this.clickedAvailable = false;\n }\n }\n else\n {\n this.clickedAvailable = false;\n }\n\n const stop = this.viewport.plugins.down(event);\n\n if (stop && this.viewport.options.stopPropagation)\n {\n event.stopPropagation();\n }\n }\n\n /** Clears all pointer events */\n public clear(): void\n {\n this.isMouseDown = false;\n this.touches = [];\n this.last = null;\n }\n\n /**\n * @param {number} change\n * @returns whether change exceeds threshold\n */\n public checkThreshold(change: number): boolean\n {\n if (Math.abs(change) >= this.viewport.threshold)\n {\n return true;\n }\n\n return false;\n }\n\n /** Handle move events for viewport */\n public move(event: InteractionEvent): void\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n\n const stop = this.viewport.plugins.move(event);\n\n if (this.clickedAvailable && this.last)\n {\n const distX = event.data.global.x - this.last.x;\n const distY = event.data.global.y - this.last.y;\n\n if (this.checkThreshold(distX) || this.checkThreshold(distY))\n {\n this.clickedAvailable = false;\n }\n }\n\n if (stop && this.viewport.options.stopPropagation)\n {\n event.stopPropagation();\n }\n }\n\n /** Handle up events for viewport */\n public up(event: InteractionEvent): void\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n\n if (event.data.pointerType === 'mouse')\n {\n this.isMouseDown = false;\n }\n\n if (event.data.pointerType !== 'mouse')\n {\n this.remove(event.data.pointerId);\n }\n\n const stop = this.viewport.plugins.up(event);\n\n if (this.clickedAvailable && this.count() === 0 && this.last)\n {\n this.viewport.emit('clicked', {\n event,\n screen: this.last,\n world: this.viewport.toWorld(this.last),\n viewport: this\n });\n this.clickedAvailable = false;\n }\n\n if (stop && this.viewport.options.stopPropagation)\n {\n event.stopPropagation();\n }\n }\n\n /** Gets pointer position if this.interaction is set */\n public getPointerPosition(event: WheelEvent): Point\n {\n const point = new Point();\n\n if (this.viewport.options.interaction)\n {\n this.viewport.options.interaction.mapPositionToPoint(point, event.clientX, event.clientY);\n }\n else\n {\n point.x = event.clientX;\n point.y = event.clientY;\n }\n\n return point;\n }\n\n /** Handle wheel events */\n public handleWheel(event: WheelEvent): void\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n\n // do not handle events coming from other elements\n if (this.viewport.options.interaction\n && (this.viewport.options.interaction as any).interactionDOMElement !== event.target)\n {\n return;\n }\n\n // only handle wheel events where the mouse is over the viewport\n const point = this.viewport.toLocal(this.getPointerPosition(event));\n\n if (this.viewport.left <= point.x\n && point.x <= this.viewport.right\n && this.viewport.top <= point.y\n && point.y <= this.viewport.bottom)\n {\n const stop = this.viewport.plugins.wheel(event);\n\n if (stop && !this.viewport.options.passiveWheel)\n {\n event.preventDefault();\n }\n }\n }\n\n public pause(): void\n {\n this.touches = [];\n this.isMouseDown = false;\n }\n\n /** Get touch by id */\n public get(id: number): IViewportTouch | null\n {\n for (const touch of this.touches)\n {\n if (touch.id === id)\n {\n return touch;\n }\n }\n\n return null;\n }\n\n /** Remove touch by number */\n remove(id: number): void\n {\n for (let i = 0; i < this.touches.length; i++)\n {\n if (this.touches[i].id === id)\n {\n this.touches.splice(i, 1);\n\n return;\n }\n }\n }\n\n /**\n * @returns {number} count of mouse/touch pointers that are down on the viewport\n */\n count(): number\n {\n return (this.isMouseDown ? 1 : 0) + this.touches.length;\n }\n}\n","import type {\n Animate,\n Bounce,\n Clamp,\n ClampZoom,\n Decelerate,\n Drag,\n Follow,\n MouseEdges,\n Pinch,\n Plugin,\n Snap,\n SnapZoom,\n Wheel,\n} from './plugins';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { Viewport } from './Viewport';\n\nconst PLUGIN_ORDER = [\n 'drag',\n 'pinch',\n 'wheel',\n 'follow',\n 'mouse-edges',\n 'decelerate',\n 'aniamte',\n 'bounce',\n 'snap-zoom',\n 'clamp-zoom',\n 'snap',\n 'clamp',\n];\n\n/**\n * Use this to access current plugins or add user-defined plugins\n *\n * @public\n */\nexport class PluginManager\n{\n /** Maps mounted plugins by their type */\n public plugins: Partial>;\n\n /**\n * List of plugins mounted\n *\n * This list is kept sorted by the internal priority of plugins (hard-coded).\n */\n public list: Array;\n\n /** The viewport using the plugins managed by `this`. */\n public readonly viewport: Viewport;\n\n /** This is called by {@link Viewport} to initialize the {@link Viewport.plugins plugins}. */\n constructor(viewport: Viewport)\n {\n this.viewport = viewport;\n this.list = [];\n this.plugins = {};\n }\n\n /**\n * Inserts a named plugin or a user plugin into the viewport\n * default plugin order: 'drag', 'pinch', 'wheel', 'follow', 'mouse-edges', 'decelerate', 'bounce',\n * 'snap-zoom', 'clamp-zoom', 'snap', 'clamp'\n *\n * @param {string} name of plugin\n * @param {Plugin} plugin - instantiated Plugin class\n * @param {number} index to insert userPlugin (otherwise inserts it at the end)\n */\n public add(name: string, plugin: Plugin, index: number = PLUGIN_ORDER.length)\n {\n this.plugins[name] = plugin;\n\n const current = PLUGIN_ORDER.indexOf(name);\n\n if (current !== -1)\n {\n PLUGIN_ORDER.splice(current, 1);\n }\n\n PLUGIN_ORDER.splice(index, 0, name);\n this.sort();\n }\n\n public get(name: 'animate', ignorePaused?: boolean): Animate | undefined | null;\n public get(name: 'bounce', ignorePaused?: boolean): Bounce | undefined | null;\n public get(name: 'clamp', ignorePaused?: boolean): Clamp | undefined | null;\n public get(name: 'clamp-zoom', ignorePaused?: boolean): ClampZoom | undefined | null;\n public get(name: 'decelerate', ignorePaused?: boolean): Decelerate | undefined | null;\n public get(name: 'drag', ignorePaused?: boolean): Drag | undefined | null;\n public get(name: 'follow', ignorePaused?: boolean): Follow | undefined | null;\n public get(name: 'mouse-edges', ignorePaused?: boolean): MouseEdges | undefined | null;\n public get(name: 'pinch', ignorePaused?: boolean): Pinch | undefined | null;\n public get(name: 'snap', ignorePaused?: boolean): Snap | undefined | null;\n public get(name: 'snap-zoom', ignorePaused?: boolean): SnapZoom | undefined | null;\n public get(name: 'wheel', ignorePaused?: boolean): Wheel | undefined | null;\n public get(name: string, ignorePaused?: boolean): T | undefined | null;\n\n /**\n * Get plugin\n *\n * @param {string} name of plugin\n * @param {boolean} [ignorePaused] return null if plugin is paused\n */\n public get(name: string, ignorePaused?: boolean): T | undefined | null\n {\n if (ignorePaused)\n {\n if (this.plugins[name]?.paused)\n {\n return null;\n }\n }\n\n return this.plugins[name] as T;\n }\n\n /**\n * Update all active plugins\n *\n * @internal\n * @ignore\n * @param {number} elapsed type in milliseconds since last update\n */\n public update(elapsed: number): void\n {\n for (const plugin of this.list)\n {\n plugin.update(elapsed);\n }\n }\n\n /**\n * Resize all active plugins\n *\n * @internal\n * @ignore\n */\n public resize(): void\n {\n for (const plugin of this.list)\n {\n plugin.resize();\n }\n }\n\n /** Clamps and resets bounce and decelerate (as needed) after manually moving viewport */\n public reset(): void\n {\n for (const plugin of this.list)\n {\n plugin.reset();\n }\n }\n\n /** removes all installed plugins */\n public removeAll(): void\n {\n this.plugins = {};\n this.sort();\n }\n\n /**\n * Removes installed plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public remove(name: string): void\n {\n if (this.plugins[name])\n {\n delete this.plugins[name];\n this.viewport.emit(`${name}-remove`);\n this.sort();\n }\n }\n\n /**\n * Pause plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public pause(name: string): void\n {\n this.plugins[name]?.pause();\n }\n\n /**\n * Resume plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public resume(name: string): void\n {\n this.plugins[name]?.resume();\n }\n\n /**\n * Sort plugins according to PLUGIN_ORDER\n *\n * @internal\n * @ignore\n */\n public sort()\n {\n this.list = [];\n\n for (const plugin of PLUGIN_ORDER)\n {\n if (this.plugins[plugin])\n {\n this.list.push(this.plugins[plugin] as Plugin);\n }\n }\n }\n\n /**\n * Handle down for all plugins\n *\n * @internal\n * @ignore\n */\n public down(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.list)\n {\n if (plugin.down(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle move for all plugins\n *\n * @internal\n * @ignore\n */\n public move(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.viewport.plugins.list)\n {\n if (plugin.move(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle up for all plugins\n *\n * @internal\n * @ignore\n */\n public up(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.list)\n {\n if (plugin.up(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle wheel event for all plugins\n *\n * @internal\n * @ignore\n */\n public wheel(e: WheelEvent): boolean\n {\n let result = false;\n\n for (const plugin of this.list)\n {\n if (plugin.wheel(e))\n {\n result = true;\n }\n }\n\n return result;\n }\n}\n","import { Container } from '@pixi/display';\nimport { Point, Rectangle } from '@pixi/math';\nimport { Ticker } from '@pixi/ticker';\n\nimport { InputManager } from './InputManager';\nimport { PluginManager } from './PluginManager';\nimport {\n Animate,\n Bounce,\n Clamp,\n ClampZoom,\n Decelerate,\n Drag,\n Follow,\n MouseEdges,\n Pinch,\n Snap,\n SnapZoom,\n Wheel\n} from './plugins';\n\nimport type { DisplayObject, IDestroyOptions } from '@pixi/display';\nimport type { IHitArea, InteractionManager } from '@pixi/interaction';\n\n/** Options for {@link Viewport}. */\nexport interface IViewportOptions {\n /** @default window.innerWidth */\n screenWidth?: number;\n\n /** @default window.innerHeight */\n screenHeight?: number;\n\n /** @default this.width */\n worldWidth?: number | null;\n\n /** @default this.height */\n worldHeight?: number | null;\n\n /**\n * Number of pixels to move to trigger an input event (e.g., drag, pinch) or disable a clicked event\n *\n * @default 5\n */\n threshold?: number;\n\n /**\n * Whether the 'wheel' event is set to passive (note: if false, e.preventDefault() will be called when wheel is used over the viewport)\n *\n * @default true\n */\n passiveWheel?: boolean;\n\n /**\n * Whether to stopPropagation of events that impact the viewport (except wheel events, see options.passiveWheel)\n */\n stopPropagation?: boolean;\n\n /**\n * Change the default hitArea from world size to a new value\n */\n forceHitArea?: Rectangle | null;\n\n /**\n * Set this if you want to manually call update() function on each frame\n *\n * @default false\n */\n noTicker?: boolean;\n\n /**\n * InteractionManager, available from instantiated `WebGLRenderer/CanvasRenderer.plugins.interaction`\n *\n * It's used to calculate pointer postion relative to canvas location on screen\n */\n interaction?: InteractionManager | null;\n\n /**\n * Remove oncontextmenu=() => {} from the divWheel element\n */\n disableOnContextMenu?: boolean;\n\n /**\n * div to attach the wheel event\n *\n * @default document.body\n */\n divWheel?: HTMLElement;\n\n /**\n * Use this PIXI.ticker for updates\n *\n * @default PIXI.Ticker.shared\n */\n ticker?: Ticker;\n}\n\nexport interface ICompleteViewportOptions extends IViewportOptions {\n screenWidth: number;\n screenHeight: number;\n threshold: number;\n passiveWheel: boolean;\n stopPropagation: boolean;\n noTicker: boolean;\n ticker: Ticker;\n}\n\nexport interface IViewportTransformState {\n x: number;\n y: number;\n scaleX: number;\n scaleY: number;\n}\n\nconst DEFAULT_VIEWPORT_OPTIONS: ICompleteViewportOptions = {\n screenWidth: window.innerWidth,\n screenHeight: window.innerHeight,\n worldWidth: null,\n worldHeight: null,\n threshold: 5,\n passiveWheel: true,\n stopPropagation: false,\n forceHitArea: null,\n noTicker: false,\n interaction: null,\n disableOnContextMenu: false,\n ticker: Ticker.shared,\n};\n\n/**\n * Main class to use when creating a Viewport\n *\n * @public\n * @fires clicked\n * @fires drag-start\n * @fires drag-end\n * @fires drag-remove\n * @fires pinch-start\n * @fires pinch-end\n * @fires pinch-remove\n * @fires snap-start\n * @fires snap-end\n * @fires snap-remove\n * @fires snap-zoom-start\n * @fires snap-zoom-end\n * @fires snap-zoom-remove\n * @fires bounce-x-start\n * @fires bounce-x-end\n * @fires bounce-y-start\n * @fires bounce-y-end\n * @fires bounce-remove\n * @fires wheel\n * @fires wheel-remove\n * @fires wheel-scroll\n * @fires wheel-scroll-remove\n * @fires mouse-edge-start\n * @fires mouse-edge-end\n * @fires mouse-edge-remove\n * @fires moved\n * @fires moved-end\n * @fires zoomed\n * @fires zoomed-end\n * @fires frame-end\n */\nexport class Viewport extends Container\n{\n /** Flags whether the viewport is being panned */\n public moving?: boolean;\n\n public screenWidth: number;\n public screenHeight: number;\n\n /** Number of pixels to move to trigger an input event (e.g., drag, pinch) or disable a clicked event */\n public threshold: number;\n\n public readonly input: InputManager;\n\n /** Use this to add user plugins or access existing plugins (e.g., to pause, resume, or remove them) */\n public readonly plugins: PluginManager;\n\n /** Flags whether the viewport zoom is being changed. */\n public zooming?: boolean;\n\n public lastViewport?: IViewportTransformState | null;\n\n /** The options passed when creating this viewport, merged with the default values */\n public readonly options: ICompleteViewportOptions & { divWheel: HTMLElement };\n\n private _dirty?: boolean;\n private _forceHitArea?: IHitArea | null;\n private _hitAreaDefault?: Rectangle;\n private _pause?: boolean;\n private readonly tickerFunction?: () => void;\n private _worldWidth?: number | null;\n private _worldHeight?: number | null;\n\n /**\n * @param options\n */\n constructor(options: IViewportOptions = {})\n {\n super();\n this.options = Object.assign(\n {},\n { divWheel: document.body },\n DEFAULT_VIEWPORT_OPTIONS,\n options\n );\n\n this.screenWidth = this.options.screenWidth;\n this.screenHeight = this.options.screenHeight;\n\n this._worldWidth = this.options.worldWidth;\n this._worldHeight = this.options.worldHeight;\n this.forceHitArea = this.options.forceHitArea;\n this.threshold = this.options.threshold;\n\n this.options.divWheel = this.options.divWheel || document.body;\n\n if (this.options.disableOnContextMenu)\n {\n this.options.divWheel.oncontextmenu = (e) => e.preventDefault();\n }\n if (!this.options.noTicker)\n {\n this.tickerFunction = () => this.update(this.options.ticker.elapsedMS);\n this.options.ticker.add(this.tickerFunction);\n }\n\n this.input = new InputManager(this);\n this.plugins = new PluginManager(this);\n }\n\n /** Overrides PIXI.Container's destroy to also remove the 'wheel' and PIXI.Ticker listeners */\n destroy(options: IDestroyOptions)\n {\n if (!this.options.noTicker && this.tickerFunction)\n {\n this.options.ticker.remove(this.tickerFunction);\n }\n\n this.input.destroy();\n super.destroy(options);\n }\n\n /**\n * Update viewport on each frame.\n *\n * By default, you do not need to call this unless you set `options.noTicker=true`.\n *\n * @param {number} elapsed time in milliseconds since last update\n */\n update(elapsed: number): void\n {\n if (!this.pause)\n {\n this.plugins.update(elapsed);\n\n if (this.lastViewport)\n {\n // Check for moved-end event\n if (this.lastViewport.x !== this.x || this.lastViewport.y !== this.y)\n {\n this.moving = true;\n }\n else if (this.moving)\n {\n this.emit('moved-end', this);\n this.moving = false;\n }\n\n // Check for zoomed-end event\n if (this.lastViewport.scaleX !== this.scale.x || this.lastViewport.scaleY !== this.scale.y)\n {\n this.zooming = true;\n }\n else if (this.zooming)\n {\n this.emit('zoomed-end', this);\n this.zooming = false;\n }\n }\n\n if (!this.forceHitArea)\n {\n this._hitAreaDefault = new Rectangle(this.left, this.top, this.worldScreenWidth, this.worldScreenHeight);\n this.hitArea = this._hitAreaDefault;\n }\n\n this._dirty = this._dirty || !this.lastViewport\n || this.lastViewport.x !== this.x || this.lastViewport.y !== this.y\n || this.lastViewport.scaleX !== this.scale.x || this.lastViewport.scaleY !== this.scale.y;\n\n this.lastViewport = {\n x: this.x,\n y: this.y,\n scaleX: this.scale.x,\n scaleY: this.scale.y\n };\n this.emit('frame-end', this);\n }\n }\n\n /** Use this to set screen and world sizes, needed for pinch/wheel/clamp/bounce. */\n resize(\n screenWidth = window.innerWidth,\n screenHeight = window.innerHeight,\n worldWidth: number,\n worldHeight: number\n ): void\n {\n this.screenWidth = screenWidth;\n this.screenHeight = screenHeight;\n\n if (typeof worldWidth !== 'undefined')\n {\n this._worldWidth = worldWidth;\n }\n if (typeof worldHeight !== 'undefined')\n {\n this._worldHeight = worldHeight;\n }\n\n this.plugins.resize();\n this.dirty = true;\n }\n\n /** World width, in pixels */\n get worldWidth(): number\n {\n if (this._worldWidth)\n {\n return this._worldWidth;\n }\n\n return this.width / this.scale.x;\n }\n set worldWidth(value)\n {\n this._worldWidth = value;\n this.plugins.resize();\n }\n\n /** World height, in pixels */\n get worldHeight(): number\n {\n if (this._worldHeight)\n {\n return this._worldHeight;\n }\n\n return this.height / this.scale.y;\n }\n set worldHeight(value)\n {\n this._worldHeight = value;\n this.plugins.resize();\n }\n\n /** Get visible world bounds of viewport */\n public getVisibleBounds(): Rectangle\n {\n return new Rectangle(this.left, this.top, this.worldScreenWidth, this.worldScreenHeight);\n }\n\n /** Change coordinates from screen to world */\n public toWorld(x: number, y: number): Point;\n /** Change coordinates from screen to world */\n public toWorld(screenPoint: Point): Point;\n\n public toWorld(x: number | Point, y?: number): Point\n {\n if (arguments.length === 2)\n {\n return this.toLocal(new Point(x as number, y));\n }\n\n return this.toLocal(x as Point);\n }\n\n /** Change coordinates from world to screen */\n public toScreen(x: number, y: number): Point;\n /** Change coordinates from world to screen */\n public toScreen(worldPoint: Point): Point;\n\n public toScreen(x: number | Point, y?: number): Point\n {\n if (arguments.length === 2)\n {\n return this.toGlobal(new Point(x as number, y));\n }\n\n return this.toGlobal(x as Point);\n }\n\n /** Screen width in world coordinates */\n get worldScreenWidth(): number\n {\n return this.screenWidth / this.scale.x;\n }\n\n /** Screen height in world coordinates */\n get worldScreenHeight(): number\n {\n return this.screenHeight / this.scale.y;\n }\n\n /** World width in screen coordinates */\n get screenWorldWidth(): number\n {\n return this.worldWidth * this.scale.x;\n }\n\n /** World height in screen coordinates */\n get screenWorldHeight()\n {\n return this.worldHeight * this.scale.y;\n }\n\n /** Center of screen in world coordinates */\n get center(): Point\n {\n return new Point(\n this.worldScreenWidth / 2 - this.x / this.scale.x,\n this.worldScreenHeight / 2 - this.y / this.scale.y,\n );\n }\n set center(value: Point)\n {\n this.moveCenter(value);\n }\n\n /** Move center of viewport to (x, y) */\n public moveCenter(x: number, y: number): this;\n\n /** Move center of viewport to {@code center}. */\n public moveCenter(center: Point): this;\n\n public moveCenter(...args: [number, number] | [Point]): this\n {\n let x: number;\n let y: number;\n\n if (typeof args[0] === 'number')\n {\n x = args[0];\n y = args[1] as number;\n }\n else\n {\n x = args[0].x;\n y = args[0].y;\n }\n\n const newX = (this.worldScreenWidth / 2 - x) * this.scale.x;\n const newY = (this.worldScreenHeight / 2 - y) * this.scale.y;\n\n if (this.x !== newX || this.y !== newY)\n {\n this.position.set(newX, newY);\n this.plugins.reset();\n this.dirty = true;\n }\n\n return this;\n }\n\n /** Top-left corner of Viewport */\n get corner(): Point\n {\n return new Point(-this.x / this.scale.x, -this.y / this.scale.y);\n }\n set corner(value: Point)\n {\n this.moveCorner(value);\n }\n\n /** Move viewport's top-left corner; also clamps and resets decelerate and bounce (as needed) */\n public moveCorner(x: number, y: number): this;\n\n /** move viewport's top-left corner; also clamps and resets decelerate and bounce (as needed) */\n public moveCorner(center: Point): this;\n\n public moveCorner(...args: [number, number] | [Point])\n {\n let x;\n let y;\n\n if (args.length === 1)\n {\n x = -args[0].x * this.scale.x;\n y = -args[0].y * this.scale.y;\n }\n else\n {\n x = -args[0] * this.scale.x;\n y = -args[1] * this.scale.y;\n }\n\n if (x !== this.x || y !== this.y)\n {\n this.position.set(x, y);\n this.plugins.reset();\n this.dirty = true;\n }\n\n return this;\n }\n\n /** Get how many world pixels fit in screen's width */\n get screenWidthInWorldPixels()\n {\n return this.screenWidth / this.scale.x;\n }\n\n /** Get how many world pixels fit on screen's height */\n get screenHeightInWorldPixels()\n {\n return this.screenHeight / this.scale.y;\n }\n\n /**\n * Find the scale value that fits a world width on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param width - Width in world pixels\n * @return - scale\n */\n findFitWidth(width: number): number\n {\n return this.screenWidth / width;\n }\n\n /**\n * Finds the scale value that fits a world height on the screens\n * does not change the viewport (use fit... to change)\n *\n * @param height - Height in world pixels\n * @return - scale\n */\n findFitHeight(height: number): number\n {\n return this.screenHeight / height;\n }\n\n /**\n * Finds the scale value that fits the smaller of a world width and world height on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param {number} width in world pixels\n * @param {number} height in world pixels\n * @returns {number} scale\n */\n findFit(width: number, height: number): number\n {\n const scaleX = this.screenWidth / width;\n const scaleY = this.screenHeight / height;\n\n return Math.min(scaleX, scaleY);\n }\n\n /**\n * Finds the scale value that fits the larger of a world width and world height on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param {number} width in world pixels\n * @param {number} height in world pixels\n * @returns {number} scale\n */\n findCover(width: number, height: number): number\n {\n const scaleX = this.screenWidth / width;\n const scaleY = this.screenHeight / height;\n\n return Math.max(scaleX, scaleY);\n }\n\n /**\n * Change zoom so the width fits in the viewport\n *\n * @param width - width in world coordinates\n * @param center - maintain the same center\n * @param scaleY - whether to set scaleY=scaleX\n * @param noClamp - whether to disable clamp-zoom\n * @returns {Viewport} this\n */\n fitWidth(width = this.worldWidth, center?: boolean, scaleY = true, noClamp?: boolean): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.x = this.screenWidth / width;\n\n if (scaleY)\n {\n this.scale.y = this.scale.x;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (!noClamp && clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so the height fits in the viewport\n *\n * @param {number} [height=this.worldHeight] in world coordinates\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @param {boolean} [scaleX=true] whether to set scaleX = scaleY\n * @param {boolean} [noClamp] whether to disable clamp-zoom\n * @returns {Viewport} this\n */\n fitHeight(height = this.worldHeight, center?: boolean, scaleX = true, noClamp?: boolean)\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.y = this.screenHeight / height;\n\n if (scaleX)\n {\n this.scale.x = this.scale.y;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (!noClamp && clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so it fits the entire world in the viewport\n *\n * @param {boolean} center maintain the same center of the screen after zoom\n * @returns {Viewport} this\n */\n fitWorld(center?: boolean): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n\n this.scale.x = this.screenWidth / this.worldWidth;\n this.scale.y = this.screenHeight / this.worldHeight;\n\n if (this.scale.x < this.scale.y)\n {\n this.scale.y = this.scale.x;\n }\n else\n {\n this.scale.x = this.scale.y;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so it fits the size or the entire world in the viewport\n *\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @param {number} [width=this.worldWidth] desired width\n * @param {number} [height=this.worldHeight] desired height\n * @returns {Viewport} this\n */\n fit(center?: boolean, width = this.worldWidth, height = this.worldHeight): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n\n this.scale.x = this.screenWidth / width;\n this.scale.y = this.screenHeight / height;\n\n if (this.scale.x < this.scale.y)\n {\n this.scale.y = this.scale.x;\n }\n else\n {\n this.scale.x = this.scale.y;\n }\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n // eslint-disable-next-line\n // @ts-ignore\n set visible(value: boolean)\n {\n if (!value)\n {\n this.input.clear();\n }\n\n super.visible = value;\n }\n\n /**\n * Zoom viewport to specific value.\n *\n * @param {number} scale value (e.g., 1 would be 100%, 0.25 would be 25%)\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n setZoom(scale: number, center?: boolean): this\n {\n let save;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.set(scale);\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Zoom viewport by a certain percent (in both x and y direction).\n *\n * @param {number} percent change (e.g., 0.25 would increase a starting scale of 1.0 to 1.25)\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n zoomPercent(percent: number, center?: boolean): this\n {\n return this.setZoom(this.scale.x + this.scale.x * percent, center);\n }\n\n /**\n * Zoom viewport by increasing/decreasing width by a certain number of pixels.\n *\n * @param {number} change in pixels\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n zoom(change: number, center?: boolean)\n {\n this.fitWidth(change + this.worldScreenWidth, center);\n\n return this;\n }\n\n /** Changes scale of viewport and maintains center of viewport */\n get scaled(): number\n {\n return this.scale.x;\n }\n set scaled(scale: number)\n {\n this.setZoom(scale, true);\n }\n\n /**\n * @param {SnapZoomOptions} options\n */\n snapZoom(options: any): this\n {\n this.plugins.add('snap-zoom', new SnapZoom(this, options));\n\n return this;\n }\n\n /** Is container out of world bounds */\n OOB(): {\n left: boolean;\n right: boolean;\n top: boolean;\n bottom: boolean;\n cornerPoint: Point;\n }\n {\n return {\n left: this.left < 0,\n right: this.right > this.worldWidth,\n top: this.top < 0,\n bottom: this.bottom > this.worldHeight,\n cornerPoint: new Point(\n this.worldWidth * this.scale.x - this.screenWidth,\n this.worldHeight * this.scale.y - this.screenHeight\n )\n };\n }\n\n /** World coordinates of the right edge of the screen */\n get right(): number\n {\n return -this.x / this.scale.x + this.worldScreenWidth;\n }\n set right(value: number)\n {\n this.x = -value * this.scale.x + this.screenWidth;\n this.plugins.reset();\n }\n\n /** World coordinates of the left edge of the screen */\n get left(): number\n {\n return -this.x / this.scale.x;\n }\n set left(value: number)\n {\n this.x = -value * this.scale.x;\n this.plugins.reset();\n }\n\n /** World coordinates of the top edge of the screen */\n get top(): number\n {\n return -this.y / this.scale.y;\n }\n set top(value: number)\n {\n this.y = -value * this.scale.y;\n this.plugins.reset();\n }\n\n /** World coordinates of the bottom edge of the screen */\n get bottom(): number\n {\n return -this.y / this.scale.y + this.worldScreenHeight;\n }\n set bottom(value: number)\n {\n this.y = -value * this.scale.y + this.screenHeight;\n this.plugins.reset();\n }\n\n /**\n * Determines whether the viewport is dirty (i.e., needs to be renderered to the screen because of a change)\n */\n get dirty(): boolean\n {\n return !!this._dirty;\n }\n set dirty(value: boolean)\n {\n this._dirty = value;\n }\n\n /**\n * Permanently changes the Viewport's hitArea\n *\n * NOTE: if not set then hitArea = PIXI.Rectangle(Viewport.left, Viewport.top, Viewport.worldScreenWidth, Viewport.worldScreenHeight)\n */\n get forceHitArea(): IHitArea | null | undefined\n {\n return this._forceHitArea;\n }\n set forceHitArea(value: IHitArea | null | undefined)\n {\n if (value)\n {\n this._forceHitArea = value;\n this.hitArea = value;\n }\n else\n {\n this._forceHitArea = null;\n this.hitArea = new Rectangle(0, 0, this.worldWidth, this.worldHeight);\n }\n }\n\n /**\n * Enable one-finger touch to drag\n *\n * NOTE: if you expect users to use right-click dragging, you should enable `viewport.options.disableOnContextMenu`\n * to avoid the context menu popping up on each right-click drag.\n *\n * @param {DragOptions} [options]\n * @returns {Viewport} this\n */\n public drag(options: any): this\n {\n this.plugins.add('drag', new Drag(this, options));\n\n return this;\n }\n\n /**\n * Clamp to world boundaries or other provided boundaries\n *\n * NOTES:\n * clamp is disabled if called with no options; use { direction: 'all' } for all edge clamping\n * screenWidth, screenHeight, worldWidth, and worldHeight needs to be set for this to work properly\n *\n * @param {ClampOptions} [options]\n * @returns {Viewport} this\n */\n public clamp(options: any): this\n {\n this.plugins.add('clamp', new Clamp(this, options));\n\n return this;\n }\n\n /**\n * Decelerate after a move\n *\n * NOTE: this fires 'moved' event during deceleration\n *\n * @param {DecelerateOptions} [options]\n * @return {Viewport} this\n */\n public decelerate(options: any): this\n {\n this.plugins.add('decelerate', new Decelerate(this, options));\n\n return this;\n }\n\n /**\n * Bounce on borders\n * NOTES:\n * screenWidth, screenHeight, worldWidth, and worldHeight needs to be set for this to work properly\n * fires 'moved', 'bounce-x-start', 'bounce-y-start', 'bounce-x-end', and 'bounce-y-end' events\n * @param {object} [options]\n * @param {string} [options.sides=all] all, horizontal, vertical, or combination of top, bottom, right, left (e.g., 'top-bottom-right')\n * @param {number} [options.friction=0.5] friction to apply to decelerate if active\n * @param {number} [options.time=150] time in ms to finish bounce\n * @param {object} [options.bounceBox] use this bounceBox instead of (0, 0, viewport.worldWidth, viewport.worldHeight)\n * @param {number} [options.bounceBox.x=0]\n * @param {number} [options.bounceBox.y=0]\n * @param {number} [options.bounceBox.width=viewport.worldWidth]\n * @param {number} [options.bounceBox.height=viewport.worldHeight]\n * @param {string|function} [options.ease=easeInOutSine] ease function or name (see http://easings.net/ for supported names)\n * @param {string} [options.underflow=center] (top/bottom/center and left/right/center, or center) where to place world if too small for screen\n * @return {Viewport} this\n */\n public bounce(options: any): this\n {\n this.plugins.add('bounce', new Bounce(this, options));\n\n return this;\n }\n\n /**\n * Enable pinch to zoom and two-finger touch to drag\n *\n * @param {PinchOptions} [options]\n * @return {Viewport} this\n */\n public pinch(options: any): this\n {\n this.plugins.add('pinch', new Pinch(this, options));\n\n return this;\n }\n\n /**\n * Snap to a point\n *\n * @param {number} x\n * @param {number} y\n * @param {SnapOptions} [options]\n * @return {Viewport} this\n */\n public snap(x: number, y: number, options: any): this\n {\n this.plugins.add('snap', new Snap(this, x, y, options));\n\n return this;\n }\n\n /**\n * Follow a target\n *\n * NOTES:\n * uses the (x, y) as the center to follow; for PIXI.Sprite to work properly, use sprite.anchor.set(0.5)\n * options.acceleration is not perfect as it doesn't know the velocity of the target\n * it adds acceleration to the start of movement and deceleration to the end of movement when the target is stopped\n * fires 'moved' event\n * @param {PIXI.DisplayObject} target to follow\n * @param {FollowOptions} [options]\n * @returns {Viewport} this\n */\n public follow(target: DisplayObject, options: any): this\n {\n this.plugins.add('follow', new Follow(this, target, options));\n\n return this;\n }\n\n /**\n * Zoom using mouse wheel\n *\n * @param {WheelOptions} [options]\n * @return {Viewport} this\n */\n public wheel(options: any): this\n {\n this.plugins.add('wheel', new Wheel(this, options));\n\n return this;\n }\n\n /**\n * Animate the position and/or scale of the viewport\n *\n * @param {AnimateOptions} options\n * @returns {Viewport} this\n */\n public animate(options: any): this\n {\n this.plugins.add('animate', new Animate(this, options));\n\n return this;\n }\n\n /**\n * Enable clamping of zoom to constraints\n *\n * The minWidth/Height settings are how small the world can get (as it would appear on the screen)\n * before clamping. The maxWidth/maxHeight is how larger the world can scale (as it would appear on\n * the screen) before clamping.\n *\n * For example, if you have a world size of 1000 x 1000 and a screen size of 100 x 100, if you set\n * minWidth/Height = 100 then the world will not be able to zoom smaller than the screen size (ie,\n * zooming out so it appears smaller than the screen). Similarly, if you set maxWidth/Height = 100\n * the world will not be able to zoom larger than the screen size (ie, zooming in so it appears\n * larger than the screen).\n *\n * @param {ClampZoomOptions} [options]\n * @return {Viewport} this\n */\n public clampZoom(options: any): this\n {\n this.plugins.add('clamp-zoom', new ClampZoom(this, options));\n\n return this;\n }\n\n /**\n * Scroll viewport when mouse hovers near one of the edges or radius-distance from center of screen.\n *\n * NOTE: fires 'moved' event\n *\n * @param {MouseEdgesOptions} [options]\n */\n public mouseEdges(options: any): this\n {\n this.plugins.add('mouse-edges', new MouseEdges(this, options));\n\n return this;\n }\n\n /** Pause viewport (including animation updates such as decelerate) */\n get pause(): boolean\n {\n return !!this._pause;\n }\n set pause(value: boolean)\n {\n this._pause = value;\n\n this.lastViewport = null;\n this.moving = false;\n this.zooming = false;\n\n if (value)\n {\n this.input.pause();\n }\n }\n\n /**\n * Move the viewport so the bounding box is visible\n *\n * @param x - left\n * @param y - top\n * @param width\n * @param height\n * @param resizeToFit - Resize the viewport so the box fits within the viewport\n */\n public ensureVisible(x: number, y: number, width: number, height: number, resizeToFit?: boolean)\n {\n if (resizeToFit && (width > this.worldScreenWidth || height > this.worldScreenHeight))\n {\n this.fit(true, width, height);\n this.emit('zoomed', { viewport: this, type: 'ensureVisible' });\n }\n let moved = false;\n\n if (x < this.left)\n {\n this.left = x;\n moved = true;\n }\n else if (x + width > this.right)\n {\n this.right = x + width;\n moved = true;\n }\n if (y < this.top)\n {\n this.top = y;\n moved = true;\n }\n else if (y + height > this.bottom)\n {\n this.bottom = y + height;\n moved = true;\n }\n if (moved)\n {\n this.emit('moved', { viewport: this, type: 'ensureVisible' });\n }\n }\n}\n\n/**\n * Fires after a mouse or touch click\n * @event Viewport#clicked\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a drag starts\n * @event Viewport#drag-start\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a drag ends\n * @event Viewport#drag-end\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a pinch starts\n * @event Viewport#pinch-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a pinch end\n * @event Viewport#pinch-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap starts\n * @event Viewport#snap-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap ends\n * @event Viewport#snap-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap-zoom starts\n * @event Viewport#snap-zoom-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap-zoom ends\n * @event Viewport#snap-zoom-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce starts in the x direction\n * @event Viewport#bounce-x-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce ends in the x direction\n * @event Viewport#bounce-x-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce starts in the y direction\n * @event Viewport#bounce-y-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce ends in the y direction\n * @event Viewport#bounce-y-end\n * @type {Viewport}\n */\n\n/**\n * Fires when for a mouse wheel event\n * @event Viewport#wheel\n * @type {object}\n * @property {object} wheel\n * @property {number} wheel.dx\n * @property {number} wheel.dy\n * @property {number} wheel.dz\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a wheel-scroll occurs\n * @event Viewport#wheel-scroll\n * @type {Viewport}\n */\n\n/**\n * Fires when a mouse-edge starts to scroll\n * @event Viewport#mouse-edge-start\n * @type {Viewport}\n */\n\n/**\n * Fires when the mouse-edge scrolling ends\n * @event Viewport#mouse-edge-end\n * @type {Viewport}\n */\n\n/**\n * Fires when viewport moves through UI interaction, deceleration, ensureVisible, or follow\n * @event Viewport#moved\n * @type {object}\n * @property {Viewport} viewport\n * @property {string} type - (drag, snap, pinch, follow, bounce-x, bounce-y,\n * clamp-x, clamp-y, decelerate, mouse-edges, wheel, ensureVisible)\n */\n\n/**\n * Fires when viewport moves through UI interaction, deceleration, ensureVisible, or follow\n * @event Viewport#zoomed\n * @type {object}\n * @property {Viewport} viewport\n * @property {string} type (drag-zoom, pinch, wheel, clamp-zoom, ensureVisible)\n */\n\n/**\n * Fires when viewport stops moving\n * @event Viewport#moved-end\n * @type {Viewport}\n */\n\n/**\n * Fires when viewport stops zooming\n * @event Viewport#zoomed-end\n * @type {Viewport}\n */\n\n/**\n* Fires at the end of an update frame\n* @event Viewport#frame-end\n* @type {Viewport}\n*/\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAGA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC1FA;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACpBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wRACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;qgBC1RA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC3XA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC1RA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACpGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACrRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC1eA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACvJA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACxRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,8HACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC/JA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACxNA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACrRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC3NA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;ACrSA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACpRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;"} \ No newline at end of file +{"version":3,"file":"viewport.es.js","sources":["../../src/plugins/Plugin.ts","../../src/ease.ts","../../src/plugins/Animate.ts","../../src/plugins/Bounce.ts","../../src/plugins/Clamp.ts","../../src/plugins/ClampZoom.ts","../../src/plugins/Decelerate.ts","../../src/plugins/Drag.ts","../../src/plugins/Follow.ts","../../src/plugins/MouseEdges.ts","../../src/plugins/Pinch.ts","../../src/plugins/Snap.ts","../../src/plugins/SnapZoom.ts","../../src/plugins/Wheel.ts","../../src/InputManager.ts","../../src/PluginManager.ts","../../src/Viewport.ts"],"sourcesContent":["import type { InteractionEvent } from '@pixi/interaction';\nimport type { Viewport } from '../Viewport';\n\n/**\n * Derive this class to create user-defined plugins\n *\n * @public\n */\nexport class Plugin\n{\n /** The viewport to which this plugin is attached. */\n public readonly parent: Viewport;\n\n /**\n * Flags whether this plugin has been \"paused\".\n *\n * @see Plugin#pause\n * @see Plugin#resume\n */\n public paused: boolean;\n\n /** @param {Viewport} parent */\n constructor(parent: Viewport)\n {\n this.parent = parent;\n this.paused = false;\n }\n\n /** Called when plugin is removed */\n public destroy()\n {\n // Override for implementation\n }\n\n /** Handler for pointerdown PIXI event */\n public down(_e: InteractionEvent): boolean\n {\n return false;\n }\n\n /** Handler for pointermove PIXI event */\n public move(_e: InteractionEvent): boolean\n {\n return false;\n }\n\n /** Handler for pointerup PIXI event */\n public up(_e: InteractionEvent): boolean\n {\n return false;\n }\n\n /** Handler for wheel event on div */\n public wheel(_e: WheelEvent): boolean | undefined\n {\n return false;\n }\n\n /**\n * Called on each tick\n * @param {number} elapsed time in millisecond since last update\n */\n public update(_delta: number): void\n {\n // Override for implementation\n }\n\n /** Called when the viewport is resized */\n public resize()\n {\n // Override for implementation\n }\n\n /** Called when the viewport is manually moved */\n public reset(): void\n {\n // Override for implementation\n }\n\n /** Pause the plugin */\n public pause(): void\n {\n this.paused = true;\n }\n\n /** Un-pause the plugin */\n public resume(): void\n {\n this.paused = false;\n }\n}\n","// eslint-disable-next-line\n// @ts-expect-error Penner seems to have no typings.\nimport Penner from 'penner';\n\n/**\n * Returns correct Penner equation using string or Function.\n *\n * @internal\n * @ignore\n * @param {(function|string)} [ease]\n * @param {defaults} default penner equation to use if none is provided\n */\nexport default function ease(ease: any, defaults?: any): any\n{\n if (!ease)\n {\n return Penner[defaults]\n }\n else if (typeof ease === 'function')\n {\n return ease\n }\n else if (typeof ease === 'string')\n {\n return Penner[ease]\n }\n}","import { Point } from '@pixi/math';\nimport { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Animate}. */\nexport interface IAnimateOptions {\n /** Time to animate */\n time?: number;\n\n /** Position to move the viewport to */\n position?: Point;\n\n /**\n * Desired viewport width in world pixels\n *\n * (use instead of scale; aspect ratio is maintained if height is not provided)\n */\n width?: number;\n\n /**\n * Desired viewport height in world pixels\n *\n * (use instead of scale; aspect ratio is maintained if width is not provided)\n */\n height?: number;\n\n /** Scale to change zoom (scale.x = scale.y) */\n scale?: number;\n\n /** Independently change zoom in x-direction */\n scaleX?: number;\n\n /** Independently change zoom in y-direction */\n scaleY?: number;\n\n /** Easing function to use */\n ease?: any;\n\n /** Callback to invoke when the animation completes */\n callbackOnComplete?: (viewport: Viewport) => void;\n\n /** Removes this plugin if interrupted by any user input */\n removeOnInterrupt?: boolean;\n}\n\nconst DEFAULT_ANIMATE_OPTIONS = {\n removeOnInterrupt: false,\n ease: 'linear',\n time: 1000,\n};\n\n/**\n * Animation plugin.\n *\n * @see Viewport#animate\n * @fires animate-end\n */\nexport class Animate extends Plugin\n{\n public readonly options: IAnimateOptions & { ease: any; time: number };\n\n /** The starting x-coordinate of the viewport. */\n protected startX?: number;\n\n /** The starting y-coordinate of the viewport. */\n protected startY?: number;\n\n /** The change in the x-coordinate of the viewport through the animation.*/\n protected deltaX?: number;\n\n /** The change in the y-coordinate of the viewport through the animation. */\n protected deltaY?: number;\n\n /** Marks whether the center of the viewport is preserved in the animation. */\n protected keepCenter!: boolean;\n\n /** The starting viewport width. */\n protected startWidth: number | null = null;\n\n /** The starting viewport height. */\n protected startHeight: number | null = null;\n\n /** The change in the viewport's width through the animation. */\n protected deltaWidth: number | null = null;\n\n /** The change in the viewport's height through the animation. */\n protected deltaHeight: number | null = null;\n\n /** The viewport's width post-animation. */\n protected width: number | null = null;\n\n /** The viewport's height post-animation. */\n protected height: number | null = null;\n\n /** The time since the animation started. */\n protected time = 0;\n\n /**\n * This is called by {@link Viewport.animate}.\n *\n * @param parent\n * @param options\n */\n constructor(parent: Viewport, options: IAnimateOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_ANIMATE_OPTIONS, options);\n this.options.ease = ease(this.options.ease);\n\n this.setupPosition();\n this.setupZoom();\n\n this.time = 0;\n }\n\n /**\n * Setup `startX`, `startY`, `deltaX`, `deltaY`, `keepCenter`.\n *\n * This is called during construction.\n */\n protected setupPosition(): void\n {\n if (typeof this.options.position !== 'undefined')\n {\n this.startX = this.parent.center.x;\n this.startY = this.parent.center.y;\n this.deltaX = this.options.position.x - this.parent.center.x;\n this.deltaY = this.options.position.y - this.parent.center.y;\n this.keepCenter = false;\n }\n else\n {\n this.keepCenter = true;\n }\n }\n\n /**\n * Setup `startWidth, `startHeight`, `deltaWidth, `deltaHeight, `width`, `height`.\n *\n * This is called during construction.\n */\n protected setupZoom(): void\n {\n this.width = null;\n this.height = null;\n\n if (typeof this.options.scale !== 'undefined')\n {\n this.width = this.parent.screenWidth / this.options.scale;\n }\n else if (typeof this.options.scaleX !== 'undefined' || typeof this.options.scaleY !== 'undefined')\n {\n if (typeof this.options.scaleX !== 'undefined')\n {\n // screenSizeInWorldPixels = screenWidth / scale\n this.width = this.parent.screenWidth / this.options.scaleX;\n }\n if (typeof this.options.scaleY !== 'undefined')\n {\n this.height = this.parent.screenHeight / this.options.scaleY;\n }\n }\n else\n {\n if (typeof this.options.width !== 'undefined')\n {\n this.width = this.options.width;\n }\n if (typeof this.options.height !== 'undefined')\n {\n this.height = this.options.height;\n }\n }\n\n if (this.width !== null)\n {\n this.startWidth = this.parent.screenWidthInWorldPixels;\n this.deltaWidth = this.width - this.startWidth;\n }\n if (this.height !== null)\n {\n this.startHeight = this.parent.screenHeightInWorldPixels;\n this.deltaHeight = this.height - this.startHeight;\n }\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('animate');\n }\n\n return false;\n }\n\n public complete(): void\n {\n this.parent.plugins.remove('animate');\n if (this.width !== null)\n {\n this.parent.fitWidth(this.width, this.keepCenter, this.height === null);\n }\n if (this.height !== null)\n {\n this.parent.fitHeight(this.height, this.keepCenter, this.width === null);\n }\n if (!this.keepCenter)\n {\n this.parent.moveCenter(this.options.position as Point);\n }\n\n this.parent.emit('animate-end', this.parent);\n\n if (this.options.callbackOnComplete)\n {\n this.options.callbackOnComplete(this.parent);\n }\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n this.time += elapsed;\n\n if (this.time >= this.options.time)\n {\n this.complete();\n }\n else\n {\n const originalZoom = new Point(this.parent.scale.x, this.parent.scale.y);\n const percent = this.options.ease(this.time, 0, 1, this.options.time);\n\n if (this.width !== null)\n {\n const startWidth = this.startWidth as number;\n const deltaWidth = this.deltaWidth as number;\n\n this.parent.fitWidth(\n startWidth + (deltaWidth * percent),\n this.keepCenter,\n this.height === null);\n }\n if (this.height !== null)\n {\n const startHeight = this.startHeight as number;\n const deltaHeight = this.deltaHeight as number;\n\n this.parent.fitHeight(\n startHeight + (deltaHeight * percent),\n this.keepCenter,\n this.width === null);\n }\n if (this.width === null)\n {\n this.parent.scale.x = this.parent.scale.y;\n }\n else if (this.height === null)\n {\n this.parent.scale.y = this.parent.scale.x;\n }\n if (!this.keepCenter)\n {\n const startX = this.startX as number;\n const startY = this.startY as number;\n const deltaX = this.deltaX as number;\n const deltaY = this.deltaY as number;\n const original = new Point(this.parent.x, this.parent.y);\n\n this.parent.moveCenter(startX + (deltaX * percent), startY + (deltaY * percent));\n this.parent.emit('moved', { viewport: this.parent, original, type: 'animate' });\n }\n if (this.width || this.height)\n {\n this.parent.emit('zoomed', { viewport: this.parent, original: originalZoom, type: 'animate' });\n }\n }\n }\n}\n","import { Point, Rectangle } from '@pixi/math';\nimport { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Drag } from './Drag';\nimport type { IDecelerateOptions } from './Decelerate';\nimport type { Pinch } from './Pinch';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Bounce}. */\nexport interface IBounceOptions {\n /** \"all\", \"horizontal\", \"vertical\", or combination of \"top\", \"bottom\", \"right\", \"left\" (e.g., 'top-bottom-right') */\n sides?:\n 'all'\n | 'horizontal'\n | 'vertical'\n | string;\n\n /** Friction to apply to decelerate if active */\n friction?: number;\n\n /** Time in ms to finish bounce */\n time?: number;\n\n /** Use this bounceBox instead of (0, 0, viewport.worldWidth, viewport.worldHeight) */\n bounceBox?: Rectangle | null;\n\n /** Ease function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /** (top/bottom/center and left/right/center, or center) where to place world if too small for screen */\n underflow?: 'center' | string;\n}\n\n/** Bounce state along an axis */\nexport interface IBounceState {\n /** Elapsed time since bounce started */\n time: number;\n\n /** Starting coordinate */\n start: number;\n\n /** Change in coordinate through bounce */\n delta: number;\n\n /** Ending coordinate */\n end: number;\n}\n\nconst DEFAULT_BOUNCE_OPTIONS: Required = {\n sides: 'all',\n friction: 0.5,\n time: 150,\n ease: 'easeInOutSine',\n underflow: 'center',\n bounceBox: null\n};\n\n/**\n * @fires bounce-start-x\n * @fires bounce.end-x\n * @fires bounce-start-y\n * @fires bounce-end-y\n * @public\n */\nexport class Bounce extends Plugin\n{\n /** The options passed to initialize this plugin, cannot be modified again. */\n public readonly options: Readonly>;\n\n /** Holds whether to bounce from left side. */\n public readonly left: boolean ;\n\n /** Holds whether to bounce from top side. */\n public readonly top: boolean;\n\n /** Holds whether to bounce from right side. */\n public readonly right: boolean;\n\n /** Holds whether to bounce from bottom side. */\n public readonly bottom: boolean;\n\n /** Direction of underflow along x-axis. */\n public readonly underflowX: -1 | 0 | 1;\n\n /** Direction of underflow along y-axis. */\n public readonly underflowY: -1 | 0 | 1;\n\n /** Easing */\n protected ease: any;\n\n /** Bounce state along x-axis */\n protected toX!: IBounceState | null;\n\n /** Bounce state along y-axis */\n protected toY!: IBounceState | null;\n\n /**\n * This is called by {@link Viewport.bounce}.\n */\n constructor(parent: Viewport, options: IBounceOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_BOUNCE_OPTIONS, options);\n this.ease = ease(this.options.ease, 'easeInOutSine');\n\n if (this.options.sides)\n {\n if (this.options.sides === 'all')\n {\n this.top = this.bottom = this.left = this.right = true;\n }\n else if (this.options.sides === 'horizontal')\n {\n this.right = this.left = true;\n this.top = this.bottom = false;\n }\n else if (this.options.sides === 'vertical')\n {\n this.left = this.right = false;\n this.top = this.bottom = true;\n }\n else\n {\n this.top = this.options.sides.indexOf('top') !== -1;\n this.bottom = this.options.sides.indexOf('bottom') !== -1;\n this.left = this.options.sides.indexOf('left') !== -1;\n this.right = this.options.sides.indexOf('right') !== -1;\n }\n } else {\n this.left = this.top = this.right = this.bottom = false;\n }\n\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'center')\n {\n this.underflowX = 0;\n this.underflowY = 0;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n }\n\n this.reset();\n }\n\n public isActive(): boolean\n {\n return this.toX !== null || this.toY !== null;\n }\n\n public down(): boolean\n {\n this.toX = this.toY = null;\n\n return false;\n }\n\n public up(): boolean\n {\n this.bounce();\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n this.bounce();\n\n if (this.toX)\n {\n const toX = this.toX;\n\n toX.time += elapsed;\n this.parent.emit('moved', { viewport: this.parent, type: 'bounce-x' });\n\n if (toX.time >= this.options.time)\n {\n this.parent.x = toX.end;\n this.toX = null;\n this.parent.emit('bounce-x-end', this.parent);\n }\n else\n {\n this.parent.x = this.ease(toX.time, toX.start, toX.delta, this.options.time);\n }\n }\n\n if (this.toY)\n {\n const toY = this.toY;\n\n toY.time += elapsed;\n this.parent.emit('moved', { viewport: this.parent, type: 'bounce-y' });\n\n if (toY.time >= this.options.time)\n {\n this.parent.y = toY.end;\n this.toY = null;\n this.parent.emit('bounce-y-end', this.parent);\n }\n else\n {\n this.parent.y = this.ease(toY.time, toY.start, toY.delta, this.options.time);\n }\n }\n }\n\n /** @internal */\n protected calcUnderflowX(): number\n {\n let x: number;\n\n switch (this.underflowX)\n {\n case -1:\n x = 0;\n break;\n case 1:\n x = (this.parent.screenWidth - this.parent.screenWorldWidth);\n break;\n default:\n x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n }\n\n return x;\n }\n\n /** @internal */\n protected calcUnderflowY(): number\n {\n let y: number;\n\n switch (this.underflowY)\n {\n case -1:\n y = 0;\n break;\n case 1:\n y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n break;\n default:\n y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n }\n\n return y;\n }\n\n private oob(): Record<'left' | 'right' | 'top' | 'bottom', boolean> & Record<'topLeft' | 'bottomRight', Point>\n {\n const box = this.options.bounceBox;\n\n if (box)\n {\n const x1 = typeof box.x === 'undefined' ? 0 : box.x;\n const y1 = typeof box.y === 'undefined' ? 0 : box.y;\n const width = typeof box.width === 'undefined' ? this.parent.worldWidth : box.width;\n const height = typeof box.height === 'undefined' ? this.parent.worldHeight : box.height;\n\n return {\n left: this.parent.left < x1,\n right: this.parent.right > width,\n top: this.parent.top < y1,\n bottom: this.parent.bottom > height,\n topLeft: new Point(\n x1 * this.parent.scale.x,\n y1 * this.parent.scale.y\n ),\n bottomRight: new Point(\n width * this.parent.scale.x - this.parent.screenWidth,\n height * this.parent.scale.y - this.parent.screenHeight\n )\n };\n }\n\n return {\n left: this.parent.left < 0,\n right: this.parent.right > this.parent.worldWidth,\n top: this.parent.top < 0,\n bottom: this.parent.bottom > this.parent.worldHeight,\n topLeft: new Point(0, 0),\n bottomRight: new Point(\n this.parent.worldWidth * this.parent.scale.x - this.parent.screenWidth,\n this.parent.worldHeight * this.parent.scale.y - this.parent.screenHeight\n )\n };\n }\n\n public bounce(): void\n {\n if (this.paused)\n {\n return;\n }\n\n let oob;\n let decelerate: undefined | null | {\n percentChangeX?: number;\n percentChangeY?: number;\n x?: number;\n y?: number;\n options?: IDecelerateOptions\n } = this.parent.plugins.get('decelerate', true) as any;\n\n if (decelerate && (decelerate.x || decelerate.y))\n {\n if ((decelerate.x && decelerate.percentChangeX === decelerate.options?.friction) || (decelerate.y && decelerate.percentChangeY === decelerate.options?.friction))\n {\n oob = this.oob();\n if ((oob.left && this.left) || (oob.right && this.right))\n {\n decelerate.percentChangeX = this.options.friction;\n }\n if ((oob.top && this.top) || (oob.bottom && this.bottom))\n {\n decelerate.percentChangeY = this.options.friction;\n }\n }\n }\n const drag: Partial = this.parent.plugins.get('drag', true) || {};\n const pinch: Partial = this.parent.plugins.get('pinch', true) || {};\n\n decelerate = decelerate || {};\n\n if (!drag?.active && !pinch?.active && ((!this.toX || !this.toY) && (!decelerate.x || !decelerate.y)))\n {\n oob = oob || this.oob();\n const topLeft = oob.topLeft;\n const bottomRight = oob.bottomRight;\n\n if (!this.toX && !decelerate.x)\n {\n let x = null;\n\n if (oob.left && this.left)\n {\n x = (this.parent.screenWorldWidth < this.parent.screenWidth) ? this.calcUnderflowX() : -topLeft.x;\n }\n else if (oob.right && this.right)\n {\n x = (this.parent.screenWorldWidth < this.parent.screenWidth) ? this.calcUnderflowX() : -bottomRight.x;\n }\n if (x !== null && this.parent.x !== x)\n {\n this.toX = { time: 0, start: this.parent.x, delta: x - this.parent.x, end: x };\n this.parent.emit('bounce-x-start', this.parent);\n }\n }\n if (!this.toY && !decelerate.y)\n {\n let y = null;\n\n if (oob.top && this.top)\n {\n y = (this.parent.screenWorldHeight < this.parent.screenHeight) ? this.calcUnderflowY() : -topLeft.y;\n }\n else if (oob.bottom && this.bottom)\n {\n y = (this.parent.screenWorldHeight < this.parent.screenHeight) ? this.calcUnderflowY() : -bottomRight.y;\n }\n if (y !== null && this.parent.y !== y)\n {\n this.toY = { time: 0, start: this.parent.y, delta: y - this.parent.y, end: y };\n this.parent.emit('bounce-y-start', this.parent);\n }\n }\n }\n }\n\n public reset(): void\n {\n this.toX = this.toY = null;\n this.bounce();\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\n\n/**\n * There are three ways to clamp:\n * 1. direction: 'all' = the world is clamped to its world boundaries, ie, you cannot drag any part of the world offscreen\n * direction: 'x' | 'y' = only the x or y direction is clamped to its world boundary\n * 2. left, right, top, bottom = true | number = the world is clamped to the world's pixel location for each side;\n * if any of these are set to true, then the location is set to the boundary [0, viewport.worldWidth/viewport.worldHeight]\n * eg: to allow the world to be completely dragged offscreen, set [-viewport.worldWidth, -viewport.worldHeight, viewport.worldWidth * 2, viewport.worldHeight * 2]\n *\n * Underflow determines what happens when the world is smaller than the viewport\n * 1. none = the world is clamped but there is no special behavior\n * 2. center = the world is centered on the viewport\n * 3. combination of top/bottom/center and left/right/center (case insensitive) = the world is stuck to the appropriate boundaries\n *\n */\nexport interface IClampOptions\n{\n /**\n * Clamp left; true = 0\n *\n * @default false\n */\n left?: number | boolean | null;\n\n /**\n * Clamp top; true = 0\n *\n * @default false\n */\n top?: number | boolean | null;\n\n /**\n * Clamp right; true = viewport.worldWidth\n *\n * @default false\n */\n right?: number | boolean | null;\n\n /**\n * Clamp bottom; true = viewport.worldHeight\n *\n * @default false\n */\n bottom?: number | boolean | null;\n\n /**\n * (all, x, or y) using clamps of [0, viewport.worldWidth/viewport.worldHeight]; replaces left/right/top/bottom if set\n *\n * @default null\n */\n direction?: 'all' | 'x' | 'y' | null;\n\n /**\n * Where to place world if too small for screen (e.g., top-right, center, none, bottomleft)\n *\n * @default 'center'\n */\n underflow?: 'center' | string;\n}\n\nconst DEFAULT_CLAMP_OPTIONS: Required = {\n left: false,\n right: false,\n top: false,\n bottom: false,\n direction: null,\n underflow: 'center'\n};\n\n/**\n * Plugin to clamp the viewport to a specific world bounding box.\n *\n * @public\n */\nexport class Clamp extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Required;\n\n /** Last state of viewport */\n protected last: {\n x: number | null;\n y: number | null;\n scaleX: number | null;\n scaleY: number | null;\n };\n\n protected noUnderflow!: boolean;\n protected underflowX!: -1 | 0 | 1;\n protected underflowY!: -1 | 0 | 1;\n\n /**\n * This is called by {@link Viewport.clamp}.\n */\n constructor(parent: Viewport, options : IClampOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_CLAMP_OPTIONS, options);\n\n if (this.options.direction)\n {\n this.options.left = this.options.direction === 'x' || this.options.direction === 'all' ? true : null;\n this.options.right = this.options.direction === 'x' || this.options.direction === 'all' ? true : null;\n this.options.top = this.options.direction === 'y' || this.options.direction === 'all' ? true : null;\n this.options.bottom = this.options.direction === 'y' || this.options.direction === 'all' ? true : null;\n }\n\n this.parseUnderflow();\n this.last = { x: null, y: null, scaleX: null, scaleY: null };\n this.update();\n }\n\n private parseUnderflow()\n {\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'none')\n {\n this.noUnderflow = true;\n }\n else if (clamp === 'center')\n {\n this.underflowX = this.underflowY = 0;\n this.noUnderflow = false;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n this.noUnderflow = false;\n }\n }\n\n public move(): boolean\n {\n this.update();\n\n return false;\n }\n\n public update(): void\n {\n if (this.paused)\n {\n return;\n }\n\n // only clamp on change\n if (this.parent.x === this.last.x\n && this.parent.y === this.last.y\n && this.parent.scale.x === this.last.scaleX\n && this.parent.scale.y === this.last.scaleY)\n {\n return;\n }\n const original = { x: this.parent.x, y: this.parent.y };\n // TODO: Fix\n const decelerate: any = (this.parent.plugins as any).decelerate || {};\n\n if (this.options.left !== null || this.options.right !== null)\n {\n let moved = false;\n\n if (!this.noUnderflow && this.parent.screenWorldWidth < this.parent.screenWidth)\n {\n switch (this.underflowX)\n {\n case -1:\n if (this.parent.x !== 0)\n {\n this.parent.x = 0;\n moved = true;\n }\n break;\n case 1:\n if (this.parent.x !== this.parent.screenWidth - this.parent.screenWorldWidth)\n {\n this.parent.x = this.parent.screenWidth - this.parent.screenWorldWidth;\n moved = true;\n }\n break;\n default:\n if (this.parent.x !== (this.parent.screenWidth - this.parent.screenWorldWidth) / 2)\n {\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n moved = true;\n }\n }\n }\n else\n {\n if (this.options.left !== null)\n {\n if (this.parent.left < (this.options.left === true ? 0 : this.options.left))\n {\n this.parent.x = -(this.options.left === true ? 0 : this.options.left) * this.parent.scale.x;\n decelerate.x = 0;\n moved = true;\n }\n }\n if (this.options.right !== null)\n {\n if (this.parent.right > (this.options.right === true ? this.parent.worldWidth : this.options.right))\n {\n this.parent.x = -(this.options.right === true ? this.parent.worldWidth : this.options.right) * this.parent.scale.x + this.parent.screenWidth;\n decelerate.x = 0;\n moved = true;\n }\n }\n }\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, original, type: 'clamp-x' });\n }\n }\n if (this.options.top !== null || this.options.bottom !== null)\n {\n let moved = false;\n\n if (!this.noUnderflow && this.parent.screenWorldHeight < this.parent.screenHeight)\n {\n switch (this.underflowY)\n {\n case -1:\n if (this.parent.y !== 0)\n {\n this.parent.y = 0;\n moved = true;\n }\n break;\n case 1:\n if (this.parent.y !== this.parent.screenHeight - this.parent.screenWorldHeight)\n {\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n moved = true;\n }\n break;\n default:\n if (this.parent.y !== (this.parent.screenHeight - this.parent.screenWorldHeight) / 2)\n {\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n moved = true;\n }\n }\n }\n else\n {\n if (this.options.top !== null)\n {\n if (this.parent.top < (this.options.top === true ? 0 : this.options.top))\n {\n this.parent.y = -(this.options.top === true ? 0 : this.options.top)\n * this.parent.scale.y;\n decelerate.y = 0;\n moved = true;\n }\n }\n if (this.options.bottom !== null)\n {\n if (this.parent.bottom > (this.options.bottom === true ? this.parent.worldHeight : this.options.bottom))\n {\n this.parent.y = -(this.options.bottom === true ? this.parent.worldHeight : this.options.bottom)\n * this.parent.scale.y + this.parent.screenHeight;\n decelerate.y = 0;\n moved = true;\n }\n }\n }\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, original, type: 'clamp-y' });\n }\n }\n this.last.x = this.parent.x;\n this.last.y = this.parent.y;\n this.last.scaleX = this.parent.scale.x;\n this.last.scaleY = this.parent.scale.y;\n }\n\n public reset(): void\n {\n this.update();\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\n\n/**\n * Options for {@link ClampZoom}.\n *\n * Use either minimum width/height or minimum scale\n */\nexport interface IClampZoomOptions\n{\n /** Minimum width */\n minWidth?: number | null;\n\n /** Minimum height */\n minHeight?: number | null;\n\n /** Maximum width */\n maxWidth?: number | null;\n\n /** Maximum height */\n maxHeight?: number | null;\n\n /** Minimum scale */\n minScale?: number | null | IScale;\n\n /** Maximum scale */\n maxScale?: number | null | IScale;\n}\n\nconst DEFAULT_CLAMP_ZOOM_OPTIONS: Required = {\n minWidth: null,\n minHeight: null,\n maxWidth: null,\n maxHeight: null,\n minScale: null,\n maxScale: null\n};\n\n/**\n * Plugin to clamp the viewport's zoom to a specific range.\n *\n * @public\n */\nexport class ClampZoom extends Plugin\n{\n public readonly options: Required;\n\n /**\n * This is called by {@link Viewport.clampZoom}.\n */\n constructor(parent: Viewport, options = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_CLAMP_ZOOM_OPTIONS, options);\n\n this.clamp();\n }\n\n public resize(): void\n {\n this.clamp();\n }\n\n /** Clamp the viewport scale zoom) */\n public clamp(): void\n {\n if (this.paused)\n {\n return;\n }\n\n if (this.options.minWidth || this.options.minHeight || this.options.maxWidth || this.options.maxHeight)\n {\n let width = this.parent.worldScreenWidth;\n let height = this.parent.worldScreenHeight;\n\n if (this.options.minWidth !== null && width < this.options.minWidth)\n {\n const original = this.parent.scale.x;\n\n this.parent.fitWidth(this.options.minWidth, false, false, true);\n this.parent.scale.y *= this.parent.scale.x / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.maxWidth !== null && width > this.options.maxWidth)\n {\n const original = this.parent.scale.x;\n\n this.parent.fitWidth(this.options.maxWidth, false, false, true);\n this.parent.scale.y *= this.parent.scale.x / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.minHeight !== null && height < this.options.minHeight)\n {\n const original = this.parent.scale.y;\n\n this.parent.fitHeight(this.options.minHeight, false, false, true);\n this.parent.scale.x *= this.parent.scale.y / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.maxHeight !== null && height > this.options.maxHeight)\n {\n const original = this.parent.scale.y;\n\n this.parent.fitHeight(this.options.maxHeight, false, false, true);\n this.parent.scale.x *= this.parent.scale.y / original;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n }\n else\n if (this.options.minScale || this.options.maxScale)\n {\n const minScale: IScale = { x: null, y: null };\n const maxScale: IScale = { x: null, y: null };\n\n if (typeof this.options.minScale === 'number')\n {\n minScale.x = this.options.minScale;\n minScale.y = this.options.minScale;\n }\n else if (this.options.minScale !== null)\n {\n const optsMinScale = this.options.minScale as IScale;\n\n minScale.x = typeof optsMinScale.x === 'undefined' ? null : optsMinScale.x;\n minScale.y = typeof optsMinScale.y === 'undefined' ? null : optsMinScale.y;\n }\n\n if (typeof this.options.maxScale === 'number')\n {\n maxScale.x = this.options.maxScale;\n maxScale.y = this.options.maxScale;\n }\n else if (this.options.maxScale !== null)\n {\n const optsMaxScale = this.options.maxScale as IScale;\n\n maxScale.x = typeof optsMaxScale.x === 'undefined' ? null : optsMaxScale.x;\n maxScale.y = typeof optsMaxScale.y === 'undefined' ? null : optsMaxScale.y;\n }\n\n let scaleX = this.parent.scale.x;\n let scaleY = this.parent.scale.y;\n\n if (minScale.x !== null && scaleX < minScale.x)\n {\n scaleX = minScale.x;\n }\n if (maxScale.x !== null && scaleX > maxScale.x)\n {\n scaleX = maxScale.x;\n }\n if (minScale.y !== null && scaleY < minScale.y)\n {\n scaleY = minScale.y;\n }\n if (maxScale.y !== null && scaleY > maxScale.y)\n {\n scaleY = maxScale.y;\n }\n if (scaleX !== this.parent.scale.x || scaleY !== this.parent.scale.y)\n {\n this.parent.scale.set(scaleX, scaleY);\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n }\n }\n\n public reset(): void\n {\n this.clamp();\n }\n}\n\n/** This allows independent x and y values for min/maxScale */\nexport interface IScale {\n x: null | number\n y: null | number\n}\n","import { Plugin } from './Plugin';\n\nimport type { Point } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\nexport interface IDecelerateOptions {\n /**\n * Percent to decelerate after movement. This should be between 0 and 1, exclusive.\n *\n * @default 0.95\n */\n friction?: number;\n\n /**\n * Percent to decelerate when past boundaries (only applicable when viewport.bounce() is active)\n *\n * @default 0.8\n */\n bounce?: number;\n\n /**\n * Minimum velocity before stopping/reversing acceleration\n *\n * @default 0.01\n */\n minSpeed?: number;\n}\n\n/** Viewport position snapshot that's saved by {@link DeceleratePlugin} to estimate panning velocity. */\nexport interface IDecelerateSnapshot {\n /** x-coordinate of the viewport. */\n x: number;\n\n /** y-coordinate of the viewport. */\n y: number;\n\n /** Time at which this snapshot was taken. */\n time: number;\n}\n\nconst DEFAULT_DECELERATE_OPTIONS: Required = {\n friction: 0.98,\n bounce: 0.8,\n minSpeed: 0.01\n};\n\n/**\n * Time period of decay (1 frame)\n *\n * @internal\n * @ignore\n */\nconst TP = 16;\n\n/**\n * Plugin to decelerate viewport velocity smoothly after panning ends.\n *\n * @public\n */\nexport class Decelerate extends Plugin\n{\n /** Options used to initialize this plugin. */\n public readonly options: Required;\n\n /**\n * x-component of the velocity of viewport provided by this plugin, at the current time.\n *\n * This is measured in px/frame, where a frame is normalized to 16 milliseconds.\n */\n public x!: number | null;\n\n /**\n * y-component of the velocity of the viewport provided by this plugin, at the current time.\n *\n * This is measured in px/frame, where a frame is normalized to 16 milliseconds.\n */\n public y!: number | null;\n\n /**\n * The decay factor for the x-component of the viewport.\n *\n * The viewport's velocity decreased by this amount each 16 milliseconds.\n */\n public percentChangeX!: number;\n\n /**\n * The decay factor for the y-component of the viewport.\n *\n * The viewport's velocity decreased by this amount each 16 milliseconds.\n */\n public percentChangeY!: number;\n\n /** Saved list of recent viewport position snapshots, to estimate velocity. */\n protected saved: Array;\n\n /** The time since the user released panning of the viewport. */\n protected timeSinceRelease: number;\n\n /**\n * This is called by {@link Viewport.decelerate}.\n */\n constructor(parent: Viewport, options: IDecelerateOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_DECELERATE_OPTIONS, options);\n this.saved = [];\n this.timeSinceRelease = 0;\n\n this.reset();\n this.parent.on('moved', (data) => this.moved(data));\n }\n\n public down(): boolean\n {\n this.saved = [];\n this.x = this.y = null;\n\n return false;\n }\n\n public isActive(): boolean\n {\n return !!(this.x || this.y);\n }\n\n public move(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n const count = this.parent.input.count();\n\n if (count === 1 || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n this.saved.push({ x: this.parent.x, y: this.parent.y, time: performance.now() });\n\n if (this.saved.length > 60)\n {\n this.saved.splice(0, 30);\n }\n }\n\n // Silently recording viewport positions\n return false;\n }\n\n /** Listener to viewport's \"moved\" event. */\n protected moved(data: { type: 'clamp-x' | 'clamp-y'; original: Point }): void\n {\n if (this.saved.length)\n {\n const last = this.saved[this.saved.length - 1];\n\n if (data.type === 'clamp-x')\n {\n if (last.x === data.original.x)\n {\n last.x = this.parent.x;\n }\n }\n else if (data.type === 'clamp-y')\n {\n if (last.y === data.original.y)\n {\n last.y = this.parent.y;\n }\n }\n }\n }\n\n public up(): boolean\n {\n if (this.parent.input.count() === 0 && this.saved.length)\n {\n const now = performance.now();\n\n for (const save of this.saved)\n {\n if (save.time >= now - 100)\n {\n const time = now - save.time;\n\n this.x = (this.parent.x - save.x) / time;\n this.y = (this.parent.y - save.y) / time;\n this.percentChangeX = this.percentChangeY = this.options.friction;\n this.timeSinceRelease = 0;\n break;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Manually activate deceleration, starting from the (x, y) velocity components passed in the options.\n *\n * @param {object} options\n * @param {number} [options.x] - Specify x-component of initial velocity.\n * @param {number} [options.y] - Specify y-component of initial velocity.\n */\n public activate(options: { x?: number; y?: number; }): void\n {\n options = options || {};\n\n if (typeof options.x !== 'undefined')\n {\n this.x = options.x;\n this.percentChangeX = this.options.friction;\n }\n if (typeof options.y !== 'undefined')\n {\n this.y = options.y;\n this.percentChangeY = this.options.friction;\n }\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n /*\n * See https://github.com/davidfig/pixi-viewport/issues/271 for math.\n *\n * The viewport velocity (this.x, this.y) decays exponentially by the the decay factor\n * (this.percentChangeX, this.percentChangeY) each frame. This velocity function is integrated\n * to calculate the displacement.\n */\n\n const moved = this.x || this.y;\n\n const ti = this.timeSinceRelease;\n const tf = this.timeSinceRelease + elapsed;\n\n if (this.x)\n {\n const k = this.percentChangeX;\n const lnk = Math.log(k);\n\n // Apply velocity delta on the viewport x-coordinate.\n this.parent.x += ((this.x * TP) / lnk) * (Math.pow(k, tf / TP) - Math.pow(k, ti / TP));\n\n // Apply decay on x-component of velocity\n this.x *= Math.pow(this.percentChangeX, elapsed / TP);\n }\n if (this.y)\n {\n const k = this.percentChangeY;\n const lnk = Math.log(k);\n\n // Apply velocity delta on the viewport y-coordinate.\n this.parent.y += ((this.y * TP) / lnk) * (Math.pow(k, tf / TP) - Math.pow(k, ti / TP));\n\n // Apply decay on y-component of velocity\n this.y *= Math.pow(this.percentChangeY, elapsed / TP);\n }\n\n this.timeSinceRelease += elapsed;\n\n // End decelerate velocity once it goes under a certain amount of precision.\n if (Math.abs(this.x || 0) < this.options.minSpeed)\n {\n this.x = 0;\n }\n if (Math.abs(this.y || 0) < this.options.minSpeed)\n {\n this.y = 0;\n }\n\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, type: 'decelerate' });\n }\n }\n\n public reset(): void\n {\n this.x = this.y = null;\n }\n}\n","import { Point } from '@pixi/math';\nimport { Plugin } from './Plugin';\n\nimport type { Decelerate } from './Decelerate';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { IPointData } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Drag}. */\nexport interface IDragOptions {\n /**\n * direction to drag\n *\n * @default \"all\"\n */\n direction?: string;\n\n /**\n * whether click to drag is active\n *\n * @default true\n */\n pressDrag?: boolean;\n\n /**\n * Use wheel to scroll in direction (unless wheel plugin is active)\n *\n * @default true\n */\n wheel?: boolean;\n\n /**\n * number of pixels to scroll with each wheel spin\n *\n * @default 1\n */\n wheelScroll?: number;\n\n /**\n * reverse the direction of the wheel scroll\n *\n * @default false\n */\n reverse?: boolean;\n\n /**\n * clamp wheel(to avoid weird bounce with mouse wheel). Can be 'x' or 'y' or `true`.\n *\n * @default false\n */\n clampWheel?: boolean | string;\n\n /**\n * where to place world if too small for screen\n *\n * @default \"center\"\n */\n underflow?: string;\n\n /**\n * factor to multiply drag to increase the speed of movement\n *\n * @default 1\n */\n factor?: number;\n\n /**\n * Changes which mouse buttons trigger drag.\n *\n * Use: 'all', 'left', right' 'middle', or some combination, like, 'middle-right'; you may want to set\n * `viewport.options.disableOnContextMenu` if you want to use right-click dragging.\n *\n * @default \"all\"\n */\n mouseButtons?: 'all' | string;\n\n /**\n * Array containing {@link key|https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code} codes of\n * keys that can be pressed for the drag to be triggered, e.g.: ['ShiftLeft', 'ShiftRight'}.\n *\n * @default null\n */\n keyToPress?: string[] | null;\n\n /**\n * Ignore keyToPress for touch events.\n *\n * @default false\n */\n ignoreKeyToPressOnTouch?: boolean;\n\n /**\n * Scaling factor for non-DOM_DELTA_PIXEL scrolling events.\n *\n * @default 20\n */\n lineHeight?: number;\n}\n\nconst DEFAULT_DRAG_OPTIONS: Required = {\n direction: 'all',\n pressDrag: true,\n wheel: true,\n wheelScroll: 1,\n reverse: false,\n clampWheel: false,\n underflow: 'center',\n factor: 1,\n mouseButtons: 'all',\n keyToPress: null,\n ignoreKeyToPressOnTouch: false,\n lineHeight: 20,\n};\n\n/**\n * Plugin to enable panning/dragging of the viewport to move around.\n *\n * @public\n */\nexport class Drag extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Readonly>;\n\n /** Flags when viewport is moving. */\n protected moved: boolean;\n\n /** Factor to apply from {@link IDecelerateOptions}'s reverse. */\n protected reverse: 1 | -1;\n\n /** Holds whether dragging is enabled along the x-axis. */\n protected xDirection: boolean\n\n /** Holds whether dragging is enabled along the y-axis. */\n protected yDirection: boolean;\n\n /** Flags whether the keys required to drag are pressed currently. */\n protected keyIsPressed: boolean;\n\n /** Holds whether the left, center, and right buttons are required to pan. */\n protected mouse!: [boolean, boolean, boolean]\n\n /** Underflow factor along x-axis */\n protected underflowX!: -1 | 0 | 1;\n\n /** Underflow factor along y-axis */\n protected underflowY!: -1 | 0 | 1;\n\n /** Last pointer position while panning. */\n protected last?: IPointData | null\n\n /** The ID of the pointer currently panning the viewport. */\n protected current?: number;\n\n /**\n * This is called by {@link Viewport.drag}.\n */\n constructor(parent: Viewport, options = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_DRAG_OPTIONS, options);\n this.moved = false;\n this.reverse = this.options.reverse ? 1 : -1;\n this.xDirection = !this.options.direction || this.options.direction === 'all' || this.options.direction === 'x';\n this.yDirection = !this.options.direction || this.options.direction === 'all' || this.options.direction === 'y';\n this.keyIsPressed = false;\n\n this.parseUnderflow();\n this.mouseButtons(this.options.mouseButtons);\n\n if (this.options.keyToPress)\n {\n this.handleKeyPresses(this.options.keyToPress);\n }\n }\n\n /**\n * Handles keypress events and set the keyIsPressed boolean accordingly\n *\n * @param {array} codes - key codes that can be used to trigger drag event\n */\n protected handleKeyPresses(codes: string[]): void\n {\n window.addEventListener('keydown', (e) =>\n {\n if (codes.includes(e.code))\n { this.keyIsPressed = true; }\n });\n\n window.addEventListener('keyup', (e) =>\n {\n if (codes.includes(e.code))\n { this.keyIsPressed = false; }\n });\n }\n\n /**\n * initialize mousebuttons array\n * @param {string} buttons\n */\n protected mouseButtons(buttons: string): void\n {\n if (!buttons || buttons === 'all')\n {\n this.mouse = [true, true, true];\n }\n else\n {\n this.mouse = [\n buttons.indexOf('left') !== -1,\n buttons.indexOf('middle') !== -1,\n buttons.indexOf('right') !== -1\n ];\n }\n }\n\n protected parseUnderflow(): void\n {\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'center')\n {\n this.underflowX = 0;\n this.underflowY = 0;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n }\n }\n\n /**\n * @param {PIXI.InteractionEvent} event\n * @returns {boolean}\n */\n protected checkButtons(event: InteractionEvent): boolean\n {\n const isMouse = event.data.pointerType === 'mouse';\n const count = this.parent.input.count();\n\n if ((count === 1) || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n if (!isMouse || this.mouse[event.data.button])\n {\n return true;\n }\n }\n\n return false;\n }\n\n /**\n * @param {PIXI.InteractionEvent} event\n * @returns {boolean}\n */\n protected checkKeyPress(event: InteractionEvent): boolean\n {\n return (!this.options.keyToPress\n || this.keyIsPressed\n || (this.options.ignoreKeyToPressOnTouch && event.data.pointerType === 'touch'));\n }\n\n public down(event: InteractionEvent): boolean\n {\n if (this.paused || !this.options.pressDrag)\n {\n return false;\n }\n if (this.checkButtons(event) && this.checkKeyPress(event))\n {\n this.last = { x: event.data.global.x, y: event.data.global.y };\n this.current = event.data.pointerId;\n\n return true;\n }\n this.last = null;\n\n return false;\n }\n\n get active(): boolean\n {\n return this.moved;\n }\n\n public move(event: InteractionEvent): boolean\n {\n if (this.paused || !this.options.pressDrag)\n {\n return false;\n }\n if (this.last && this.current === event.data.pointerId)\n {\n const x = event.data.global.x;\n const y = event.data.global.y;\n const count = this.parent.input.count();\n\n if (count === 1 || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n const distX = x - this.last.x;\n const distY = y - this.last.y;\n\n if (this.moved\n || ((this.xDirection && this.parent.input.checkThreshold(distX))\n || (this.yDirection && this.parent.input.checkThreshold(distY))))\n {\n const newPoint = { x, y };\n\n if (this.xDirection)\n {\n this.parent.x += (newPoint.x - this.last.x) * this.options.factor;\n }\n if (this.yDirection)\n {\n this.parent.y += (newPoint.y - this.last.y) * this.options.factor;\n }\n this.last = newPoint;\n if (!this.moved)\n {\n this.parent.emit('drag-start', {\n event,\n screen: new Point(this.last.x, this.last.y),\n world: this.parent.toWorld(new Point(this.last.x, this.last.y)),\n viewport: this.parent\n });\n }\n this.moved = true;\n this.parent.emit('moved', { viewport: this.parent, type: 'drag' });\n\n return true;\n }\n }\n else\n {\n this.moved = false;\n }\n }\n\n return false;\n }\n\n public up(event: InteractionEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n const touches = this.parent.input.touches;\n\n if (touches.length === 1)\n {\n const pointer = touches[0];\n\n if (pointer.last)\n {\n this.last = { x: pointer.last.x, y: pointer.last.y };\n this.current = pointer.id;\n }\n this.moved = false;\n\n return true;\n }\n else if (this.last)\n {\n if (this.moved)\n {\n const screen = new Point(this.last.x, this.last.y);\n\n this.parent.emit('drag-end', {\n event, screen,\n world: this.parent.toWorld(screen),\n viewport: this.parent,\n });\n this.last = null;\n this.moved = false;\n\n return true;\n }\n }\n\n return false;\n }\n\n public wheel(event: WheelEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n if (this.options.wheel)\n {\n const wheel = this.parent.plugins.get('wheel', true);\n\n if (!wheel)\n {\n const step = event.deltaMode ? this.options.lineHeight : 1;\n\n if (this.xDirection)\n {\n this.parent.x += event.deltaX * step * this.options.wheelScroll * this.reverse;\n }\n if (this.yDirection)\n {\n this.parent.y += event.deltaY * step * this.options.wheelScroll * this.reverse;\n }\n if (this.options.clampWheel)\n {\n this.clamp();\n }\n this.parent.emit('wheel-scroll', this.parent);\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n if (!this.parent.options.passiveWheel)\n {\n event.preventDefault();\n }\n\n return true;\n }\n }\n\n return false;\n }\n\n public resume()\n {\n this.last = null;\n this.paused = false;\n }\n\n public clamp()\n {\n const decelerate: Partial = this.parent.plugins.get('decelerate', true) || {};\n\n if (this.options.clampWheel !== 'y')\n {\n if (this.parent.screenWorldWidth < this.parent.screenWidth)\n {\n switch (this.underflowX)\n {\n case -1:\n this.parent.x = 0;\n break;\n case 1:\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth);\n break;\n default:\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n }\n }\n else\n if (this.parent.left < 0)\n {\n this.parent.x = 0;\n decelerate.x = 0;\n }\n else if (this.parent.right > this.parent.worldWidth)\n {\n this.parent.x = -this.parent.worldWidth * this.parent.scale.x + this.parent.screenWidth;\n decelerate.x = 0;\n }\n }\n if (this.options.clampWheel !== 'x')\n {\n if (this.parent.screenWorldHeight < this.parent.screenHeight)\n {\n switch (this.underflowY)\n {\n case -1:\n this.parent.y = 0;\n break;\n case 1:\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n break;\n default:\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n }\n }\n else\n {\n if (this.parent.top < 0)\n {\n this.parent.y = 0;\n decelerate.y = 0;\n }\n if (this.parent.bottom > this.parent.worldHeight)\n {\n this.parent.y = -this.parent.worldHeight * this.parent.scale.y + this.parent.screenHeight;\n decelerate.y = 0;\n }\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { DisplayObject } from '@pixi/display';\nimport type { IPointData } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Follow}. */\nexport interface IFollowOptions\n{\n /**\n * Speed to follow in px/frame (0 = teleport to location)\n *\n * @default 9\n */\n speed?: number;\n\n /**\n * Set acceleration to accelerate and decelerate at this rate; speed cannot be 0 to use acceleration\n *\n * @default null\n */\n acceleration?: number | null;\n\n /**\n * Radius (in world coordinates) of center circle where movement is allowed without moving the viewport\n *\n * @default null\n */\n radius?: number | null;\n}\n\nconst DEFAULT_FOLLOW_OPTIONS: Required = {\n speed: 0,\n acceleration: null,\n radius: null\n};\n\n/**\n * Plugin to follow a display-object.\n *\n * @see Viewport.follow\n * @public\n */\nexport class Follow extends Plugin\n{\n /** The options used to initialize this plugin. */\n public readonly options: Required;\n\n /** The target this plugin will make the viewport follow. */\n public target: DisplayObject;\n\n /** The velocity provided the viewport by following, at the current time. */\n protected velocity: IPointData;\n\n /**\n * This is called by {@link Viewport.follow}.\n *\n * @param parent\n * @param target - target to follow\n * @param options\n */\n constructor(parent: Viewport, target: DisplayObject, options: IFollowOptions = {})\n {\n super(parent);\n\n this.target = target;\n this.options = Object.assign({}, DEFAULT_FOLLOW_OPTIONS, options);\n this.velocity = { x: 0, y: 0 };\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n const center = this.parent.center;\n let toX = this.target.x;\n let toY = this.target.y;\n\n if (this.options.radius)\n {\n const distance = Math.sqrt(Math.pow(this.target.y - center.y, 2) + Math.pow(this.target.x - center.x, 2));\n\n if (distance > this.options.radius)\n {\n const angle = Math.atan2(this.target.y - center.y, this.target.x - center.x);\n\n toX = this.target.x - (Math.cos(angle) * this.options.radius);\n toY = this.target.y - (Math.sin(angle) * this.options.radius);\n }\n else\n {\n return;\n }\n }\n\n const deltaX = toX - center.x;\n const deltaY = toY - center.y;\n\n if (deltaX || deltaY)\n {\n if (this.options.speed)\n {\n if (this.options.acceleration)\n {\n const angle = Math.atan2(toY - center.y, toX - center.x);\n const distance = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));\n\n if (distance)\n {\n const decelerationDistance = (Math.pow(this.velocity.x, 2) + Math.pow(this.velocity.y, 2)) / (2 * this.options.acceleration);\n\n if (distance > decelerationDistance)\n {\n this.velocity = {\n x: Math.min(this.velocity.x + this.options.acceleration * elapsed, this.options.speed),\n y: Math.min(this.velocity.y + this.options.acceleration * elapsed, this.options.speed)\n };\n }\n else\n {\n this.velocity = {\n x: Math.max(this.velocity.x - this.options.acceleration * this.options.speed, 0),\n y: Math.max(this.velocity.y - this.options.acceleration * this.options.speed, 0)\n };\n }\n const changeX = Math.cos(angle) * this.velocity.x;\n const changeY = Math.sin(angle) * this.velocity.y;\n const x = Math.abs(changeX) > Math.abs(deltaX) ? toX : center.x + changeX;\n const y = Math.abs(changeY) > Math.abs(deltaY) ? toY : center.y + changeY;\n\n this.parent.moveCenter(x, y);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n else\n {\n const angle = Math.atan2(toY - center.y, toX - center.x);\n const changeX = Math.cos(angle) * this.options.speed;\n const changeY = Math.sin(angle) * this.options.speed;\n const x = Math.abs(changeX) > Math.abs(deltaX) ? toX : center.x + changeX;\n const y = Math.abs(changeY) > Math.abs(deltaY) ? toY : center.y + changeY;\n\n this.parent.moveCenter(x, y);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n else\n {\n this.parent.moveCenter(toX, toY);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\nimport type { InteractionEvent } from '@pixi/interaction';\n\n/** Insets for mouse edges scrolling regions */\nexport interface IMouseEdgesInsets {\n /** Distance from center of screen in screen pixels */\n radius?: number | null;\n\n /** Distance from all sides in screen pixels */\n distance?: number | null;\n\n /** Alternatively, set top distance (leave unset for no top scroll) */\n top?: number | null;\n\n /** Alternatively, set bottom distance (leave unset for no top scroll) */\n bottom?: number | null;\n\n /** Alternatively, set left distance (leave unset for no top scroll) */\n left?: number | null;\n\n /** Alternatively, set right distance (leave unset for no top scroll) */\n right?: number | null;\n}\n\n/** Options for {@link MouseEdges}. */\nexport interface IMouseEdgesOptions extends IMouseEdgesInsets {\n /** Speed in pixels/frame to scroll viewport */\n speed?: number;\n\n /** Reverse direction of scroll */\n reverse?: boolean;\n\n /** Don't use decelerate plugin even if it's installed */\n noDecelerate?: boolean;\n\n /**\n * If using radius, use linear movement (+/- 1, +/- 1) instead of angled movement.\n *\n * (Math.cos(angle from center), Math.sin(angle from center))\n */\n linear?: boolean;\n\n /** Allows plugin to continue working even when there's a `mousedown` event. */\n allowButtons?: boolean;\n}\n\nconst MOUSE_EDGES_OPTIONS: Required = {\n radius: null,\n distance: null,\n top: null,\n bottom: null,\n left: null,\n right: null,\n speed: 8,\n reverse: false,\n noDecelerate: false,\n linear: false,\n allowButtons: false,\n};\n\n/**\n * Scroll viewport when mouse hovers near one of the edges.\n *\n * @event mouse-edge-start(Viewport) emitted when mouse-edge starts\n * @event mouse-edge-end(Viewport) emitted when mouse-edge ends\n */\nexport class MouseEdges extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Readonly>;\n\n /** Factor from reverse option. */\n protected readonly reverse: -1 | 1;\n\n /** Radius squared */\n protected readonly radiusSquared: number | null;\n\n /** Scroll region size on the left side. */\n protected left!: number | null;\n\n /** Scroll region size on the top size. */\n protected top!: number | null;\n\n /** Scroll region size on the right side. */\n protected right!: number | null;\n\n /** Scroll region size on the bottom side. */\n protected bottom!: number | null;\n\n protected horizontal?: number | null;\n\n protected vertical?: number | null;\n\n /**\n * This is called by {@link Viewport.mouseEdges}.\n */\n constructor(parent: Viewport, options: IMouseEdgesOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, MOUSE_EDGES_OPTIONS, options);\n this.reverse = this.options.reverse ? 1 : -1;\n this.radiusSquared = typeof this.options.radius === 'number' ? Math.pow(this.options.radius, 2) : null;\n\n this.resize();\n }\n\n public resize(): void\n {\n const distance = this.options.distance;\n\n if (distance !== null)\n {\n this.left = distance;\n this.top = distance;\n this.right = this.parent.worldScreenWidth - distance;\n this.bottom = this.parent.worldScreenHeight - distance;\n }\n else if (!this.options.radius)\n {\n this.left = this.options.left;\n this.top = this.options.top;\n this.right = this.options.right === null ? null : this.parent.worldScreenWidth - this.options.right;\n this.bottom = this.options.bottom === null ? null : this.parent.worldScreenHeight - this.options.bottom;\n }\n }\n\n public down(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if (!this.options.allowButtons)\n {\n this.horizontal = this.vertical = null;\n }\n\n return false;\n }\n\n public move(event: InteractionEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if ((event.data.pointerType !== 'mouse' && event.data.identifier !== 1)\n || (!this.options.allowButtons && event.data.buttons !== 0))\n {\n return false;\n }\n\n const x = event.data.global.x;\n const y = event.data.global.y;\n\n if (this.radiusSquared)\n {\n const center = this.parent.toScreen(this.parent.center);\n const distance = Math.pow(center.x - x, 2) + Math.pow(center.y - y, 2);\n\n if (distance >= this.radiusSquared)\n {\n const angle = Math.atan2(center.y - y, center.x - x);\n\n if (this.options.linear)\n {\n this.horizontal = Math.round(Math.cos(angle)) * this.options.speed * this.reverse * (60 / 1000);\n this.vertical = Math.round(Math.sin(angle)) * this.options.speed * this.reverse * (60 / 1000);\n }\n else\n {\n this.horizontal = Math.cos(angle) * this.options.speed * this.reverse * (60 / 1000);\n this.vertical = Math.sin(angle) * this.options.speed * this.reverse * (60 / 1000);\n }\n }\n else\n {\n if (this.horizontal)\n {\n this.decelerateHorizontal();\n }\n if (this.vertical)\n {\n this.decelerateVertical();\n }\n\n this.horizontal = this.vertical = 0;\n }\n }\n else\n {\n if (this.left !== null && x < this.left)\n {\n this.horizontal = Number(this.reverse) * this.options.speed * (60 / 1000);\n }\n else if (this.right !== null && x > this.right)\n {\n this.horizontal = -1 * this.reverse * this.options.speed * (60 / 1000);\n }\n else\n {\n this.decelerateHorizontal();\n this.horizontal = 0;\n }\n if (this.top !== null && y < this.top)\n {\n this.vertical = Number(this.reverse) * this.options.speed * (60 / 1000);\n }\n else if (this.bottom !== null && y > this.bottom)\n {\n this.vertical = -1 * this.reverse * this.options.speed * (60 / 1000);\n }\n else\n {\n this.decelerateVertical();\n this.vertical = 0;\n }\n }\n\n return false;\n }\n\n private decelerateHorizontal(): void\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (this.horizontal && decelerate && !this.options.noDecelerate)\n {\n decelerate.activate({ x: (this.horizontal * this.options.speed * this.reverse) / (1000 / 60) });\n }\n }\n\n private decelerateVertical(): void\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (this.vertical && decelerate && !this.options.noDecelerate)\n {\n decelerate.activate({ y: (this.vertical * this.options.speed * this.reverse) / (1000 / 60) });\n }\n }\n\n public up(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if (this.horizontal)\n {\n this.decelerateHorizontal();\n }\n if (this.vertical)\n {\n this.decelerateVertical();\n }\n this.horizontal = this.vertical = null;\n\n return false;\n }\n\n public update(): void\n {\n if (this.paused)\n {\n return;\n }\n\n if (this.horizontal || this.vertical)\n {\n const center = this.parent.center;\n\n if (this.horizontal)\n {\n center.x += this.horizontal * this.options.speed;\n }\n if (this.vertical)\n {\n center.y += this.vertical * this.options.speed;\n }\n\n this.parent.moveCenter(center);\n this.parent.emit('moved', { viewport: this.parent, type: 'mouse-edges' });\n }\n }\n}\n","import { Plugin } from './Plugin';\nimport { Point } from '@pixi/math';\n\nimport type { IPointData } from '@pixi/math';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { IViewportTouch } from '../InputManager';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Pinch}. */\nexport interface IPinchOptions\n{\n /** Disable two-finger dragging. */\n noDrag?: boolean;\n\n /**\n * Percent to modify pinch speed.\n *\n * @default 1\n */\n percent?: number;\n\n /**\n * Factor to multiply two-finger drag to increase speed of movement\n *\n * @default 1\n */\n factor?: number;\n\n /** Place this point at center during zoom instead of center of two fingers */\n center?: Point | null;\n\n /** Axis to zoom */\n axis?: 'all' | 'x' | 'y';\n}\n\nconst DEFAULT_PINCH_OPTIONS: Required = {\n noDrag: false,\n percent: 1,\n center: null,\n factor: 1,\n axis: 'all',\n};\n\n/**\n * Plugin for enabling two-finger pinching (or dragging).\n *\n * @public\n */\nexport class Pinch extends Plugin\n{\n /** Options used to initialize this plugin. */\n public readonly options: Required;\n\n /** Flags whether this plugin is active, i.e. a pointer is down on the viewport. */\n public active = false;\n\n /** Flags whether the viewport is being pinched. */\n public pinching = false;\n\n protected moved = false;\n protected lastCenter?: IPointData | null;\n\n /**\n * This is called by {@link Viewport.pinch}.\n */\n constructor(parent: Viewport, options: IPinchOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_PINCH_OPTIONS, options);\n }\n\n public down(): boolean\n {\n if (this.parent.input.count() >= 2)\n {\n this.active = true;\n return true;\n }\n\n return false;\n }\n\n public isAxisX(): boolean\n {\n return ['all', 'x'].includes(this.options.axis);\n }\n\n public isAxisY(): boolean\n {\n return ['all', 'y'].includes(this.options.axis);\n }\n\n public move(e: InteractionEvent): boolean\n {\n if (this.paused || !this.active)\n {\n return false;\n }\n\n const x = e.data.global.x;\n const y = e.data.global.y;\n\n const pointers = this.parent.input.touches;\n\n if (pointers.length >= 2)\n {\n const first = pointers[0] as IViewportTouch;\n const second = pointers[1] as IViewportTouch;\n const last = (first.last && second.last)\n ? Math.sqrt(Math.pow(second.last.x - first.last.x, 2) + Math.pow(second.last.y - first.last.y, 2))\n : null;\n\n if (first.id === e.data.pointerId)\n {\n first.last = { x, y, data: e.data } as IPointData;\n }\n else if (second.id === e.data.pointerId)\n {\n second.last = { x, y, data: e.data } as IPointData;\n }\n if (last)\n {\n let oldPoint: IPointData | undefined;\n\n const point = {\n x: (first.last as IPointData).x +\n ((second.last as IPointData).x - (first.last as IPointData).x) / 2,\n y: (first.last as IPointData).y +\n ((second.last as IPointData).y - (first.last as IPointData).y) / 2,\n };\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point);\n }\n let dist = Math.sqrt(Math.pow(\n (second.last as IPointData).x - (first.last as IPointData).x, 2) +\n Math.pow((second.last as IPointData).y - (first.last as IPointData).y, 2));\n\n dist = dist === 0 ? dist = 0.0000000001 : dist;\n\n const change = (1 - last / dist) * this.options.percent\n * (this.isAxisX() ? this.parent.scale.x : this.parent.scale.y);\n\n if (this.isAxisX())\n {\n this.parent.scale.x += change;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y += change;\n }\n\n this.parent.emit('zoomed', { viewport: this.parent, type: 'pinch', center: point });\n\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += (point.x - newPoint.x) * this.options.factor;\n this.parent.y += (point.y - newPoint.y) * this.options.factor;\n this.parent.emit('moved', { viewport: this.parent, type: 'pinch' });\n }\n if (!this.options.noDrag && this.lastCenter)\n {\n this.parent.x += (point.x - this.lastCenter.x) * this.options.factor;\n this.parent.y += (point.y - this.lastCenter.y) * this.options.factor;\n this.parent.emit('moved', { viewport: this.parent, type: 'pinch' });\n }\n\n this.lastCenter = point;\n this.moved = true;\n }\n else if (!this.pinching)\n {\n this.parent.emit('pinch-start', this.parent);\n this.pinching = true;\n }\n\n return true;\n }\n\n return false;\n }\n\n public up(): boolean\n {\n if (this.pinching)\n {\n if (this.parent.input.touches.length <= 1)\n {\n this.active = false;\n this.lastCenter = null;\n this.pinching = false;\n this.moved = false;\n this.parent.emit('pinch-end', this.parent);\n\n return true;\n }\n }\n\n return false;\n }\n}\n","import { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Viewport } from '../Viewport';\n\nexport interface ISnapOptions\n{\n /** snap to the top-left of viewport instead of center */\n topLeft?: boolean;\n\n /**\n * Friction/frame to apply if decelerate is active\n *\n * @default 0.8\n */\n friction?: number;\n\n /**\n * @default 1000\n */\n time?: number;\n\n /** Easing function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /**\n * Pause snapping with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Removes this plugin after snapping is complete\n *\n * @default false\n */\n removeOnComplete?: boolean;\n\n /**\n * Removes this plugin if interrupted by any user input\n *\n * @default false\n */\n removeOnInterrupt?: boolean;\n\n /**\n * Starts the snap immediately regardless of whether the viewport is at the desired location\n *\n * @default false\n */\n forceStart?: boolean;\n}\n\nconst DEFAULT_SNAP_OPTIONS: Required = {\n topLeft: false,\n friction: 0.8,\n time: 1000,\n ease: 'easeInOutSine',\n interrupt: true,\n removeOnComplete: false,\n removeOnInterrupt: false,\n forceStart: false\n};\n\n/**\n * @event snap-start(Viewport) emitted each time a snap animation starts\n * @event snap-restart(Viewport) emitted each time a snap resets because of a change in viewport size\n * @event snap-end(Viewport) emitted each time snap reaches its target\n * @event snap-remove(Viewport) emitted if snap plugin is removed\n */\nexport class Snap extends Plugin\n{\n public readonly options: Required;\n public ease?: any;\n public x: number;\n public y: number;\n\n protected percent?: number;\n protected snapping?: { time: number } | null;\n protected deltaX?: number;\n protected deltaY?: number;\n protected startX?: number;\n protected startY?: number;\n\n /**\n * This is called by {@link Viewport.snap}.\n */\n constructor(parent: Viewport, x: number, y: number, options: ISnapOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_SNAP_OPTIONS, options);\n this.ease = ease(options.ease, 'easeInOutSine');\n this.x = x;\n this.y = y;\n\n if (this.options.forceStart)\n {\n this.snapStart();\n }\n }\n\n public snapStart(): void\n {\n this.percent = 0;\n this.snapping = { time: 0 };\n const current = this.options.topLeft ? this.parent.corner : this.parent.center;\n\n this.deltaX = this.x - current.x;\n this.deltaY = this.y - current.y;\n this.startX = current.x;\n this.startY = current.y;\n this.parent.emit('snap-start', this.parent);\n }\n\n public wheel(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap');\n }\n\n return false;\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap');\n }\n else if (this.options.interrupt)\n {\n this.snapping = null;\n }\n\n return false;\n }\n\n public up(): boolean\n {\n if (this.parent.input.count() === 0)\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (decelerate && (decelerate.x || decelerate.y))\n {\n decelerate.percentChangeX = decelerate.percentChangeY = this.options.friction;\n }\n }\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n if (this.options.interrupt && this.parent.input.count() !== 0)\n {\n return;\n }\n if (!this.snapping)\n {\n const current = this.options.topLeft ? this.parent.corner : this.parent.center;\n\n if (current.x !== this.x || current.y !== this.y)\n {\n this.snapStart();\n }\n }\n else\n {\n const snapping = this.snapping;\n\n snapping.time += elapsed;\n let finished;\n let x;\n let y;\n\n const startX = this.startX as number;\n const startY = this.startY as number;\n const deltaX = this.deltaX as number;\n const deltaY = this.deltaY as number;\n\n if (snapping.time > this.options.time)\n {\n finished = true;\n x = startX + deltaX;\n y = startY + deltaY;\n }\n else\n {\n const percent = this.ease(snapping.time, 0, 1, this.options.time);\n\n x = startX + (deltaX * percent);\n y = startY + (deltaY * percent);\n }\n if (this.options.topLeft)\n {\n this.parent.moveCorner(x, y);\n }\n else\n {\n this.parent.moveCenter(x, y);\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'snap' });\n\n if (finished)\n {\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap');\n }\n this.parent.emit('snap-end', this.parent);\n this.snapping = null;\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Point } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link SnapZoom}. */\nexport interface ISnapZoomOptions\n{\n /** the desired width to snap (to maintain aspect ratio, choose only width or height) */\n width?: number;\n\n /** the desired height to snap (to maintain aspect ratio, choose only width or height) */\n height?: number;\n\n /**\n * time for snapping in ms\n *\n * @default 1000\n */\n time?: number;\n\n /** ease function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /** Place this point at center during zoom instead of center of the viewport */\n center?: Point | null;\n\n /**\n * Pause snapping with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Removes this plugin after snapping is complete\n *\n * @default false\n */\n removeOnComplete?: boolean;\n\n /**\n * Removes this plugin if interrupted by any user input\n *\n * @default false\n */\n removeOnInterrupt?: boolean;\n\n /**\n * Starts the snap immediately regardless of whether the viewport is at the desired zoom\n *\n * @default false\n */\n forceStart?: boolean;\n\n /**\n * Zoom but do not move\n *\n * @default false\n */\n noMove?: boolean;\n}\n\nconst DEFAULT_SNAP_ZOOM_OPTIONS: Required = {\n width: 0,\n height: 0,\n time: 1000,\n ease: 'easeInOutSine',\n center: null,\n interrupt: true,\n removeOnComplete: false,\n removeOnInterrupt: false,\n forceStart: false,\n noMove: false\n};\n\n/**\n * @event snap-zoom-start(Viewport) emitted each time a fit animation starts\n * @event snap-zoom-end(Viewport) emitted each time fit reaches its target\n * @event snap-zoom-end(Viewport) emitted each time fit reaches its target\n */\nexport class SnapZoom extends Plugin\n{\n public readonly options: Required;\n\n protected ease: any;\n protected xScale: number;\n protected yScale: number;\n protected xIndependent: boolean;\n protected yIndependent: boolean;\n protected snapping?: {\n time: number;\n startX: number;\n startY: number;\n deltaX: number;\n deltaY: number;\n } | null;\n\n /**\n * This is called by {@link Viewport.snapZoom}.\n */\n constructor(parent: Viewport, options: ISnapZoomOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_SNAP_ZOOM_OPTIONS, options);\n this.ease = ease(this.options.ease);\n\n // Assign defaults for typescript.\n this.xIndependent = false;\n this.yIndependent = false;\n this.xScale = 0;\n this.yScale = 0;\n\n if (this.options.width > 0)\n {\n this.xScale = parent.screenWidth / this.options.width;\n this.xIndependent = true;\n }\n if (this.options.height > 0)\n {\n this.yScale = parent.screenHeight / this.options.height;\n this.yIndependent = true;\n }\n\n this.xScale = this.xIndependent ? (this.xScale as number) : (this.yScale as number);\n this.yScale = this.yIndependent ? (this.yScale as number) : this.xScale;\n\n if (this.options.time === 0)\n {\n // TODO: Fix this\n // @ts-expect-error todo\n parent.container.scale.x = this.xScale;\n\n // TODO: Fix this\n // @ts-expect-error todo\n parent.container.scale.y = this.yScale;\n\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n }\n else if (options.forceStart)\n {\n this.createSnapping();\n }\n }\n\n private createSnapping(): void\n {\n const startWorldScreenWidth = this.parent.worldScreenWidth;\n const startWorldScreenHeight = this.parent.worldScreenHeight;\n const endWorldScreenWidth = this.parent.screenWidth / this.xScale;\n const endWorldScreenHeight = this.parent.screenHeight / this.yScale;\n\n this.snapping = {\n time: 0,\n startX: startWorldScreenWidth,\n startY: startWorldScreenHeight,\n deltaX: endWorldScreenWidth - startWorldScreenWidth,\n deltaY: endWorldScreenHeight - startWorldScreenHeight\n };\n\n this.parent.emit('snap-zoom-start', this.parent);\n }\n\n public resize(): void\n {\n this.snapping = null;\n\n if (this.options.width > 0)\n {\n this.xScale = this.parent.screenWidth / this.options.width;\n }\n if (this.options.height > 0)\n {\n this.yScale = this.parent.screenHeight / this.options.height;\n }\n this.xScale = this.xIndependent ? this.xScale : this.yScale;\n this.yScale = this.yIndependent ? this.yScale : this.xScale;\n }\n\n public wheel(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n\n return false;\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n else if (this.options.interrupt)\n {\n this.snapping = null;\n }\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n if (this.options.interrupt && this.parent.input.count() !== 0)\n {\n return;\n }\n\n let oldCenter: Point | undefined;\n\n if (!this.options.center && !this.options.noMove)\n {\n oldCenter = this.parent.center;\n }\n if (!this.snapping)\n {\n if (this.parent.scale.x !== this.xScale || this.parent.scale.y !== this.yScale)\n {\n this.createSnapping();\n }\n }\n else if (this.snapping)\n {\n const snapping = this.snapping;\n\n snapping.time += elapsed;\n\n if (snapping.time >= this.options.time)\n {\n this.parent.scale.set(this.xScale, this.yScale);\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n this.parent.emit('snap-zoom-end', this.parent);\n this.snapping = null;\n }\n else\n {\n const snapping = this.snapping;\n const worldScreenWidth = this.ease(snapping.time, snapping.startX, snapping.deltaX, this.options.time);\n const worldScreenHeight = this.ease(snapping.time, snapping.startY, snapping.deltaY, this.options.time);\n\n this.parent.scale.x = this.parent.screenWidth / worldScreenWidth;\n this.parent.scale.y = this.parent.screenHeight / worldScreenHeight;\n }\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (!this.options.noMove)\n {\n if (!this.options.center)\n {\n this.parent.moveCenter(oldCenter as Point);\n }\n else\n {\n this.parent.moveCenter(this.options.center);\n }\n }\n }\n }\n\n public resume(): void\n {\n this.snapping = null;\n super.resume();\n }\n}\n","import { Plugin } from './Plugin';\nimport { IPointData, Point } from '@pixi/math';\n\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Wheel}. */\nexport interface IWheelOptions\n{\n /**\n * Percent to scroll with each spin\n *\n * @default 0.1\n */\n percent?: number;\n\n /**\n * smooth the zooming by providing the number of frames to zoom between wheel spins\n *\n * @default false\n */\n smooth?: false | number;\n\n /**\n * Stop smoothing with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Reverse the direction of the scroll\n *\n * @default false\n */\n reverse?: boolean;\n\n /**\n * Place this point at center during zoom instead of current mouse position\n *\n * @default null\n */\n center?: Point | null;\n\n /**\n * Scaling factor for non-DOM_DELTA_PIXEL scrolling events\n *\n * @default 20\n */\n lineHeight?: number;\n\n /**\n * Axis to zoom\n *\n * @default 'all'\n */\n axis?: 'all' | 'x' | 'y';\n}\n\nconst DEFAULT_WHEEL_OPTIONS: Required = {\n percent: 0.1,\n smooth: false,\n interrupt: true,\n reverse: false,\n center: null,\n lineHeight: 20,\n axis: 'all',\n};\n\n/**\n * Plugin for handling wheel scrolling for viewport zoom.\n *\n * @event wheel({wheel: {dx, dy, dz}, event, viewport})\n */\nexport class Wheel extends Plugin\n{\n public readonly options: Required;\n\n protected smoothing?: IPointData | null;\n protected smoothingCenter?: Point | null;\n protected smoothingCount?: number;\n\n /**\n * This is called by {@link Viewport.wheel}.\n */\n constructor(parent: Viewport, options: IWheelOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_WHEEL_OPTIONS, options);\n }\n\n public down(): boolean\n {\n if (this.options.interrupt)\n {\n this.smoothing = null;\n }\n\n return false;\n }\n\n protected isAxisX(): boolean\n {\n return ['all', 'x'].includes(this.options.axis);\n }\n\n protected isAxisY(): boolean\n {\n return ['all', 'y'].includes(this.options.axis);\n }\n\n public update(): void\n {\n if (this.smoothing)\n {\n const point = this.smoothingCenter;\n const change = this.smoothing;\n let oldPoint;\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point as IPointData);\n }\n if (this.isAxisX())\n {\n this.parent.scale.x += change.x;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y += change.y;\n }\n\n this.parent.emit('zoomed', { viewport: this.parent, type: 'wheel' });\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += (point as IPointData).x - newPoint.x;\n this.parent.y += (point as IPointData).y - newPoint.y;\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n (this.smoothingCount as number)++;\n\n if (this.smoothingCount as number >= this.options.smooth)\n {\n this.smoothing = null;\n }\n }\n }\n\n public wheel(e: WheelEvent): boolean | undefined\n {\n if (this.paused)\n {\n return;\n }\n\n const point = this.parent.input.getPointerPosition(e);\n const sign = this.options.reverse ? -1 : 1;\n const step = sign * -e.deltaY * (e.deltaMode ? this.options.lineHeight : 1) / 500;\n const change = Math.pow(2, (1 + this.options.percent) * step);\n\n if (this.options.smooth)\n {\n const original = {\n x: this.smoothing ? this.smoothing.x * (this.options.smooth - (this.smoothingCount as number)) : 0,\n y: this.smoothing ? this.smoothing.y * (this.options.smooth - (this.smoothingCount as number)) : 0\n };\n\n this.smoothing = {\n x: ((this.parent.scale.x + original.x) * change - this.parent.scale.x) / this.options.smooth,\n y: ((this.parent.scale.y + original.y) * change - this.parent.scale.y) / this.options.smooth,\n };\n this.smoothingCount = 0;\n this.smoothingCenter = point;\n }\n else\n {\n let oldPoint: IPointData | undefined;\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point);\n }\n if (this.isAxisX())\n {\n this.parent.scale.x *= change;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y *= change;\n }\n this.parent.emit('zoomed', { viewport: this.parent, type: 'wheel' });\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += point.x - newPoint.x;\n this.parent.y += point.y - newPoint.y;\n }\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n this.parent.emit('wheel', { wheel: { dx: e.deltaX, dy: e.deltaY, dz: e.deltaZ }, event: e, viewport: this.parent });\n\n if (!this.parent.options.passiveWheel)\n {\n return true;\n }\n }\n}\n","import { Point, Rectangle } from '@pixi/math';\n\nimport type { IPointData } from '@pixi/math';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { Viewport } from './Viewport';\n\nexport interface IViewportTouch {\n id: number;\n last: IPointData | null;\n}\n\n/**\n * Handles all input for Viewport\n *\n * @internal\n * @ignore\n * @private\n */\nexport class InputManager\n{\n public readonly viewport: Viewport;\n\n public clickedAvailable?: boolean;\n public isMouseDown?: boolean;\n public last?: Point | null;\n public wheelFunction?: (e: WheelEvent) => void;\n /** List of active touches on viewport */\n public touches: IViewportTouch[];\n\n constructor(viewport: Viewport)\n {\n this.viewport = viewport;\n this.touches = [];\n\n this.addListeners();\n }\n\n /** Add input listeners */\n private addListeners()\n {\n this.viewport.interactive = true;\n if (!this.viewport.forceHitArea)\n {\n this.viewport.hitArea = new Rectangle(0, 0, this.viewport.worldWidth, this.viewport.worldHeight);\n }\n this.viewport.on('pointerdown', this.down, this);\n this.viewport.on('pointermove', this.move, this);\n this.viewport.on('pointerup', this.up, this);\n this.viewport.on('pointerupoutside', this.up, this);\n this.viewport.on('pointercancel', this.up, this);\n this.viewport.on('pointerout', this.up, this);\n this.wheelFunction = (e) => this.handleWheel(e);\n this.viewport.options.divWheel.addEventListener(\n 'wheel',\n this.wheelFunction as any,\n { passive: this.viewport.options.passiveWheel });\n this.isMouseDown = false;\n }\n\n /**\n * Removes all event listeners from viewport\n * (useful for cleanup of wheel when removing viewport)\n */\n public destroy()\n {\n this.viewport.options.divWheel.removeEventListener('wheel', this.wheelFunction as any);\n }\n\n /**\n * handle down events for viewport\n *\n * @param {PIXI.InteractionEvent} event\n */\n public down(event: InteractionEvent)\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n if (event.data.pointerType === 'mouse')\n {\n this.isMouseDown = true;\n }\n else if (!this.get(event.data.pointerId))\n {\n this.touches.push({ id: event.data.pointerId, last: null });\n }\n if (this.count() === 1)\n {\n this.last = event.data.global.clone();\n\n // clicked event does not fire if viewport is decelerating or bouncing\n const decelerate = this.viewport.plugins.get('decelerate', true);\n const bounce = this.viewport.plugins.get('bounce', true);\n\n if ((!decelerate || !decelerate.isActive()) && (!bounce || !bounce.isActive()))\n {\n this.clickedAvailable = true;\n }\n else\n {\n this.clickedAvailable = false;\n }\n }\n else\n {\n this.clickedAvailable = false;\n }\n\n const stop = this.viewport.plugins.down(event);\n\n if (stop && this.viewport.options.stopPropagation)\n {\n event.stopPropagation();\n }\n }\n\n /** Clears all pointer events */\n public clear(): void\n {\n this.isMouseDown = false;\n this.touches = [];\n this.last = null;\n }\n\n /**\n * @param {number} change\n * @returns whether change exceeds threshold\n */\n public checkThreshold(change: number): boolean\n {\n if (Math.abs(change) >= this.viewport.threshold)\n {\n return true;\n }\n\n return false;\n }\n\n /** Handle move events for viewport */\n public move(event: InteractionEvent): void\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n\n const stop = this.viewport.plugins.move(event);\n\n if (this.clickedAvailable && this.last)\n {\n const distX = event.data.global.x - this.last.x;\n const distY = event.data.global.y - this.last.y;\n\n if (this.checkThreshold(distX) || this.checkThreshold(distY))\n {\n this.clickedAvailable = false;\n }\n }\n\n if (stop && this.viewport.options.stopPropagation)\n {\n event.stopPropagation();\n }\n }\n\n /** Handle up events for viewport */\n public up(event: InteractionEvent): void\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n\n if (event.data.pointerType === 'mouse')\n {\n this.isMouseDown = false;\n }\n\n if (event.data.pointerType !== 'mouse')\n {\n this.remove(event.data.pointerId);\n }\n\n const stop = this.viewport.plugins.up(event);\n\n if (this.clickedAvailable && this.count() === 0 && this.last)\n {\n this.viewport.emit('clicked', {\n event,\n screen: this.last,\n world: this.viewport.toWorld(this.last),\n viewport: this\n });\n this.clickedAvailable = false;\n }\n\n if (stop && this.viewport.options.stopPropagation)\n {\n event.stopPropagation();\n }\n }\n\n /** Gets pointer position if this.interaction is set */\n public getPointerPosition(event: WheelEvent): Point\n {\n const point = new Point();\n\n if (this.viewport.options.interaction)\n {\n this.viewport.options.interaction.mapPositionToPoint(point, event.clientX, event.clientY);\n }\n else\n {\n point.x = event.clientX;\n point.y = event.clientY;\n }\n\n return point;\n }\n\n /** Handle wheel events */\n public handleWheel(event: WheelEvent): void\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n\n // do not handle events coming from other elements\n if (this.viewport.options.interaction\n && (this.viewport.options.interaction as any).interactionDOMElement !== event.target)\n {\n return;\n }\n\n // only handle wheel events where the mouse is over the viewport\n const point = this.viewport.toLocal(this.getPointerPosition(event));\n\n if (this.viewport.left <= point.x\n && point.x <= this.viewport.right\n && this.viewport.top <= point.y\n && point.y <= this.viewport.bottom)\n {\n const stop = this.viewport.plugins.wheel(event);\n\n if (stop && !this.viewport.options.passiveWheel)\n {\n event.preventDefault();\n }\n }\n }\n\n public pause(): void\n {\n this.touches = [];\n this.isMouseDown = false;\n }\n\n /** Get touch by id */\n public get(id: number): IViewportTouch | null\n {\n for (const touch of this.touches)\n {\n if (touch.id === id)\n {\n return touch;\n }\n }\n\n return null;\n }\n\n /** Remove touch by number */\n remove(id: number): void\n {\n for (let i = 0; i < this.touches.length; i++)\n {\n if (this.touches[i].id === id)\n {\n this.touches.splice(i, 1);\n\n return;\n }\n }\n }\n\n /**\n * @returns {number} count of mouse/touch pointers that are down on the viewport\n */\n count(): number\n {\n return (this.isMouseDown ? 1 : 0) + this.touches.length;\n }\n}\n","import type {\n Animate,\n Bounce,\n Clamp,\n ClampZoom,\n Decelerate,\n Drag,\n Follow,\n MouseEdges,\n Pinch,\n Plugin,\n Snap,\n SnapZoom,\n Wheel,\n} from './plugins';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { Viewport } from './Viewport';\n\nconst PLUGIN_ORDER = [\n 'drag',\n 'pinch',\n 'wheel',\n 'follow',\n 'mouse-edges',\n 'decelerate',\n 'aniamte',\n 'bounce',\n 'snap-zoom',\n 'clamp-zoom',\n 'snap',\n 'clamp',\n];\n\n/**\n * Use this to access current plugins or add user-defined plugins\n *\n * @public\n */\nexport class PluginManager\n{\n /** Maps mounted plugins by their type */\n public plugins: Partial>;\n\n /**\n * List of plugins mounted\n *\n * This list is kept sorted by the internal priority of plugins (hard-coded).\n */\n public list: Array;\n\n /** The viewport using the plugins managed by `this`. */\n public readonly viewport: Viewport;\n\n /** This is called by {@link Viewport} to initialize the {@link Viewport.plugins plugins}. */\n constructor(viewport: Viewport)\n {\n this.viewport = viewport;\n this.list = [];\n this.plugins = {};\n }\n\n /**\n * Inserts a named plugin or a user plugin into the viewport\n * default plugin order: 'drag', 'pinch', 'wheel', 'follow', 'mouse-edges', 'decelerate', 'bounce',\n * 'snap-zoom', 'clamp-zoom', 'snap', 'clamp'\n *\n * @param {string} name of plugin\n * @param {Plugin} plugin - instantiated Plugin class\n * @param {number} index to insert userPlugin (otherwise inserts it at the end)\n */\n public add(name: string, plugin: Plugin, index: number = PLUGIN_ORDER.length)\n {\n this.plugins[name] = plugin;\n\n const current = PLUGIN_ORDER.indexOf(name);\n\n if (current !== -1)\n {\n PLUGIN_ORDER.splice(current, 1);\n }\n\n PLUGIN_ORDER.splice(index, 0, name);\n this.sort();\n }\n\n public get(name: 'animate', ignorePaused?: boolean): Animate | undefined | null;\n public get(name: 'bounce', ignorePaused?: boolean): Bounce | undefined | null;\n public get(name: 'clamp', ignorePaused?: boolean): Clamp | undefined | null;\n public get(name: 'clamp-zoom', ignorePaused?: boolean): ClampZoom | undefined | null;\n public get(name: 'decelerate', ignorePaused?: boolean): Decelerate | undefined | null;\n public get(name: 'drag', ignorePaused?: boolean): Drag | undefined | null;\n public get(name: 'follow', ignorePaused?: boolean): Follow | undefined | null;\n public get(name: 'mouse-edges', ignorePaused?: boolean): MouseEdges | undefined | null;\n public get(name: 'pinch', ignorePaused?: boolean): Pinch | undefined | null;\n public get(name: 'snap', ignorePaused?: boolean): Snap | undefined | null;\n public get(name: 'snap-zoom', ignorePaused?: boolean): SnapZoom | undefined | null;\n public get(name: 'wheel', ignorePaused?: boolean): Wheel | undefined | null;\n public get(name: string, ignorePaused?: boolean): T | undefined | null;\n\n /**\n * Get plugin\n *\n * @param {string} name of plugin\n * @param {boolean} [ignorePaused] return null if plugin is paused\n */\n public get(name: string, ignorePaused?: boolean): T | undefined | null\n {\n if (ignorePaused)\n {\n if (this.plugins[name]?.paused)\n {\n return null;\n }\n }\n\n return this.plugins[name] as T;\n }\n\n /**\n * Update all active plugins\n *\n * @internal\n * @ignore\n * @param {number} elapsed type in milliseconds since last update\n */\n public update(elapsed: number): void\n {\n for (const plugin of this.list)\n {\n plugin.update(elapsed);\n }\n }\n\n /**\n * Resize all active plugins\n *\n * @internal\n * @ignore\n */\n public resize(): void\n {\n for (const plugin of this.list)\n {\n plugin.resize();\n }\n }\n\n /** Clamps and resets bounce and decelerate (as needed) after manually moving viewport */\n public reset(): void\n {\n for (const plugin of this.list)\n {\n plugin.reset();\n }\n }\n\n /** removes all installed plugins */\n public removeAll(): void\n {\n this.plugins = {};\n this.sort();\n }\n\n /**\n * Removes installed plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public remove(name: string): void\n {\n if (this.plugins[name])\n {\n delete this.plugins[name];\n this.viewport.emit(`${name}-remove`);\n this.sort();\n }\n }\n\n /**\n * Pause plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public pause(name: string): void\n {\n this.plugins[name]?.pause();\n }\n\n /**\n * Resume plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public resume(name: string): void\n {\n this.plugins[name]?.resume();\n }\n\n /**\n * Sort plugins according to PLUGIN_ORDER\n *\n * @internal\n * @ignore\n */\n public sort()\n {\n this.list = [];\n\n for (const plugin of PLUGIN_ORDER)\n {\n if (this.plugins[plugin])\n {\n this.list.push(this.plugins[plugin] as Plugin);\n }\n }\n }\n\n /**\n * Handle down for all plugins\n *\n * @internal\n * @ignore\n */\n public down(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.list)\n {\n if (plugin.down(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle move for all plugins\n *\n * @internal\n * @ignore\n */\n public move(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.viewport.plugins.list)\n {\n if (plugin.move(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle up for all plugins\n *\n * @internal\n * @ignore\n */\n public up(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.list)\n {\n if (plugin.up(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle wheel event for all plugins\n *\n * @internal\n * @ignore\n */\n public wheel(e: WheelEvent): boolean\n {\n let result = false;\n\n for (const plugin of this.list)\n {\n if (plugin.wheel(e))\n {\n result = true;\n }\n }\n\n return result;\n }\n}\n","import { Container } from '@pixi/display';\nimport { Point, Rectangle } from '@pixi/math';\nimport { Ticker } from '@pixi/ticker';\n\nimport { InputManager } from './InputManager';\nimport { PluginManager } from './PluginManager';\nimport {\n Animate,\n Bounce,\n Clamp,\n ClampZoom,\n Decelerate,\n Drag,\n Follow,\n MouseEdges,\n Pinch,\n Snap,\n SnapZoom,\n Wheel\n} from './plugins';\n\nimport type { DisplayObject, IDestroyOptions } from '@pixi/display';\nimport type { IHitArea, InteractionManager } from '@pixi/interaction';\n\n/** Options for {@link Viewport}. */\nexport interface IViewportOptions {\n /** @default window.innerWidth */\n screenWidth?: number;\n\n /** @default window.innerHeight */\n screenHeight?: number;\n\n /** @default this.width */\n worldWidth?: number | null;\n\n /** @default this.height */\n worldHeight?: number | null;\n\n /**\n * Number of pixels to move to trigger an input event (e.g., drag, pinch) or disable a clicked event\n *\n * @default 5\n */\n threshold?: number;\n\n /**\n * Whether the 'wheel' event is set to passive (note: if false, e.preventDefault() will be called when wheel is used over the viewport)\n *\n * @default true\n */\n passiveWheel?: boolean;\n\n /**\n * Whether to stopPropagation of events that impact the viewport (except wheel events, see options.passiveWheel)\n */\n stopPropagation?: boolean;\n\n /**\n * Change the default hitArea from world size to a new value\n */\n forceHitArea?: Rectangle | null;\n\n /**\n * Set this if you want to manually call update() function on each frame\n *\n * @default false\n */\n noTicker?: boolean;\n\n /**\n * InteractionManager, available from instantiated `WebGLRenderer/CanvasRenderer.plugins.interaction`\n *\n * It's used to calculate pointer postion relative to canvas location on screen\n */\n interaction?: InteractionManager | null;\n\n /**\n * Remove oncontextmenu=() => {} from the divWheel element\n */\n disableOnContextMenu?: boolean;\n\n /**\n * div to attach the wheel event\n *\n * @default document.body\n */\n divWheel?: HTMLElement;\n\n /**\n * Use this PIXI.ticker for updates\n *\n * @default PIXI.Ticker.shared\n */\n ticker?: Ticker;\n}\n\nexport interface ICompleteViewportOptions extends IViewportOptions {\n screenWidth: number;\n screenHeight: number;\n threshold: number;\n passiveWheel: boolean;\n stopPropagation: boolean;\n noTicker: boolean;\n ticker: Ticker;\n}\n\nexport interface IViewportTransformState {\n x: number;\n y: number;\n scaleX: number;\n scaleY: number;\n}\n\nconst DEFAULT_VIEWPORT_OPTIONS: ICompleteViewportOptions = {\n screenWidth: window.innerWidth,\n screenHeight: window.innerHeight,\n worldWidth: null,\n worldHeight: null,\n threshold: 5,\n passiveWheel: true,\n stopPropagation: false,\n forceHitArea: null,\n noTicker: false,\n interaction: null,\n disableOnContextMenu: false,\n ticker: Ticker.shared,\n};\n\n/**\n * Main class to use when creating a Viewport\n *\n * @public\n * @fires clicked\n * @fires drag-start\n * @fires drag-end\n * @fires drag-remove\n * @fires pinch-start\n * @fires pinch-end\n * @fires pinch-remove\n * @fires snap-start\n * @fires snap-end\n * @fires snap-remove\n * @fires snap-zoom-start\n * @fires snap-zoom-end\n * @fires snap-zoom-remove\n * @fires bounce-x-start\n * @fires bounce-x-end\n * @fires bounce-y-start\n * @fires bounce-y-end\n * @fires bounce-remove\n * @fires wheel\n * @fires wheel-remove\n * @fires wheel-scroll\n * @fires wheel-scroll-remove\n * @fires mouse-edge-start\n * @fires mouse-edge-end\n * @fires mouse-edge-remove\n * @fires moved\n * @fires moved-end\n * @fires zoomed\n * @fires zoomed-end\n * @fires frame-end\n */\nexport class Viewport extends Container\n{\n /** Flags whether the viewport is being panned */\n public moving?: boolean;\n\n public screenWidth: number;\n public screenHeight: number;\n\n /** Number of pixels to move to trigger an input event (e.g., drag, pinch) or disable a clicked event */\n public threshold: number;\n\n public readonly input: InputManager;\n\n /** Use this to add user plugins or access existing plugins (e.g., to pause, resume, or remove them) */\n public readonly plugins: PluginManager;\n\n /** Flags whether the viewport zoom is being changed. */\n public zooming?: boolean;\n\n public lastViewport?: IViewportTransformState | null;\n\n /** The options passed when creating this viewport, merged with the default values */\n public readonly options: ICompleteViewportOptions & { divWheel: HTMLElement };\n\n private _dirty?: boolean;\n private _forceHitArea?: IHitArea | null;\n private _hitAreaDefault?: Rectangle;\n private _pause?: boolean;\n private readonly tickerFunction?: () => void;\n private _worldWidth?: number | null;\n private _worldHeight?: number | null;\n\n /**\n * @param options\n */\n constructor(options: IViewportOptions = {})\n {\n super();\n this.options = Object.assign(\n {},\n { divWheel: document.body },\n DEFAULT_VIEWPORT_OPTIONS,\n options\n );\n\n this.screenWidth = this.options.screenWidth;\n this.screenHeight = this.options.screenHeight;\n\n this._worldWidth = this.options.worldWidth;\n this._worldHeight = this.options.worldHeight;\n this.forceHitArea = this.options.forceHitArea;\n this.threshold = this.options.threshold;\n\n this.options.divWheel = this.options.divWheel || document.body;\n\n if (this.options.disableOnContextMenu)\n {\n this.options.divWheel.oncontextmenu = (e) => e.preventDefault();\n }\n if (!this.options.noTicker)\n {\n this.tickerFunction = () => this.update(this.options.ticker.elapsedMS);\n this.options.ticker.add(this.tickerFunction);\n }\n\n this.input = new InputManager(this);\n this.plugins = new PluginManager(this);\n }\n\n /** Overrides PIXI.Container's destroy to also remove the 'wheel' and PIXI.Ticker listeners */\n destroy(options: IDestroyOptions)\n {\n if (!this.options.noTicker && this.tickerFunction)\n {\n this.options.ticker.remove(this.tickerFunction);\n }\n\n this.input.destroy();\n super.destroy(options);\n }\n\n /**\n * Update viewport on each frame.\n *\n * By default, you do not need to call this unless you set `options.noTicker=true`.\n *\n * @param {number} elapsed time in milliseconds since last update\n */\n update(elapsed: number): void\n {\n if (!this.pause)\n {\n this.plugins.update(elapsed);\n\n if (this.lastViewport)\n {\n // Check for moved-end event\n if (this.lastViewport.x !== this.x || this.lastViewport.y !== this.y)\n {\n this.moving = true;\n }\n else if (this.moving)\n {\n this.emit('moved-end', this);\n this.moving = false;\n }\n\n // Check for zoomed-end event\n if (this.lastViewport.scaleX !== this.scale.x || this.lastViewport.scaleY !== this.scale.y)\n {\n this.zooming = true;\n }\n else if (this.zooming)\n {\n this.emit('zoomed-end', this);\n this.zooming = false;\n }\n }\n\n if (!this.forceHitArea)\n {\n this._hitAreaDefault = new Rectangle(this.left, this.top, this.worldScreenWidth, this.worldScreenHeight);\n this.hitArea = this._hitAreaDefault;\n }\n\n this._dirty = this._dirty || !this.lastViewport\n || this.lastViewport.x !== this.x || this.lastViewport.y !== this.y\n || this.lastViewport.scaleX !== this.scale.x || this.lastViewport.scaleY !== this.scale.y;\n\n this.lastViewport = {\n x: this.x,\n y: this.y,\n scaleX: this.scale.x,\n scaleY: this.scale.y\n };\n this.emit('frame-end', this);\n }\n }\n\n /** Use this to set screen and world sizes, needed for pinch/wheel/clamp/bounce. */\n resize(\n screenWidth = window.innerWidth,\n screenHeight = window.innerHeight,\n worldWidth: number,\n worldHeight: number\n ): void\n {\n this.screenWidth = screenWidth;\n this.screenHeight = screenHeight;\n\n if (typeof worldWidth !== 'undefined')\n {\n this._worldWidth = worldWidth;\n }\n if (typeof worldHeight !== 'undefined')\n {\n this._worldHeight = worldHeight;\n }\n\n this.plugins.resize();\n this.dirty = true;\n }\n\n /** World width, in pixels */\n get worldWidth(): number\n {\n if (this._worldWidth)\n {\n return this._worldWidth;\n }\n\n return this.width / this.scale.x;\n }\n set worldWidth(value)\n {\n this._worldWidth = value;\n this.plugins.resize();\n }\n\n /** World height, in pixels */\n get worldHeight(): number\n {\n if (this._worldHeight)\n {\n return this._worldHeight;\n }\n\n return this.height / this.scale.y;\n }\n set worldHeight(value)\n {\n this._worldHeight = value;\n this.plugins.resize();\n }\n\n /** Get visible world bounds of viewport */\n public getVisibleBounds(): Rectangle\n {\n return new Rectangle(this.left, this.top, this.worldScreenWidth, this.worldScreenHeight);\n }\n\n /** Change coordinates from screen to world */\n public toWorld(x: number, y: number): Point;\n /** Change coordinates from screen to world */\n public toWorld(screenPoint: Point): Point;\n\n public toWorld(x: number | Point, y?: number): Point\n {\n if (arguments.length === 2)\n {\n return this.toLocal(new Point(x as number, y));\n }\n\n return this.toLocal(x as Point);\n }\n\n /** Change coordinates from world to screen */\n public toScreen(x: number, y: number): Point;\n /** Change coordinates from world to screen */\n public toScreen(worldPoint: Point): Point;\n\n public toScreen(x: number | Point, y?: number): Point\n {\n if (arguments.length === 2)\n {\n return this.toGlobal(new Point(x as number, y));\n }\n\n return this.toGlobal(x as Point);\n }\n\n /** Screen width in world coordinates */\n get worldScreenWidth(): number\n {\n return this.screenWidth / this.scale.x;\n }\n\n /** Screen height in world coordinates */\n get worldScreenHeight(): number\n {\n return this.screenHeight / this.scale.y;\n }\n\n /** World width in screen coordinates */\n get screenWorldWidth(): number\n {\n return this.worldWidth * this.scale.x;\n }\n\n /** World height in screen coordinates */\n get screenWorldHeight()\n {\n return this.worldHeight * this.scale.y;\n }\n\n /** Center of screen in world coordinates */\n get center(): Point\n {\n return new Point(\n this.worldScreenWidth / 2 - this.x / this.scale.x,\n this.worldScreenHeight / 2 - this.y / this.scale.y,\n );\n }\n set center(value: Point)\n {\n this.moveCenter(value);\n }\n\n /** Move center of viewport to (x, y) */\n public moveCenter(x: number, y: number): this;\n\n /** Move center of viewport to {@code center}. */\n public moveCenter(center: Point): this;\n\n public moveCenter(...args: [number, number] | [Point]): this\n {\n let x: number;\n let y: number;\n\n if (typeof args[0] === 'number')\n {\n x = args[0];\n y = args[1] as number;\n }\n else\n {\n x = args[0].x;\n y = args[0].y;\n }\n\n const newX = (this.worldScreenWidth / 2 - x) * this.scale.x;\n const newY = (this.worldScreenHeight / 2 - y) * this.scale.y;\n\n if (this.x !== newX || this.y !== newY)\n {\n this.position.set(newX, newY);\n this.plugins.reset();\n this.dirty = true;\n }\n\n return this;\n }\n\n /** Top-left corner of Viewport */\n get corner(): Point\n {\n return new Point(-this.x / this.scale.x, -this.y / this.scale.y);\n }\n set corner(value: Point)\n {\n this.moveCorner(value);\n }\n\n /** Move viewport's top-left corner; also clamps and resets decelerate and bounce (as needed) */\n public moveCorner(x: number, y: number): this;\n\n /** move viewport's top-left corner; also clamps and resets decelerate and bounce (as needed) */\n public moveCorner(center: Point): this;\n\n public moveCorner(...args: [number, number] | [Point])\n {\n let x;\n let y;\n\n if (args.length === 1)\n {\n x = -args[0].x * this.scale.x;\n y = -args[0].y * this.scale.y;\n }\n else\n {\n x = -args[0] * this.scale.x;\n y = -args[1] * this.scale.y;\n }\n\n if (x !== this.x || y !== this.y)\n {\n this.position.set(x, y);\n this.plugins.reset();\n this.dirty = true;\n }\n\n return this;\n }\n\n /** Get how many world pixels fit in screen's width */\n get screenWidthInWorldPixels()\n {\n return this.screenWidth / this.scale.x;\n }\n\n /** Get how many world pixels fit on screen's height */\n get screenHeightInWorldPixels()\n {\n return this.screenHeight / this.scale.y;\n }\n\n /**\n * Find the scale value that fits a world width on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param width - Width in world pixels\n * @return - scale\n */\n findFitWidth(width: number): number\n {\n return this.screenWidth / width;\n }\n\n /**\n * Finds the scale value that fits a world height on the screens\n * does not change the viewport (use fit... to change)\n *\n * @param height - Height in world pixels\n * @return - scale\n */\n findFitHeight(height: number): number\n {\n return this.screenHeight / height;\n }\n\n /**\n * Finds the scale value that fits the smaller of a world width and world height on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param {number} width in world pixels\n * @param {number} height in world pixels\n * @returns {number} scale\n */\n findFit(width: number, height: number): number\n {\n const scaleX = this.screenWidth / width;\n const scaleY = this.screenHeight / height;\n\n return Math.min(scaleX, scaleY);\n }\n\n /**\n * Finds the scale value that fits the larger of a world width and world height on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param {number} width in world pixels\n * @param {number} height in world pixels\n * @returns {number} scale\n */\n findCover(width: number, height: number): number\n {\n const scaleX = this.screenWidth / width;\n const scaleY = this.screenHeight / height;\n\n return Math.max(scaleX, scaleY);\n }\n\n /**\n * Change zoom so the width fits in the viewport\n *\n * @param width - width in world coordinates\n * @param center - maintain the same center\n * @param scaleY - whether to set scaleY=scaleX\n * @param noClamp - whether to disable clamp-zoom\n * @returns {Viewport} this\n */\n fitWidth(width = this.worldWidth, center?: boolean, scaleY = true, noClamp?: boolean): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.x = this.screenWidth / width;\n\n if (scaleY)\n {\n this.scale.y = this.scale.x;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (!noClamp && clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so the height fits in the viewport\n *\n * @param {number} [height=this.worldHeight] in world coordinates\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @param {boolean} [scaleX=true] whether to set scaleX = scaleY\n * @param {boolean} [noClamp] whether to disable clamp-zoom\n * @returns {Viewport} this\n */\n fitHeight(height = this.worldHeight, center?: boolean, scaleX = true, noClamp?: boolean)\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.y = this.screenHeight / height;\n\n if (scaleX)\n {\n this.scale.x = this.scale.y;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (!noClamp && clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so it fits the entire world in the viewport\n *\n * @param {boolean} center maintain the same center of the screen after zoom\n * @returns {Viewport} this\n */\n fitWorld(center?: boolean): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n\n this.scale.x = this.screenWidth / this.worldWidth;\n this.scale.y = this.screenHeight / this.worldHeight;\n\n if (this.scale.x < this.scale.y)\n {\n this.scale.y = this.scale.x;\n }\n else\n {\n this.scale.x = this.scale.y;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so it fits the size or the entire world in the viewport\n *\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @param {number} [width=this.worldWidth] desired width\n * @param {number} [height=this.worldHeight] desired height\n * @returns {Viewport} this\n */\n fit(center?: boolean, width = this.worldWidth, height = this.worldHeight): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n\n this.scale.x = this.screenWidth / width;\n this.scale.y = this.screenHeight / height;\n\n if (this.scale.x < this.scale.y)\n {\n this.scale.y = this.scale.x;\n }\n else\n {\n this.scale.x = this.scale.y;\n }\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n // eslint-disable-next-line\n // @ts-ignore\n set visible(value: boolean)\n {\n if (!value)\n {\n this.input.clear();\n }\n\n super.visible = value;\n }\n\n /**\n * Zoom viewport to specific value.\n *\n * @param {number} scale value (e.g., 1 would be 100%, 0.25 would be 25%)\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n setZoom(scale: number, center?: boolean): this\n {\n let save;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.set(scale);\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Zoom viewport by a certain percent (in both x and y direction).\n *\n * @param {number} percent change (e.g., 0.25 would increase a starting scale of 1.0 to 1.25)\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n zoomPercent(percent: number, center?: boolean): this\n {\n return this.setZoom(this.scale.x + this.scale.x * percent, center);\n }\n\n /**\n * Zoom viewport by increasing/decreasing width by a certain number of pixels.\n *\n * @param {number} change in pixels\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n zoom(change: number, center?: boolean)\n {\n this.fitWidth(change + this.worldScreenWidth, center);\n\n return this;\n }\n\n /** Changes scale of viewport and maintains center of viewport */\n get scaled(): number\n {\n return this.scale.x;\n }\n set scaled(scale: number)\n {\n this.setZoom(scale, true);\n }\n\n /**\n * @param {SnapZoomOptions} options\n */\n snapZoom(options: any): this\n {\n this.plugins.add('snap-zoom', new SnapZoom(this, options));\n\n return this;\n }\n\n /** Is container out of world bounds */\n OOB(): {\n left: boolean;\n right: boolean;\n top: boolean;\n bottom: boolean;\n cornerPoint: Point;\n }\n {\n return {\n left: this.left < 0,\n right: this.right > this.worldWidth,\n top: this.top < 0,\n bottom: this.bottom > this.worldHeight,\n cornerPoint: new Point(\n this.worldWidth * this.scale.x - this.screenWidth,\n this.worldHeight * this.scale.y - this.screenHeight\n )\n };\n }\n\n /** World coordinates of the right edge of the screen */\n get right(): number\n {\n return -this.x / this.scale.x + this.worldScreenWidth;\n }\n set right(value: number)\n {\n this.x = -value * this.scale.x + this.screenWidth;\n this.plugins.reset();\n }\n\n /** World coordinates of the left edge of the screen */\n get left(): number\n {\n return -this.x / this.scale.x;\n }\n set left(value: number)\n {\n this.x = -value * this.scale.x;\n this.plugins.reset();\n }\n\n /** World coordinates of the top edge of the screen */\n get top(): number\n {\n return -this.y / this.scale.y;\n }\n set top(value: number)\n {\n this.y = -value * this.scale.y;\n this.plugins.reset();\n }\n\n /** World coordinates of the bottom edge of the screen */\n get bottom(): number\n {\n return -this.y / this.scale.y + this.worldScreenHeight;\n }\n set bottom(value: number)\n {\n this.y = -value * this.scale.y + this.screenHeight;\n this.plugins.reset();\n }\n\n /**\n * Determines whether the viewport is dirty (i.e., needs to be renderered to the screen because of a change)\n */\n get dirty(): boolean\n {\n return !!this._dirty;\n }\n set dirty(value: boolean)\n {\n this._dirty = value;\n }\n\n /**\n * Permanently changes the Viewport's hitArea\n *\n * NOTE: if not set then hitArea = PIXI.Rectangle(Viewport.left, Viewport.top, Viewport.worldScreenWidth, Viewport.worldScreenHeight)\n */\n get forceHitArea(): IHitArea | null | undefined\n {\n return this._forceHitArea;\n }\n set forceHitArea(value: IHitArea | null | undefined)\n {\n if (value)\n {\n this._forceHitArea = value;\n this.hitArea = value;\n }\n else\n {\n this._forceHitArea = null;\n this.hitArea = new Rectangle(0, 0, this.worldWidth, this.worldHeight);\n }\n }\n\n /**\n * Enable one-finger touch to drag\n *\n * NOTE: if you expect users to use right-click dragging, you should enable `viewport.options.disableOnContextMenu`\n * to avoid the context menu popping up on each right-click drag.\n *\n * @param {DragOptions} [options]\n * @returns {Viewport} this\n */\n public drag(options: any): this\n {\n this.plugins.add('drag', new Drag(this, options));\n\n return this;\n }\n\n /**\n * Clamp to world boundaries or other provided boundaries\n *\n * NOTES:\n * clamp is disabled if called with no options; use { direction: 'all' } for all edge clamping\n * screenWidth, screenHeight, worldWidth, and worldHeight needs to be set for this to work properly\n *\n * @param {ClampOptions} [options]\n * @returns {Viewport} this\n */\n public clamp(options: any): this\n {\n this.plugins.add('clamp', new Clamp(this, options));\n\n return this;\n }\n\n /**\n * Decelerate after a move\n *\n * NOTE: this fires 'moved' event during deceleration\n *\n * @param {DecelerateOptions} [options]\n * @return {Viewport} this\n */\n public decelerate(options: any): this\n {\n this.plugins.add('decelerate', new Decelerate(this, options));\n\n return this;\n }\n\n /**\n * Bounce on borders\n * NOTES:\n * screenWidth, screenHeight, worldWidth, and worldHeight needs to be set for this to work properly\n * fires 'moved', 'bounce-x-start', 'bounce-y-start', 'bounce-x-end', and 'bounce-y-end' events\n * @param {object} [options]\n * @param {string} [options.sides=all] all, horizontal, vertical, or combination of top, bottom, right, left (e.g., 'top-bottom-right')\n * @param {number} [options.friction=0.5] friction to apply to decelerate if active\n * @param {number} [options.time=150] time in ms to finish bounce\n * @param {object} [options.bounceBox] use this bounceBox instead of (0, 0, viewport.worldWidth, viewport.worldHeight)\n * @param {number} [options.bounceBox.x=0]\n * @param {number} [options.bounceBox.y=0]\n * @param {number} [options.bounceBox.width=viewport.worldWidth]\n * @param {number} [options.bounceBox.height=viewport.worldHeight]\n * @param {string|function} [options.ease=easeInOutSine] ease function or name (see http://easings.net/ for supported names)\n * @param {string} [options.underflow=center] (top/bottom/center and left/right/center, or center) where to place world if too small for screen\n * @return {Viewport} this\n */\n public bounce(options: any): this\n {\n this.plugins.add('bounce', new Bounce(this, options));\n\n return this;\n }\n\n /**\n * Enable pinch to zoom and two-finger touch to drag\n *\n * @param {PinchOptions} [options]\n * @return {Viewport} this\n */\n public pinch(options: any): this\n {\n this.plugins.add('pinch', new Pinch(this, options));\n\n return this;\n }\n\n /**\n * Snap to a point\n *\n * @param {number} x\n * @param {number} y\n * @param {SnapOptions} [options]\n * @return {Viewport} this\n */\n public snap(x: number, y: number, options: any): this\n {\n this.plugins.add('snap', new Snap(this, x, y, options));\n\n return this;\n }\n\n /**\n * Follow a target\n *\n * NOTES:\n * uses the (x, y) as the center to follow; for PIXI.Sprite to work properly, use sprite.anchor.set(0.5)\n * options.acceleration is not perfect as it doesn't know the velocity of the target\n * it adds acceleration to the start of movement and deceleration to the end of movement when the target is stopped\n * fires 'moved' event\n * @param {PIXI.DisplayObject} target to follow\n * @param {FollowOptions} [options]\n * @returns {Viewport} this\n */\n public follow(target: DisplayObject, options: any): this\n {\n this.plugins.add('follow', new Follow(this, target, options));\n\n return this;\n }\n\n /**\n * Zoom using mouse wheel\n *\n * @param {WheelOptions} [options]\n * @return {Viewport} this\n */\n public wheel(options: any): this\n {\n this.plugins.add('wheel', new Wheel(this, options));\n\n return this;\n }\n\n /**\n * Animate the position and/or scale of the viewport\n *\n * @param {AnimateOptions} options\n * @returns {Viewport} this\n */\n public animate(options: any): this\n {\n this.plugins.add('animate', new Animate(this, options));\n\n return this;\n }\n\n /**\n * Enable clamping of zoom to constraints\n *\n * The minWidth/Height settings are how small the world can get (as it would appear on the screen)\n * before clamping. The maxWidth/maxHeight is how larger the world can scale (as it would appear on\n * the screen) before clamping.\n *\n * For example, if you have a world size of 1000 x 1000 and a screen size of 100 x 100, if you set\n * minWidth/Height = 100 then the world will not be able to zoom smaller than the screen size (ie,\n * zooming out so it appears smaller than the screen). Similarly, if you set maxWidth/Height = 100\n * the world will not be able to zoom larger than the screen size (ie, zooming in so it appears\n * larger than the screen).\n *\n * @param {ClampZoomOptions} [options]\n * @return {Viewport} this\n */\n public clampZoom(options: any): this\n {\n this.plugins.add('clamp-zoom', new ClampZoom(this, options));\n\n return this;\n }\n\n /**\n * Scroll viewport when mouse hovers near one of the edges or radius-distance from center of screen.\n *\n * NOTE: fires 'moved' event\n *\n * @param {MouseEdgesOptions} [options]\n */\n public mouseEdges(options: any): this\n {\n this.plugins.add('mouse-edges', new MouseEdges(this, options));\n\n return this;\n }\n\n /** Pause viewport (including animation updates such as decelerate) */\n get pause(): boolean\n {\n return !!this._pause;\n }\n set pause(value: boolean)\n {\n this._pause = value;\n\n this.lastViewport = null;\n this.moving = false;\n this.zooming = false;\n\n if (value)\n {\n this.input.pause();\n }\n }\n\n /**\n * Move the viewport so the bounding box is visible\n *\n * @param x - left\n * @param y - top\n * @param width\n * @param height\n * @param resizeToFit - Resize the viewport so the box fits within the viewport\n */\n public ensureVisible(x: number, y: number, width: number, height: number, resizeToFit?: boolean)\n {\n if (resizeToFit && (width > this.worldScreenWidth || height > this.worldScreenHeight))\n {\n this.fit(true, width, height);\n this.emit('zoomed', { viewport: this, type: 'ensureVisible' });\n }\n let moved = false;\n\n if (x < this.left)\n {\n this.left = x;\n moved = true;\n }\n else if (x + width > this.right)\n {\n this.right = x + width;\n moved = true;\n }\n if (y < this.top)\n {\n this.top = y;\n moved = true;\n }\n else if (y + height > this.bottom)\n {\n this.bottom = y + height;\n moved = true;\n }\n if (moved)\n {\n this.emit('moved', { viewport: this, type: 'ensureVisible' });\n }\n }\n}\n\n/**\n * Fires after a mouse or touch click\n * @event Viewport#clicked\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a drag starts\n * @event Viewport#drag-start\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a drag ends\n * @event Viewport#drag-end\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a pinch starts\n * @event Viewport#pinch-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a pinch end\n * @event Viewport#pinch-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap starts\n * @event Viewport#snap-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap ends\n * @event Viewport#snap-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap-zoom starts\n * @event Viewport#snap-zoom-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap-zoom ends\n * @event Viewport#snap-zoom-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce starts in the x direction\n * @event Viewport#bounce-x-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce ends in the x direction\n * @event Viewport#bounce-x-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce starts in the y direction\n * @event Viewport#bounce-y-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce ends in the y direction\n * @event Viewport#bounce-y-end\n * @type {Viewport}\n */\n\n/**\n * Fires when for a mouse wheel event\n * @event Viewport#wheel\n * @type {object}\n * @property {object} wheel\n * @property {number} wheel.dx\n * @property {number} wheel.dy\n * @property {number} wheel.dz\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a wheel-scroll occurs\n * @event Viewport#wheel-scroll\n * @type {Viewport}\n */\n\n/**\n * Fires when a mouse-edge starts to scroll\n * @event Viewport#mouse-edge-start\n * @type {Viewport}\n */\n\n/**\n * Fires when the mouse-edge scrolling ends\n * @event Viewport#mouse-edge-end\n * @type {Viewport}\n */\n\n/**\n * Fires when viewport moves through UI interaction, deceleration, ensureVisible, or follow\n * @event Viewport#moved\n * @type {object}\n * @property {Viewport} viewport\n * @property {string} type - (drag, snap, pinch, follow, bounce-x, bounce-y,\n * clamp-x, clamp-y, decelerate, mouse-edges, wheel, ensureVisible)\n */\n\n/**\n * Fires when viewport moves through UI interaction, deceleration, ensureVisible, or follow\n * @event Viewport#zoomed\n * @type {object}\n * @property {Viewport} viewport\n * @property {string} type (drag-zoom, pinch, wheel, clamp-zoom, ensureVisible)\n */\n\n/**\n * Fires when viewport stops moving\n * @event Viewport#moved-end\n * @type {Viewport}\n */\n\n/**\n * Fires when viewport stops zooming\n * @event Viewport#zoomed-end\n * @type {Viewport}\n */\n\n/**\n* Fires at the end of an update frame\n* @event Viewport#frame-end\n* @type {Viewport}\n*/\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAGA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC1FA;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACpBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wRACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;qgBC1RA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC3XA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC1RA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC7IA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACrRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC1eA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACvJA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACxRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,8HACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC/JA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACxNA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACrRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC3NA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;ACrSA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACpRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;"} \ No newline at end of file diff --git a/dist/viewport.min.js b/dist/viewport.min.js index e6dc9e07..bf938a04 100644 --- a/dist/viewport.min.js +++ b/dist/viewport.min.js @@ -2,7 +2,7 @@ /*! * pixi-viewport - v4.30.0 - * Compiled Thu, 22 Apr 2021 09:16:32 UTC + * Compiled Thu, 22 Apr 2021 10:41:51 UTC * * pixi-viewport is licensed under the MIT License. * http://www.opensource.org/licenses/mit-license @@ -1142,7 +1142,7 @@ this.PIXI = this.PIXI || {}; this.clamp(); } - /** Clamp the viewport's zoom immediately. */ + /** Clamp the viewport scale zoom) */ clamp() { if (this.paused) @@ -1195,20 +1195,59 @@ this.PIXI = this.PIXI || {}; } } else + if (this.options.minScale || this.options.maxScale) { - let scale = this.parent.scale.x; + const minScale = { x: null, y: null }; + const maxScale = { x: null, y: null }; - if (this.options.minScale !== null && scale < this.options.minScale) + if (typeof this.options.minScale === 'number') { - scale = this.options.minScale; + minScale.x = this.options.minScale; + minScale.y = this.options.minScale; } - if (this.options.maxScale !== null && scale > this.options.maxScale) + else if (this.options.minScale !== null) { - scale = this.options.maxScale; + const optsMinScale = this.options.minScale ; + + minScale.x = typeof optsMinScale.x === 'undefined' ? null : optsMinScale.x; + minScale.y = typeof optsMinScale.y === 'undefined' ? null : optsMinScale.y; + } + + if (typeof this.options.maxScale === 'number') + { + maxScale.x = this.options.maxScale; + maxScale.y = this.options.maxScale; + } + else if (this.options.maxScale !== null) + { + const optsMaxScale = this.options.maxScale ; + + maxScale.x = typeof optsMaxScale.x === 'undefined' ? null : optsMaxScale.x; + maxScale.y = typeof optsMaxScale.y === 'undefined' ? null : optsMaxScale.y; + } + + let scaleX = this.parent.scale.x; + let scaleY = this.parent.scale.y; + + if (minScale.x !== null && scaleX < minScale.x) + { + scaleX = minScale.x; } - if (scale !== this.parent.scale.x) + if (maxScale.x !== null && scaleX > maxScale.x) { - this.parent.scale.set(scale); + scaleX = maxScale.x; + } + if (minScale.y !== null && scaleY < minScale.y) + { + scaleY = minScale.y; + } + if (maxScale.y !== null && scaleY > maxScale.y) + { + scaleY = maxScale.y; + } + if (scaleX !== this.parent.scale.x || scaleY !== this.parent.scale.y) + { + this.parent.scale.set(scaleX, scaleY); this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' }); } } @@ -1220,6 +1259,8 @@ this.PIXI = this.PIXI || {}; } } + /** This allows independent x and y values for min/maxScale */ + const DEFAULT_DECELERATE_OPTIONS = { friction: 0.98, bounce: 0.8, diff --git a/dist/viewport.min.js.map b/dist/viewport.min.js.map index b21afc1d..9332b503 100644 --- a/dist/viewport.min.js.map +++ b/dist/viewport.min.js.map @@ -1 +1 @@ -{"version":3,"file":"viewport.min.js","sources":["../src/plugins/Plugin.ts","../src/ease.ts","../src/plugins/Animate.ts","../src/plugins/Bounce.ts","../src/plugins/Clamp.ts","../src/plugins/ClampZoom.ts","../src/plugins/Decelerate.ts","../src/plugins/Drag.ts","../src/plugins/Follow.ts","../src/plugins/MouseEdges.ts","../src/plugins/Pinch.ts","../src/plugins/Snap.ts","../src/plugins/SnapZoom.ts","../src/plugins/Wheel.ts","../src/InputManager.ts","../src/PluginManager.ts","../src/Viewport.ts"],"sourcesContent":["import type { InteractionEvent } from '@pixi/interaction';\nimport type { Viewport } from '../Viewport';\n\n/**\n * Derive this class to create user-defined plugins\n *\n * @public\n */\nexport class Plugin\n{\n /** The viewport to which this plugin is attached. */\n public readonly parent: Viewport;\n\n /**\n * Flags whether this plugin has been \"paused\".\n *\n * @see Plugin#pause\n * @see Plugin#resume\n */\n public paused: boolean;\n\n /** @param {Viewport} parent */\n constructor(parent: Viewport)\n {\n this.parent = parent;\n this.paused = false;\n }\n\n /** Called when plugin is removed */\n public destroy()\n {\n // Override for implementation\n }\n\n /** Handler for pointerdown PIXI event */\n public down(_e: InteractionEvent): boolean\n {\n return false;\n }\n\n /** Handler for pointermove PIXI event */\n public move(_e: InteractionEvent): boolean\n {\n return false;\n }\n\n /** Handler for pointerup PIXI event */\n public up(_e: InteractionEvent): boolean\n {\n return false;\n }\n\n /** Handler for wheel event on div */\n public wheel(_e: WheelEvent): boolean | undefined\n {\n return false;\n }\n\n /**\n * Called on each tick\n * @param {number} elapsed time in millisecond since last update\n */\n public update(_delta: number): void\n {\n // Override for implementation\n }\n\n /** Called when the viewport is resized */\n public resize()\n {\n // Override for implementation\n }\n\n /** Called when the viewport is manually moved */\n public reset(): void\n {\n // Override for implementation\n }\n\n /** Pause the plugin */\n public pause(): void\n {\n this.paused = true;\n }\n\n /** Un-pause the plugin */\n public resume(): void\n {\n this.paused = false;\n }\n}\n","// eslint-disable-next-line\n// @ts-expect-error Penner seems to have no typings.\nimport Penner from 'penner';\n\n/**\n * Returns correct Penner equation using string or Function.\n *\n * @internal\n * @ignore\n * @param {(function|string)} [ease]\n * @param {defaults} default penner equation to use if none is provided\n */\nexport default function ease(ease: any, defaults?: any): any\n{\n if (!ease)\n {\n return Penner[defaults]\n }\n else if (typeof ease === 'function')\n {\n return ease\n }\n else if (typeof ease === 'string')\n {\n return Penner[ease]\n }\n}","import { Point } from '@pixi/math';\nimport { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Animate}. */\nexport interface IAnimateOptions {\n /** Time to animate */\n time?: number;\n\n /** Position to move the viewport to */\n position?: Point;\n\n /**\n * Desired viewport width in world pixels\n *\n * (use instead of scale; aspect ratio is maintained if height is not provided)\n */\n width?: number;\n\n /**\n * Desired viewport height in world pixels\n *\n * (use instead of scale; aspect ratio is maintained if width is not provided)\n */\n height?: number;\n\n /** Scale to change zoom (scale.x = scale.y) */\n scale?: number;\n\n /** Independently change zoom in x-direction */\n scaleX?: number;\n\n /** Independently change zoom in y-direction */\n scaleY?: number;\n\n /** Easing function to use */\n ease?: any;\n\n /** Callback to invoke when the animation completes */\n callbackOnComplete?: (viewport: Viewport) => void;\n\n /** Removes this plugin if interrupted by any user input */\n removeOnInterrupt?: boolean;\n}\n\nconst DEFAULT_ANIMATE_OPTIONS = {\n removeOnInterrupt: false,\n ease: 'linear',\n time: 1000,\n};\n\n/**\n * Animation plugin.\n *\n * @see Viewport#animate\n * @fires animate-end\n */\nexport class Animate extends Plugin\n{\n public readonly options: IAnimateOptions & { ease: any; time: number };\n\n /** The starting x-coordinate of the viewport. */\n protected startX?: number;\n\n /** The starting y-coordinate of the viewport. */\n protected startY?: number;\n\n /** The change in the x-coordinate of the viewport through the animation.*/\n protected deltaX?: number;\n\n /** The change in the y-coordinate of the viewport through the animation. */\n protected deltaY?: number;\n\n /** Marks whether the center of the viewport is preserved in the animation. */\n protected keepCenter!: boolean;\n\n /** The starting viewport width. */\n protected startWidth: number | null = null;\n\n /** The starting viewport height. */\n protected startHeight: number | null = null;\n\n /** The change in the viewport's width through the animation. */\n protected deltaWidth: number | null = null;\n\n /** The change in the viewport's height through the animation. */\n protected deltaHeight: number | null = null;\n\n /** The viewport's width post-animation. */\n protected width: number | null = null;\n\n /** The viewport's height post-animation. */\n protected height: number | null = null;\n\n /** The time since the animation started. */\n protected time = 0;\n\n /**\n * This is called by {@link Viewport.animate}.\n *\n * @param parent\n * @param options\n */\n constructor(parent: Viewport, options: IAnimateOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_ANIMATE_OPTIONS, options);\n this.options.ease = ease(this.options.ease);\n\n this.setupPosition();\n this.setupZoom();\n\n this.time = 0;\n }\n\n /**\n * Setup `startX`, `startY`, `deltaX`, `deltaY`, `keepCenter`.\n *\n * This is called during construction.\n */\n protected setupPosition(): void\n {\n if (typeof this.options.position !== 'undefined')\n {\n this.startX = this.parent.center.x;\n this.startY = this.parent.center.y;\n this.deltaX = this.options.position.x - this.parent.center.x;\n this.deltaY = this.options.position.y - this.parent.center.y;\n this.keepCenter = false;\n }\n else\n {\n this.keepCenter = true;\n }\n }\n\n /**\n * Setup `startWidth, `startHeight`, `deltaWidth, `deltaHeight, `width`, `height`.\n *\n * This is called during construction.\n */\n protected setupZoom(): void\n {\n this.width = null;\n this.height = null;\n\n if (typeof this.options.scale !== 'undefined')\n {\n this.width = this.parent.screenWidth / this.options.scale;\n }\n else if (typeof this.options.scaleX !== 'undefined' || typeof this.options.scaleY !== 'undefined')\n {\n if (typeof this.options.scaleX !== 'undefined')\n {\n // screenSizeInWorldPixels = screenWidth / scale\n this.width = this.parent.screenWidth / this.options.scaleX;\n }\n if (typeof this.options.scaleY !== 'undefined')\n {\n this.height = this.parent.screenHeight / this.options.scaleY;\n }\n }\n else\n {\n if (typeof this.options.width !== 'undefined')\n {\n this.width = this.options.width;\n }\n if (typeof this.options.height !== 'undefined')\n {\n this.height = this.options.height;\n }\n }\n\n if (this.width !== null)\n {\n this.startWidth = this.parent.screenWidthInWorldPixels;\n this.deltaWidth = this.width - this.startWidth;\n }\n if (this.height !== null)\n {\n this.startHeight = this.parent.screenHeightInWorldPixels;\n this.deltaHeight = this.height - this.startHeight;\n }\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('animate');\n }\n\n return false;\n }\n\n public complete(): void\n {\n this.parent.plugins.remove('animate');\n if (this.width !== null)\n {\n this.parent.fitWidth(this.width, this.keepCenter, this.height === null);\n }\n if (this.height !== null)\n {\n this.parent.fitHeight(this.height, this.keepCenter, this.width === null);\n }\n if (!this.keepCenter)\n {\n this.parent.moveCenter(this.options.position as Point);\n }\n\n this.parent.emit('animate-end', this.parent);\n\n if (this.options.callbackOnComplete)\n {\n this.options.callbackOnComplete(this.parent);\n }\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n this.time += elapsed;\n\n if (this.time >= this.options.time)\n {\n this.complete();\n }\n else\n {\n const originalZoom = new Point(this.parent.scale.x, this.parent.scale.y);\n const percent = this.options.ease(this.time, 0, 1, this.options.time);\n\n if (this.width !== null)\n {\n const startWidth = this.startWidth as number;\n const deltaWidth = this.deltaWidth as number;\n\n this.parent.fitWidth(\n startWidth + (deltaWidth * percent),\n this.keepCenter,\n this.height === null);\n }\n if (this.height !== null)\n {\n const startHeight = this.startHeight as number;\n const deltaHeight = this.deltaHeight as number;\n\n this.parent.fitHeight(\n startHeight + (deltaHeight * percent),\n this.keepCenter,\n this.width === null);\n }\n if (this.width === null)\n {\n this.parent.scale.x = this.parent.scale.y;\n }\n else if (this.height === null)\n {\n this.parent.scale.y = this.parent.scale.x;\n }\n if (!this.keepCenter)\n {\n const startX = this.startX as number;\n const startY = this.startY as number;\n const deltaX = this.deltaX as number;\n const deltaY = this.deltaY as number;\n const original = new Point(this.parent.x, this.parent.y);\n\n this.parent.moveCenter(startX + (deltaX * percent), startY + (deltaY * percent));\n this.parent.emit('moved', { viewport: this.parent, original, type: 'animate' });\n }\n if (this.width || this.height)\n {\n this.parent.emit('zoomed', { viewport: this.parent, original: originalZoom, type: 'animate' });\n }\n }\n }\n}\n","import { Point, Rectangle } from '@pixi/math';\nimport { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Drag } from './Drag';\nimport type { IDecelerateOptions } from './Decelerate';\nimport type { Pinch } from './Pinch';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Bounce}. */\nexport interface IBounceOptions {\n /** \"all\", \"horizontal\", \"vertical\", or combination of \"top\", \"bottom\", \"right\", \"left\" (e.g., 'top-bottom-right') */\n sides?:\n 'all'\n | 'horizontal'\n | 'vertical'\n | string;\n\n /** Friction to apply to decelerate if active */\n friction?: number;\n\n /** Time in ms to finish bounce */\n time?: number;\n\n /** Use this bounceBox instead of (0, 0, viewport.worldWidth, viewport.worldHeight) */\n bounceBox?: Rectangle | null;\n\n /** Ease function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /** (top/bottom/center and left/right/center, or center) where to place world if too small for screen */\n underflow?: 'center' | string;\n}\n\n/** Bounce state along an axis */\nexport interface IBounceState {\n /** Elapsed time since bounce started */\n time: number;\n\n /** Starting coordinate */\n start: number;\n\n /** Change in coordinate through bounce */\n delta: number;\n\n /** Ending coordinate */\n end: number;\n}\n\nconst DEFAULT_BOUNCE_OPTIONS: Required = {\n sides: 'all',\n friction: 0.5,\n time: 150,\n ease: 'easeInOutSine',\n underflow: 'center',\n bounceBox: null\n};\n\n/**\n * @fires bounce-start-x\n * @fires bounce.end-x\n * @fires bounce-start-y\n * @fires bounce-end-y\n * @public\n */\nexport class Bounce extends Plugin\n{\n /** The options passed to initialize this plugin, cannot be modified again. */\n public readonly options: Readonly>;\n\n /** Holds whether to bounce from left side. */\n public readonly left: boolean ;\n\n /** Holds whether to bounce from top side. */\n public readonly top: boolean;\n\n /** Holds whether to bounce from right side. */\n public readonly right: boolean;\n\n /** Holds whether to bounce from bottom side. */\n public readonly bottom: boolean;\n\n /** Direction of underflow along x-axis. */\n public readonly underflowX: -1 | 0 | 1;\n\n /** Direction of underflow along y-axis. */\n public readonly underflowY: -1 | 0 | 1;\n\n /** Easing */\n protected ease: any;\n\n /** Bounce state along x-axis */\n protected toX!: IBounceState | null;\n\n /** Bounce state along y-axis */\n protected toY!: IBounceState | null;\n\n /**\n * This is called by {@link Viewport.bounce}.\n */\n constructor(parent: Viewport, options: IBounceOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_BOUNCE_OPTIONS, options);\n this.ease = ease(this.options.ease, 'easeInOutSine');\n\n if (this.options.sides)\n {\n if (this.options.sides === 'all')\n {\n this.top = this.bottom = this.left = this.right = true;\n }\n else if (this.options.sides === 'horizontal')\n {\n this.right = this.left = true;\n this.top = this.bottom = false;\n }\n else if (this.options.sides === 'vertical')\n {\n this.left = this.right = false;\n this.top = this.bottom = true;\n }\n else\n {\n this.top = this.options.sides.indexOf('top') !== -1;\n this.bottom = this.options.sides.indexOf('bottom') !== -1;\n this.left = this.options.sides.indexOf('left') !== -1;\n this.right = this.options.sides.indexOf('right') !== -1;\n }\n } else {\n this.left = this.top = this.right = this.bottom = false;\n }\n\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'center')\n {\n this.underflowX = 0;\n this.underflowY = 0;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n }\n\n this.reset();\n }\n\n public isActive(): boolean\n {\n return this.toX !== null || this.toY !== null;\n }\n\n public down(): boolean\n {\n this.toX = this.toY = null;\n\n return false;\n }\n\n public up(): boolean\n {\n this.bounce();\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n this.bounce();\n\n if (this.toX)\n {\n const toX = this.toX;\n\n toX.time += elapsed;\n this.parent.emit('moved', { viewport: this.parent, type: 'bounce-x' });\n\n if (toX.time >= this.options.time)\n {\n this.parent.x = toX.end;\n this.toX = null;\n this.parent.emit('bounce-x-end', this.parent);\n }\n else\n {\n this.parent.x = this.ease(toX.time, toX.start, toX.delta, this.options.time);\n }\n }\n\n if (this.toY)\n {\n const toY = this.toY;\n\n toY.time += elapsed;\n this.parent.emit('moved', { viewport: this.parent, type: 'bounce-y' });\n\n if (toY.time >= this.options.time)\n {\n this.parent.y = toY.end;\n this.toY = null;\n this.parent.emit('bounce-y-end', this.parent);\n }\n else\n {\n this.parent.y = this.ease(toY.time, toY.start, toY.delta, this.options.time);\n }\n }\n }\n\n /** @internal */\n protected calcUnderflowX(): number\n {\n let x: number;\n\n switch (this.underflowX)\n {\n case -1:\n x = 0;\n break;\n case 1:\n x = (this.parent.screenWidth - this.parent.screenWorldWidth);\n break;\n default:\n x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n }\n\n return x;\n }\n\n /** @internal */\n protected calcUnderflowY(): number\n {\n let y: number;\n\n switch (this.underflowY)\n {\n case -1:\n y = 0;\n break;\n case 1:\n y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n break;\n default:\n y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n }\n\n return y;\n }\n\n private oob(): Record<'left' | 'right' | 'top' | 'bottom', boolean> & Record<'topLeft' | 'bottomRight', Point>\n {\n const box = this.options.bounceBox;\n\n if (box)\n {\n const x1 = typeof box.x === 'undefined' ? 0 : box.x;\n const y1 = typeof box.y === 'undefined' ? 0 : box.y;\n const width = typeof box.width === 'undefined' ? this.parent.worldWidth : box.width;\n const height = typeof box.height === 'undefined' ? this.parent.worldHeight : box.height;\n\n return {\n left: this.parent.left < x1,\n right: this.parent.right > width,\n top: this.parent.top < y1,\n bottom: this.parent.bottom > height,\n topLeft: new Point(\n x1 * this.parent.scale.x,\n y1 * this.parent.scale.y\n ),\n bottomRight: new Point(\n width * this.parent.scale.x - this.parent.screenWidth,\n height * this.parent.scale.y - this.parent.screenHeight\n )\n };\n }\n\n return {\n left: this.parent.left < 0,\n right: this.parent.right > this.parent.worldWidth,\n top: this.parent.top < 0,\n bottom: this.parent.bottom > this.parent.worldHeight,\n topLeft: new Point(0, 0),\n bottomRight: new Point(\n this.parent.worldWidth * this.parent.scale.x - this.parent.screenWidth,\n this.parent.worldHeight * this.parent.scale.y - this.parent.screenHeight\n )\n };\n }\n\n public bounce(): void\n {\n if (this.paused)\n {\n return;\n }\n\n let oob;\n let decelerate: undefined | null | {\n percentChangeX?: number;\n percentChangeY?: number;\n x?: number;\n y?: number;\n options?: IDecelerateOptions\n } = this.parent.plugins.get('decelerate', true) as any;\n\n if (decelerate && (decelerate.x || decelerate.y))\n {\n if ((decelerate.x && decelerate.percentChangeX === decelerate.options?.friction) || (decelerate.y && decelerate.percentChangeY === decelerate.options?.friction))\n {\n oob = this.oob();\n if ((oob.left && this.left) || (oob.right && this.right))\n {\n decelerate.percentChangeX = this.options.friction;\n }\n if ((oob.top && this.top) || (oob.bottom && this.bottom))\n {\n decelerate.percentChangeY = this.options.friction;\n }\n }\n }\n const drag: Partial = this.parent.plugins.get('drag', true) || {};\n const pinch: Partial = this.parent.plugins.get('pinch', true) || {};\n\n decelerate = decelerate || {};\n\n if (!drag?.active && !pinch?.active && ((!this.toX || !this.toY) && (!decelerate.x || !decelerate.y)))\n {\n oob = oob || this.oob();\n const topLeft = oob.topLeft;\n const bottomRight = oob.bottomRight;\n\n if (!this.toX && !decelerate.x)\n {\n let x = null;\n\n if (oob.left && this.left)\n {\n x = (this.parent.screenWorldWidth < this.parent.screenWidth) ? this.calcUnderflowX() : -topLeft.x;\n }\n else if (oob.right && this.right)\n {\n x = (this.parent.screenWorldWidth < this.parent.screenWidth) ? this.calcUnderflowX() : -bottomRight.x;\n }\n if (x !== null && this.parent.x !== x)\n {\n this.toX = { time: 0, start: this.parent.x, delta: x - this.parent.x, end: x };\n this.parent.emit('bounce-x-start', this.parent);\n }\n }\n if (!this.toY && !decelerate.y)\n {\n let y = null;\n\n if (oob.top && this.top)\n {\n y = (this.parent.screenWorldHeight < this.parent.screenHeight) ? this.calcUnderflowY() : -topLeft.y;\n }\n else if (oob.bottom && this.bottom)\n {\n y = (this.parent.screenWorldHeight < this.parent.screenHeight) ? this.calcUnderflowY() : -bottomRight.y;\n }\n if (y !== null && this.parent.y !== y)\n {\n this.toY = { time: 0, start: this.parent.y, delta: y - this.parent.y, end: y };\n this.parent.emit('bounce-y-start', this.parent);\n }\n }\n }\n }\n\n public reset(): void\n {\n this.toX = this.toY = null;\n this.bounce();\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\n\n/**\n * There are three ways to clamp:\n * 1. direction: 'all' = the world is clamped to its world boundaries, ie, you cannot drag any part of the world offscreen\n * direction: 'x' | 'y' = only the x or y direction is clamped to its world boundary\n * 2. left, right, top, bottom = true | number = the world is clamped to the world's pixel location for each side;\n * if any of these are set to true, then the location is set to the boundary [0, viewport.worldWidth/viewport.worldHeight]\n * eg: to allow the world to be completely dragged offscreen, set [-viewport.worldWidth, -viewport.worldHeight, viewport.worldWidth * 2, viewport.worldHeight * 2]\n *\n * Underflow determines what happens when the world is smaller than the viewport\n * 1. none = the world is clamped but there is no special behavior\n * 2. center = the world is centered on the viewport\n * 3. combination of top/bottom/center and left/right/center (case insensitive) = the world is stuck to the appropriate boundaries\n *\n */\nexport interface IClampOptions\n{\n /**\n * Clamp left; true = 0\n *\n * @default false\n */\n left?: number | boolean | null;\n\n /**\n * Clamp top; true = 0\n *\n * @default false\n */\n top?: number | boolean | null;\n\n /**\n * Clamp right; true = viewport.worldWidth\n *\n * @default false\n */\n right?: number | boolean | null;\n\n /**\n * Clamp bottom; true = viewport.worldHeight\n *\n * @default false\n */\n bottom?: number | boolean | null;\n\n /**\n * (all, x, or y) using clamps of [0, viewport.worldWidth/viewport.worldHeight]; replaces left/right/top/bottom if set\n *\n * @default null\n */\n direction?: 'all' | 'x' | 'y' | null;\n\n /**\n * Where to place world if too small for screen (e.g., top-right, center, none, bottomleft)\n *\n * @default 'center'\n */\n underflow?: 'center' | string;\n}\n\nconst DEFAULT_CLAMP_OPTIONS: Required = {\n left: false,\n right: false,\n top: false,\n bottom: false,\n direction: null,\n underflow: 'center'\n};\n\n/**\n * Plugin to clamp the viewport to a specific world bounding box.\n *\n * @public\n */\nexport class Clamp extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Required;\n\n /** Last state of viewport */\n protected last: {\n x: number | null;\n y: number | null;\n scaleX: number | null;\n scaleY: number | null;\n };\n\n protected noUnderflow!: boolean;\n protected underflowX!: -1 | 0 | 1;\n protected underflowY!: -1 | 0 | 1;\n\n /**\n * This is called by {@link Viewport.clamp}.\n */\n constructor(parent: Viewport, options : IClampOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_CLAMP_OPTIONS, options);\n\n if (this.options.direction)\n {\n this.options.left = this.options.direction === 'x' || this.options.direction === 'all' ? true : null;\n this.options.right = this.options.direction === 'x' || this.options.direction === 'all' ? true : null;\n this.options.top = this.options.direction === 'y' || this.options.direction === 'all' ? true : null;\n this.options.bottom = this.options.direction === 'y' || this.options.direction === 'all' ? true : null;\n }\n\n this.parseUnderflow();\n this.last = { x: null, y: null, scaleX: null, scaleY: null };\n this.update();\n }\n\n private parseUnderflow()\n {\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'none')\n {\n this.noUnderflow = true;\n }\n else if (clamp === 'center')\n {\n this.underflowX = this.underflowY = 0;\n this.noUnderflow = false;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n this.noUnderflow = false;\n }\n }\n\n public move(): boolean\n {\n this.update();\n\n return false;\n }\n\n public update(): void\n {\n if (this.paused)\n {\n return;\n }\n\n // only clamp on change\n if (this.parent.x === this.last.x\n && this.parent.y === this.last.y\n && this.parent.scale.x === this.last.scaleX\n && this.parent.scale.y === this.last.scaleY)\n {\n return;\n }\n const original = { x: this.parent.x, y: this.parent.y };\n // TODO: Fix\n const decelerate: any = (this.parent.plugins as any).decelerate || {};\n\n if (this.options.left !== null || this.options.right !== null)\n {\n let moved = false;\n\n if (!this.noUnderflow && this.parent.screenWorldWidth < this.parent.screenWidth)\n {\n switch (this.underflowX)\n {\n case -1:\n if (this.parent.x !== 0)\n {\n this.parent.x = 0;\n moved = true;\n }\n break;\n case 1:\n if (this.parent.x !== this.parent.screenWidth - this.parent.screenWorldWidth)\n {\n this.parent.x = this.parent.screenWidth - this.parent.screenWorldWidth;\n moved = true;\n }\n break;\n default:\n if (this.parent.x !== (this.parent.screenWidth - this.parent.screenWorldWidth) / 2)\n {\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n moved = true;\n }\n }\n }\n else\n {\n if (this.options.left !== null)\n {\n if (this.parent.left < (this.options.left === true ? 0 : this.options.left))\n {\n this.parent.x = -(this.options.left === true ? 0 : this.options.left) * this.parent.scale.x;\n decelerate.x = 0;\n moved = true;\n }\n }\n if (this.options.right !== null)\n {\n if (this.parent.right > (this.options.right === true ? this.parent.worldWidth : this.options.right))\n {\n this.parent.x = -(this.options.right === true ? this.parent.worldWidth : this.options.right) * this.parent.scale.x + this.parent.screenWidth;\n decelerate.x = 0;\n moved = true;\n }\n }\n }\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, original, type: 'clamp-x' });\n }\n }\n if (this.options.top !== null || this.options.bottom !== null)\n {\n let moved = false;\n\n if (!this.noUnderflow && this.parent.screenWorldHeight < this.parent.screenHeight)\n {\n switch (this.underflowY)\n {\n case -1:\n if (this.parent.y !== 0)\n {\n this.parent.y = 0;\n moved = true;\n }\n break;\n case 1:\n if (this.parent.y !== this.parent.screenHeight - this.parent.screenWorldHeight)\n {\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n moved = true;\n }\n break;\n default:\n if (this.parent.y !== (this.parent.screenHeight - this.parent.screenWorldHeight) / 2)\n {\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n moved = true;\n }\n }\n }\n else\n {\n if (this.options.top !== null)\n {\n if (this.parent.top < (this.options.top === true ? 0 : this.options.top))\n {\n this.parent.y = -(this.options.top === true ? 0 : this.options.top)\n * this.parent.scale.y;\n decelerate.y = 0;\n moved = true;\n }\n }\n if (this.options.bottom !== null)\n {\n if (this.parent.bottom > (this.options.bottom === true ? this.parent.worldHeight : this.options.bottom))\n {\n this.parent.y = -(this.options.bottom === true ? this.parent.worldHeight : this.options.bottom)\n * this.parent.scale.y + this.parent.screenHeight;\n decelerate.y = 0;\n moved = true;\n }\n }\n }\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, original, type: 'clamp-y' });\n }\n }\n this.last.x = this.parent.x;\n this.last.y = this.parent.y;\n this.last.scaleX = this.parent.scale.x;\n this.last.scaleY = this.parent.scale.y;\n }\n\n public reset(): void\n {\n this.update();\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\n\n/**\n * Options for {@link ClampZoom}.\n *\n * Use either minimum width/height or minimum scale\n */\nexport interface IClampZoomOptions\n{\n /** Minimum width */\n minWidth?: number | null;\n\n /** Minimum height */\n minHeight?: number | null;\n\n /** Maximum width */\n maxWidth?: number | null;\n\n /** Maximum height */\n maxHeight?: number | null;\n\n /** Minimum scale */\n minScale?: number | null;\n\n /** Maximum scale */\n maxScale?: number | null;\n}\n\nconst DEFAULT_CLAMP_ZOOM_OPTIONS: Required = {\n minWidth: null,\n minHeight: null,\n maxWidth: null,\n maxHeight: null,\n minScale: null,\n maxScale: null\n};\n\n/**\n * Plugin to clamp the viewport's zoom to a specific range.\n *\n * @public\n */\nexport class ClampZoom extends Plugin\n{\n public readonly options: Required;\n\n /**\n * This is called by {@link Viewport.clampZoom}.\n */\n constructor(parent: Viewport, options = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_CLAMP_ZOOM_OPTIONS, options);\n\n this.clamp();\n }\n\n public resize(): void\n {\n this.clamp();\n }\n\n /** Clamp the viewport's zoom immediately. */\n public clamp(): void\n {\n if (this.paused)\n {\n return;\n }\n\n if (this.options.minWidth || this.options.minHeight || this.options.maxWidth || this.options.maxHeight)\n {\n let width = this.parent.worldScreenWidth;\n let height = this.parent.worldScreenHeight;\n\n if (this.options.minWidth !== null && width < this.options.minWidth)\n {\n const original = this.parent.scale.x;\n\n this.parent.fitWidth(this.options.minWidth, false, false, true);\n this.parent.scale.y *= this.parent.scale.x / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.maxWidth !== null && width > this.options.maxWidth)\n {\n const original = this.parent.scale.x;\n\n this.parent.fitWidth(this.options.maxWidth, false, false, true);\n this.parent.scale.y *= this.parent.scale.x / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.minHeight !== null && height < this.options.minHeight)\n {\n const original = this.parent.scale.y;\n\n this.parent.fitHeight(this.options.minHeight, false, false, true);\n this.parent.scale.x *= this.parent.scale.y / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.maxHeight !== null && height > this.options.maxHeight)\n {\n const original = this.parent.scale.y;\n\n this.parent.fitHeight(this.options.maxHeight, false, false, true);\n this.parent.scale.x *= this.parent.scale.y / original;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n }\n else\n {\n let scale = this.parent.scale.x;\n\n if (this.options.minScale !== null && scale < this.options.minScale)\n {\n scale = this.options.minScale;\n }\n if (this.options.maxScale !== null && scale > this.options.maxScale)\n {\n scale = this.options.maxScale;\n }\n if (scale !== this.parent.scale.x)\n {\n this.parent.scale.set(scale);\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n }\n }\n\n public reset(): void\n {\n this.clamp();\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Point } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\nexport interface IDecelerateOptions {\n /**\n * Percent to decelerate after movement. This should be between 0 and 1, exclusive.\n *\n * @default 0.95\n */\n friction?: number;\n\n /**\n * Percent to decelerate when past boundaries (only applicable when viewport.bounce() is active)\n *\n * @default 0.8\n */\n bounce?: number;\n\n /**\n * Minimum velocity before stopping/reversing acceleration\n *\n * @default 0.01\n */\n minSpeed?: number;\n}\n\n/** Viewport position snapshot that's saved by {@link DeceleratePlugin} to estimate panning velocity. */\nexport interface IDecelerateSnapshot {\n /** x-coordinate of the viewport. */\n x: number;\n\n /** y-coordinate of the viewport. */\n y: number;\n\n /** Time at which this snapshot was taken. */\n time: number;\n}\n\nconst DEFAULT_DECELERATE_OPTIONS: Required = {\n friction: 0.98,\n bounce: 0.8,\n minSpeed: 0.01\n};\n\n/**\n * Time period of decay (1 frame)\n *\n * @internal\n * @ignore\n */\nconst TP = 16;\n\n/**\n * Plugin to decelerate viewport velocity smoothly after panning ends.\n *\n * @public\n */\nexport class Decelerate extends Plugin\n{\n /** Options used to initialize this plugin. */\n public readonly options: Required;\n\n /**\n * x-component of the velocity of viewport provided by this plugin, at the current time.\n *\n * This is measured in px/frame, where a frame is normalized to 16 milliseconds.\n */\n public x!: number | null;\n\n /**\n * y-component of the velocity of the viewport provided by this plugin, at the current time.\n *\n * This is measured in px/frame, where a frame is normalized to 16 milliseconds.\n */\n public y!: number | null;\n\n /**\n * The decay factor for the x-component of the viewport.\n *\n * The viewport's velocity decreased by this amount each 16 milliseconds.\n */\n public percentChangeX!: number;\n\n /**\n * The decay factor for the y-component of the viewport.\n *\n * The viewport's velocity decreased by this amount each 16 milliseconds.\n */\n public percentChangeY!: number;\n\n /** Saved list of recent viewport position snapshots, to estimate velocity. */\n protected saved: Array;\n\n /** The time since the user released panning of the viewport. */\n protected timeSinceRelease: number;\n\n /**\n * This is called by {@link Viewport.decelerate}.\n */\n constructor(parent: Viewport, options: IDecelerateOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_DECELERATE_OPTIONS, options);\n this.saved = [];\n this.timeSinceRelease = 0;\n\n this.reset();\n this.parent.on('moved', (data) => this.moved(data));\n }\n\n public down(): boolean\n {\n this.saved = [];\n this.x = this.y = null;\n\n return false;\n }\n\n public isActive(): boolean\n {\n return !!(this.x || this.y);\n }\n\n public move(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n const count = this.parent.input.count();\n\n if (count === 1 || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n this.saved.push({ x: this.parent.x, y: this.parent.y, time: performance.now() });\n\n if (this.saved.length > 60)\n {\n this.saved.splice(0, 30);\n }\n }\n\n // Silently recording viewport positions\n return false;\n }\n\n /** Listener to viewport's \"moved\" event. */\n protected moved(data: { type: 'clamp-x' | 'clamp-y'; original: Point }): void\n {\n if (this.saved.length)\n {\n const last = this.saved[this.saved.length - 1];\n\n if (data.type === 'clamp-x')\n {\n if (last.x === data.original.x)\n {\n last.x = this.parent.x;\n }\n }\n else if (data.type === 'clamp-y')\n {\n if (last.y === data.original.y)\n {\n last.y = this.parent.y;\n }\n }\n }\n }\n\n public up(): boolean\n {\n if (this.parent.input.count() === 0 && this.saved.length)\n {\n const now = performance.now();\n\n for (const save of this.saved)\n {\n if (save.time >= now - 100)\n {\n const time = now - save.time;\n\n this.x = (this.parent.x - save.x) / time;\n this.y = (this.parent.y - save.y) / time;\n this.percentChangeX = this.percentChangeY = this.options.friction;\n this.timeSinceRelease = 0;\n break;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Manually activate deceleration, starting from the (x, y) velocity components passed in the options.\n *\n * @param {object} options\n * @param {number} [options.x] - Specify x-component of initial velocity.\n * @param {number} [options.y] - Specify y-component of initial velocity.\n */\n public activate(options: { x?: number; y?: number; }): void\n {\n options = options || {};\n\n if (typeof options.x !== 'undefined')\n {\n this.x = options.x;\n this.percentChangeX = this.options.friction;\n }\n if (typeof options.y !== 'undefined')\n {\n this.y = options.y;\n this.percentChangeY = this.options.friction;\n }\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n /*\n * See https://github.com/davidfig/pixi-viewport/issues/271 for math.\n *\n * The viewport velocity (this.x, this.y) decays exponentially by the the decay factor\n * (this.percentChangeX, this.percentChangeY) each frame. This velocity function is integrated\n * to calculate the displacement.\n */\n\n const moved = this.x || this.y;\n\n const ti = this.timeSinceRelease;\n const tf = this.timeSinceRelease + elapsed;\n\n if (this.x)\n {\n const k = this.percentChangeX;\n const lnk = Math.log(k);\n\n // Apply velocity delta on the viewport x-coordinate.\n this.parent.x += ((this.x * TP) / lnk) * (Math.pow(k, tf / TP) - Math.pow(k, ti / TP));\n\n // Apply decay on x-component of velocity\n this.x *= Math.pow(this.percentChangeX, elapsed / TP);\n }\n if (this.y)\n {\n const k = this.percentChangeY;\n const lnk = Math.log(k);\n\n // Apply velocity delta on the viewport y-coordinate.\n this.parent.y += ((this.y * TP) / lnk) * (Math.pow(k, tf / TP) - Math.pow(k, ti / TP));\n\n // Apply decay on y-component of velocity\n this.y *= Math.pow(this.percentChangeY, elapsed / TP);\n }\n\n this.timeSinceRelease += elapsed;\n\n // End decelerate velocity once it goes under a certain amount of precision.\n if (Math.abs(this.x || 0) < this.options.minSpeed)\n {\n this.x = 0;\n }\n if (Math.abs(this.y || 0) < this.options.minSpeed)\n {\n this.y = 0;\n }\n\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, type: 'decelerate' });\n }\n }\n\n public reset(): void\n {\n this.x = this.y = null;\n }\n}\n","import { Point } from '@pixi/math';\nimport { Plugin } from './Plugin';\n\nimport type { Decelerate } from './Decelerate';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { IPointData } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Drag}. */\nexport interface IDragOptions {\n /**\n * direction to drag\n *\n * @default \"all\"\n */\n direction?: string;\n\n /**\n * whether click to drag is active\n *\n * @default true\n */\n pressDrag?: boolean;\n\n /**\n * Use wheel to scroll in direction (unless wheel plugin is active)\n *\n * @default true\n */\n wheel?: boolean;\n\n /**\n * number of pixels to scroll with each wheel spin\n *\n * @default 1\n */\n wheelScroll?: number;\n\n /**\n * reverse the direction of the wheel scroll\n *\n * @default false\n */\n reverse?: boolean;\n\n /**\n * clamp wheel(to avoid weird bounce with mouse wheel). Can be 'x' or 'y' or `true`.\n *\n * @default false\n */\n clampWheel?: boolean | string;\n\n /**\n * where to place world if too small for screen\n *\n * @default \"center\"\n */\n underflow?: string;\n\n /**\n * factor to multiply drag to increase the speed of movement\n *\n * @default 1\n */\n factor?: number;\n\n /**\n * Changes which mouse buttons trigger drag.\n *\n * Use: 'all', 'left', right' 'middle', or some combination, like, 'middle-right'; you may want to set\n * `viewport.options.disableOnContextMenu` if you want to use right-click dragging.\n *\n * @default \"all\"\n */\n mouseButtons?: 'all' | string;\n\n /**\n * Array containing {@link key|https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code} codes of\n * keys that can be pressed for the drag to be triggered, e.g.: ['ShiftLeft', 'ShiftRight'}.\n *\n * @default null\n */\n keyToPress?: string[] | null;\n\n /**\n * Ignore keyToPress for touch events.\n *\n * @default false\n */\n ignoreKeyToPressOnTouch?: boolean;\n\n /**\n * Scaling factor for non-DOM_DELTA_PIXEL scrolling events.\n *\n * @default 20\n */\n lineHeight?: number;\n}\n\nconst DEFAULT_DRAG_OPTIONS: Required = {\n direction: 'all',\n pressDrag: true,\n wheel: true,\n wheelScroll: 1,\n reverse: false,\n clampWheel: false,\n underflow: 'center',\n factor: 1,\n mouseButtons: 'all',\n keyToPress: null,\n ignoreKeyToPressOnTouch: false,\n lineHeight: 20,\n};\n\n/**\n * Plugin to enable panning/dragging of the viewport to move around.\n *\n * @public\n */\nexport class Drag extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Readonly>;\n\n /** Flags when viewport is moving. */\n protected moved: boolean;\n\n /** Factor to apply from {@link IDecelerateOptions}'s reverse. */\n protected reverse: 1 | -1;\n\n /** Holds whether dragging is enabled along the x-axis. */\n protected xDirection: boolean\n\n /** Holds whether dragging is enabled along the y-axis. */\n protected yDirection: boolean;\n\n /** Flags whether the keys required to drag are pressed currently. */\n protected keyIsPressed: boolean;\n\n /** Holds whether the left, center, and right buttons are required to pan. */\n protected mouse!: [boolean, boolean, boolean]\n\n /** Underflow factor along x-axis */\n protected underflowX!: -1 | 0 | 1;\n\n /** Underflow factor along y-axis */\n protected underflowY!: -1 | 0 | 1;\n\n /** Last pointer position while panning. */\n protected last?: IPointData | null\n\n /** The ID of the pointer currently panning the viewport. */\n protected current?: number;\n\n /**\n * This is called by {@link Viewport.drag}.\n */\n constructor(parent: Viewport, options = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_DRAG_OPTIONS, options);\n this.moved = false;\n this.reverse = this.options.reverse ? 1 : -1;\n this.xDirection = !this.options.direction || this.options.direction === 'all' || this.options.direction === 'x';\n this.yDirection = !this.options.direction || this.options.direction === 'all' || this.options.direction === 'y';\n this.keyIsPressed = false;\n\n this.parseUnderflow();\n this.mouseButtons(this.options.mouseButtons);\n\n if (this.options.keyToPress)\n {\n this.handleKeyPresses(this.options.keyToPress);\n }\n }\n\n /**\n * Handles keypress events and set the keyIsPressed boolean accordingly\n *\n * @param {array} codes - key codes that can be used to trigger drag event\n */\n protected handleKeyPresses(codes: string[]): void\n {\n window.addEventListener('keydown', (e) =>\n {\n if (codes.includes(e.code))\n { this.keyIsPressed = true; }\n });\n\n window.addEventListener('keyup', (e) =>\n {\n if (codes.includes(e.code))\n { this.keyIsPressed = false; }\n });\n }\n\n /**\n * initialize mousebuttons array\n * @param {string} buttons\n */\n protected mouseButtons(buttons: string): void\n {\n if (!buttons || buttons === 'all')\n {\n this.mouse = [true, true, true];\n }\n else\n {\n this.mouse = [\n buttons.indexOf('left') !== -1,\n buttons.indexOf('middle') !== -1,\n buttons.indexOf('right') !== -1\n ];\n }\n }\n\n protected parseUnderflow(): void\n {\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'center')\n {\n this.underflowX = 0;\n this.underflowY = 0;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n }\n }\n\n /**\n * @param {PIXI.InteractionEvent} event\n * @returns {boolean}\n */\n protected checkButtons(event: InteractionEvent): boolean\n {\n const isMouse = event.data.pointerType === 'mouse';\n const count = this.parent.input.count();\n\n if ((count === 1) || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n if (!isMouse || this.mouse[event.data.button])\n {\n return true;\n }\n }\n\n return false;\n }\n\n /**\n * @param {PIXI.InteractionEvent} event\n * @returns {boolean}\n */\n protected checkKeyPress(event: InteractionEvent): boolean\n {\n return (!this.options.keyToPress\n || this.keyIsPressed\n || (this.options.ignoreKeyToPressOnTouch && event.data.pointerType === 'touch'));\n }\n\n public down(event: InteractionEvent): boolean\n {\n if (this.paused || !this.options.pressDrag)\n {\n return false;\n }\n if (this.checkButtons(event) && this.checkKeyPress(event))\n {\n this.last = { x: event.data.global.x, y: event.data.global.y };\n this.current = event.data.pointerId;\n\n return true;\n }\n this.last = null;\n\n return false;\n }\n\n get active(): boolean\n {\n return this.moved;\n }\n\n public move(event: InteractionEvent): boolean\n {\n if (this.paused || !this.options.pressDrag)\n {\n return false;\n }\n if (this.last && this.current === event.data.pointerId)\n {\n const x = event.data.global.x;\n const y = event.data.global.y;\n const count = this.parent.input.count();\n\n if (count === 1 || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n const distX = x - this.last.x;\n const distY = y - this.last.y;\n\n if (this.moved\n || ((this.xDirection && this.parent.input.checkThreshold(distX))\n || (this.yDirection && this.parent.input.checkThreshold(distY))))\n {\n const newPoint = { x, y };\n\n if (this.xDirection)\n {\n this.parent.x += (newPoint.x - this.last.x) * this.options.factor;\n }\n if (this.yDirection)\n {\n this.parent.y += (newPoint.y - this.last.y) * this.options.factor;\n }\n this.last = newPoint;\n if (!this.moved)\n {\n this.parent.emit('drag-start', {\n event,\n screen: new Point(this.last.x, this.last.y),\n world: this.parent.toWorld(new Point(this.last.x, this.last.y)),\n viewport: this.parent\n });\n }\n this.moved = true;\n this.parent.emit('moved', { viewport: this.parent, type: 'drag' });\n\n return true;\n }\n }\n else\n {\n this.moved = false;\n }\n }\n\n return false;\n }\n\n public up(event: InteractionEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n const touches = this.parent.input.touches;\n\n if (touches.length === 1)\n {\n const pointer = touches[0];\n\n if (pointer.last)\n {\n this.last = { x: pointer.last.x, y: pointer.last.y };\n this.current = pointer.id;\n }\n this.moved = false;\n\n return true;\n }\n else if (this.last)\n {\n if (this.moved)\n {\n const screen = new Point(this.last.x, this.last.y);\n\n this.parent.emit('drag-end', {\n event, screen,\n world: this.parent.toWorld(screen),\n viewport: this.parent,\n });\n this.last = null;\n this.moved = false;\n\n return true;\n }\n }\n\n return false;\n }\n\n public wheel(event: WheelEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n if (this.options.wheel)\n {\n const wheel = this.parent.plugins.get('wheel', true);\n\n if (!wheel)\n {\n const step = event.deltaMode ? this.options.lineHeight : 1;\n\n if (this.xDirection)\n {\n this.parent.x += event.deltaX * step * this.options.wheelScroll * this.reverse;\n }\n if (this.yDirection)\n {\n this.parent.y += event.deltaY * step * this.options.wheelScroll * this.reverse;\n }\n if (this.options.clampWheel)\n {\n this.clamp();\n }\n this.parent.emit('wheel-scroll', this.parent);\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n if (!this.parent.options.passiveWheel)\n {\n event.preventDefault();\n }\n\n return true;\n }\n }\n\n return false;\n }\n\n public resume()\n {\n this.last = null;\n this.paused = false;\n }\n\n public clamp()\n {\n const decelerate: Partial = this.parent.plugins.get('decelerate', true) || {};\n\n if (this.options.clampWheel !== 'y')\n {\n if (this.parent.screenWorldWidth < this.parent.screenWidth)\n {\n switch (this.underflowX)\n {\n case -1:\n this.parent.x = 0;\n break;\n case 1:\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth);\n break;\n default:\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n }\n }\n else\n if (this.parent.left < 0)\n {\n this.parent.x = 0;\n decelerate.x = 0;\n }\n else if (this.parent.right > this.parent.worldWidth)\n {\n this.parent.x = -this.parent.worldWidth * this.parent.scale.x + this.parent.screenWidth;\n decelerate.x = 0;\n }\n }\n if (this.options.clampWheel !== 'x')\n {\n if (this.parent.screenWorldHeight < this.parent.screenHeight)\n {\n switch (this.underflowY)\n {\n case -1:\n this.parent.y = 0;\n break;\n case 1:\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n break;\n default:\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n }\n }\n else\n {\n if (this.parent.top < 0)\n {\n this.parent.y = 0;\n decelerate.y = 0;\n }\n if (this.parent.bottom > this.parent.worldHeight)\n {\n this.parent.y = -this.parent.worldHeight * this.parent.scale.y + this.parent.screenHeight;\n decelerate.y = 0;\n }\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { DisplayObject } from '@pixi/display';\nimport type { IPointData } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Follow}. */\nexport interface IFollowOptions\n{\n /**\n * Speed to follow in px/frame (0 = teleport to location)\n *\n * @default 9\n */\n speed?: number;\n\n /**\n * Set acceleration to accelerate and decelerate at this rate; speed cannot be 0 to use acceleration\n *\n * @default null\n */\n acceleration?: number | null;\n\n /**\n * Radius (in world coordinates) of center circle where movement is allowed without moving the viewport\n *\n * @default null\n */\n radius?: number | null;\n}\n\nconst DEFAULT_FOLLOW_OPTIONS: Required = {\n speed: 0,\n acceleration: null,\n radius: null\n};\n\n/**\n * Plugin to follow a display-object.\n *\n * @see Viewport.follow\n * @public\n */\nexport class Follow extends Plugin\n{\n /** The options used to initialize this plugin. */\n public readonly options: Required;\n\n /** The target this plugin will make the viewport follow. */\n public target: DisplayObject;\n\n /** The velocity provided the viewport by following, at the current time. */\n protected velocity: IPointData;\n\n /**\n * This is called by {@link Viewport.follow}.\n *\n * @param parent\n * @param target - target to follow\n * @param options\n */\n constructor(parent: Viewport, target: DisplayObject, options: IFollowOptions = {})\n {\n super(parent);\n\n this.target = target;\n this.options = Object.assign({}, DEFAULT_FOLLOW_OPTIONS, options);\n this.velocity = { x: 0, y: 0 };\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n const center = this.parent.center;\n let toX = this.target.x;\n let toY = this.target.y;\n\n if (this.options.radius)\n {\n const distance = Math.sqrt(Math.pow(this.target.y - center.y, 2) + Math.pow(this.target.x - center.x, 2));\n\n if (distance > this.options.radius)\n {\n const angle = Math.atan2(this.target.y - center.y, this.target.x - center.x);\n\n toX = this.target.x - (Math.cos(angle) * this.options.radius);\n toY = this.target.y - (Math.sin(angle) * this.options.radius);\n }\n else\n {\n return;\n }\n }\n\n const deltaX = toX - center.x;\n const deltaY = toY - center.y;\n\n if (deltaX || deltaY)\n {\n if (this.options.speed)\n {\n if (this.options.acceleration)\n {\n const angle = Math.atan2(toY - center.y, toX - center.x);\n const distance = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));\n\n if (distance)\n {\n const decelerationDistance = (Math.pow(this.velocity.x, 2) + Math.pow(this.velocity.y, 2)) / (2 * this.options.acceleration);\n\n if (distance > decelerationDistance)\n {\n this.velocity = {\n x: Math.min(this.velocity.x + this.options.acceleration * elapsed, this.options.speed),\n y: Math.min(this.velocity.y + this.options.acceleration * elapsed, this.options.speed)\n };\n }\n else\n {\n this.velocity = {\n x: Math.max(this.velocity.x - this.options.acceleration * this.options.speed, 0),\n y: Math.max(this.velocity.y - this.options.acceleration * this.options.speed, 0)\n };\n }\n const changeX = Math.cos(angle) * this.velocity.x;\n const changeY = Math.sin(angle) * this.velocity.y;\n const x = Math.abs(changeX) > Math.abs(deltaX) ? toX : center.x + changeX;\n const y = Math.abs(changeY) > Math.abs(deltaY) ? toY : center.y + changeY;\n\n this.parent.moveCenter(x, y);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n else\n {\n const angle = Math.atan2(toY - center.y, toX - center.x);\n const changeX = Math.cos(angle) * this.options.speed;\n const changeY = Math.sin(angle) * this.options.speed;\n const x = Math.abs(changeX) > Math.abs(deltaX) ? toX : center.x + changeX;\n const y = Math.abs(changeY) > Math.abs(deltaY) ? toY : center.y + changeY;\n\n this.parent.moveCenter(x, y);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n else\n {\n this.parent.moveCenter(toX, toY);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\nimport type { InteractionEvent } from '@pixi/interaction';\n\n/** Insets for mouse edges scrolling regions */\nexport interface IMouseEdgesInsets {\n /** Distance from center of screen in screen pixels */\n radius?: number | null;\n\n /** Distance from all sides in screen pixels */\n distance?: number | null;\n\n /** Alternatively, set top distance (leave unset for no top scroll) */\n top?: number | null;\n\n /** Alternatively, set bottom distance (leave unset for no top scroll) */\n bottom?: number | null;\n\n /** Alternatively, set left distance (leave unset for no top scroll) */\n left?: number | null;\n\n /** Alternatively, set right distance (leave unset for no top scroll) */\n right?: number | null;\n}\n\n/** Options for {@link MouseEdges}. */\nexport interface IMouseEdgesOptions extends IMouseEdgesInsets {\n /** Speed in pixels/frame to scroll viewport */\n speed?: number;\n\n /** Reverse direction of scroll */\n reverse?: boolean;\n\n /** Don't use decelerate plugin even if it's installed */\n noDecelerate?: boolean;\n\n /**\n * If using radius, use linear movement (+/- 1, +/- 1) instead of angled movement.\n *\n * (Math.cos(angle from center), Math.sin(angle from center))\n */\n linear?: boolean;\n\n /** Allows plugin to continue working even when there's a `mousedown` event. */\n allowButtons?: boolean;\n}\n\nconst MOUSE_EDGES_OPTIONS: Required = {\n radius: null,\n distance: null,\n top: null,\n bottom: null,\n left: null,\n right: null,\n speed: 8,\n reverse: false,\n noDecelerate: false,\n linear: false,\n allowButtons: false,\n};\n\n/**\n * Scroll viewport when mouse hovers near one of the edges.\n *\n * @event mouse-edge-start(Viewport) emitted when mouse-edge starts\n * @event mouse-edge-end(Viewport) emitted when mouse-edge ends\n */\nexport class MouseEdges extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Readonly>;\n\n /** Factor from reverse option. */\n protected readonly reverse: -1 | 1;\n\n /** Radius squared */\n protected readonly radiusSquared: number | null;\n\n /** Scroll region size on the left side. */\n protected left!: number | null;\n\n /** Scroll region size on the top size. */\n protected top!: number | null;\n\n /** Scroll region size on the right side. */\n protected right!: number | null;\n\n /** Scroll region size on the bottom side. */\n protected bottom!: number | null;\n\n protected horizontal?: number | null;\n\n protected vertical?: number | null;\n\n /**\n * This is called by {@link Viewport.mouseEdges}.\n */\n constructor(parent: Viewport, options: IMouseEdgesOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, MOUSE_EDGES_OPTIONS, options);\n this.reverse = this.options.reverse ? 1 : -1;\n this.radiusSquared = typeof this.options.radius === 'number' ? Math.pow(this.options.radius, 2) : null;\n\n this.resize();\n }\n\n public resize(): void\n {\n const distance = this.options.distance;\n\n if (distance !== null)\n {\n this.left = distance;\n this.top = distance;\n this.right = this.parent.worldScreenWidth - distance;\n this.bottom = this.parent.worldScreenHeight - distance;\n }\n else if (!this.options.radius)\n {\n this.left = this.options.left;\n this.top = this.options.top;\n this.right = this.options.right === null ? null : this.parent.worldScreenWidth - this.options.right;\n this.bottom = this.options.bottom === null ? null : this.parent.worldScreenHeight - this.options.bottom;\n }\n }\n\n public down(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if (!this.options.allowButtons)\n {\n this.horizontal = this.vertical = null;\n }\n\n return false;\n }\n\n public move(event: InteractionEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if ((event.data.pointerType !== 'mouse' && event.data.identifier !== 1)\n || (!this.options.allowButtons && event.data.buttons !== 0))\n {\n return false;\n }\n\n const x = event.data.global.x;\n const y = event.data.global.y;\n\n if (this.radiusSquared)\n {\n const center = this.parent.toScreen(this.parent.center);\n const distance = Math.pow(center.x - x, 2) + Math.pow(center.y - y, 2);\n\n if (distance >= this.radiusSquared)\n {\n const angle = Math.atan2(center.y - y, center.x - x);\n\n if (this.options.linear)\n {\n this.horizontal = Math.round(Math.cos(angle)) * this.options.speed * this.reverse * (60 / 1000);\n this.vertical = Math.round(Math.sin(angle)) * this.options.speed * this.reverse * (60 / 1000);\n }\n else\n {\n this.horizontal = Math.cos(angle) * this.options.speed * this.reverse * (60 / 1000);\n this.vertical = Math.sin(angle) * this.options.speed * this.reverse * (60 / 1000);\n }\n }\n else\n {\n if (this.horizontal)\n {\n this.decelerateHorizontal();\n }\n if (this.vertical)\n {\n this.decelerateVertical();\n }\n\n this.horizontal = this.vertical = 0;\n }\n }\n else\n {\n if (this.left !== null && x < this.left)\n {\n this.horizontal = Number(this.reverse) * this.options.speed * (60 / 1000);\n }\n else if (this.right !== null && x > this.right)\n {\n this.horizontal = -1 * this.reverse * this.options.speed * (60 / 1000);\n }\n else\n {\n this.decelerateHorizontal();\n this.horizontal = 0;\n }\n if (this.top !== null && y < this.top)\n {\n this.vertical = Number(this.reverse) * this.options.speed * (60 / 1000);\n }\n else if (this.bottom !== null && y > this.bottom)\n {\n this.vertical = -1 * this.reverse * this.options.speed * (60 / 1000);\n }\n else\n {\n this.decelerateVertical();\n this.vertical = 0;\n }\n }\n\n return false;\n }\n\n private decelerateHorizontal(): void\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (this.horizontal && decelerate && !this.options.noDecelerate)\n {\n decelerate.activate({ x: (this.horizontal * this.options.speed * this.reverse) / (1000 / 60) });\n }\n }\n\n private decelerateVertical(): void\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (this.vertical && decelerate && !this.options.noDecelerate)\n {\n decelerate.activate({ y: (this.vertical * this.options.speed * this.reverse) / (1000 / 60) });\n }\n }\n\n public up(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if (this.horizontal)\n {\n this.decelerateHorizontal();\n }\n if (this.vertical)\n {\n this.decelerateVertical();\n }\n this.horizontal = this.vertical = null;\n\n return false;\n }\n\n public update(): void\n {\n if (this.paused)\n {\n return;\n }\n\n if (this.horizontal || this.vertical)\n {\n const center = this.parent.center;\n\n if (this.horizontal)\n {\n center.x += this.horizontal * this.options.speed;\n }\n if (this.vertical)\n {\n center.y += this.vertical * this.options.speed;\n }\n\n this.parent.moveCenter(center);\n this.parent.emit('moved', { viewport: this.parent, type: 'mouse-edges' });\n }\n }\n}\n","import { Plugin } from './Plugin';\nimport { Point } from '@pixi/math';\n\nimport type { IPointData } from '@pixi/math';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { IViewportTouch } from '../InputManager';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Pinch}. */\nexport interface IPinchOptions\n{\n /** Disable two-finger dragging. */\n noDrag?: boolean;\n\n /**\n * Percent to modify pinch speed.\n *\n * @default 1\n */\n percent?: number;\n\n /**\n * Factor to multiply two-finger drag to increase speed of movement\n *\n * @default 1\n */\n factor?: number;\n\n /** Place this point at center during zoom instead of center of two fingers */\n center?: Point | null;\n\n /** Axis to zoom */\n axis?: 'all' | 'x' | 'y';\n}\n\nconst DEFAULT_PINCH_OPTIONS: Required = {\n noDrag: false,\n percent: 1,\n center: null,\n factor: 1,\n axis: 'all',\n};\n\n/**\n * Plugin for enabling two-finger pinching (or dragging).\n *\n * @public\n */\nexport class Pinch extends Plugin\n{\n /** Options used to initialize this plugin. */\n public readonly options: Required;\n\n /** Flags whether this plugin is active, i.e. a pointer is down on the viewport. */\n public active = false;\n\n /** Flags whether the viewport is being pinched. */\n public pinching = false;\n\n protected moved = false;\n protected lastCenter?: IPointData | null;\n\n /**\n * This is called by {@link Viewport.pinch}.\n */\n constructor(parent: Viewport, options: IPinchOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_PINCH_OPTIONS, options);\n }\n\n public down(): boolean\n {\n if (this.parent.input.count() >= 2)\n {\n this.active = true;\n return true;\n }\n\n return false;\n }\n\n public isAxisX(): boolean\n {\n return ['all', 'x'].includes(this.options.axis);\n }\n\n public isAxisY(): boolean\n {\n return ['all', 'y'].includes(this.options.axis);\n }\n\n public move(e: InteractionEvent): boolean\n {\n if (this.paused || !this.active)\n {\n return false;\n }\n\n const x = e.data.global.x;\n const y = e.data.global.y;\n\n const pointers = this.parent.input.touches;\n\n if (pointers.length >= 2)\n {\n const first = pointers[0] as IViewportTouch;\n const second = pointers[1] as IViewportTouch;\n const last = (first.last && second.last)\n ? Math.sqrt(Math.pow(second.last.x - first.last.x, 2) + Math.pow(second.last.y - first.last.y, 2))\n : null;\n\n if (first.id === e.data.pointerId)\n {\n first.last = { x, y, data: e.data } as IPointData;\n }\n else if (second.id === e.data.pointerId)\n {\n second.last = { x, y, data: e.data } as IPointData;\n }\n if (last)\n {\n let oldPoint: IPointData | undefined;\n\n const point = {\n x: (first.last as IPointData).x +\n ((second.last as IPointData).x - (first.last as IPointData).x) / 2,\n y: (first.last as IPointData).y +\n ((second.last as IPointData).y - (first.last as IPointData).y) / 2,\n };\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point);\n }\n let dist = Math.sqrt(Math.pow(\n (second.last as IPointData).x - (first.last as IPointData).x, 2) +\n Math.pow((second.last as IPointData).y - (first.last as IPointData).y, 2));\n\n dist = dist === 0 ? dist = 0.0000000001 : dist;\n\n const change = (1 - last / dist) * this.options.percent\n * (this.isAxisX() ? this.parent.scale.x : this.parent.scale.y);\n\n if (this.isAxisX())\n {\n this.parent.scale.x += change;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y += change;\n }\n\n this.parent.emit('zoomed', { viewport: this.parent, type: 'pinch', center: point });\n\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += (point.x - newPoint.x) * this.options.factor;\n this.parent.y += (point.y - newPoint.y) * this.options.factor;\n this.parent.emit('moved', { viewport: this.parent, type: 'pinch' });\n }\n if (!this.options.noDrag && this.lastCenter)\n {\n this.parent.x += (point.x - this.lastCenter.x) * this.options.factor;\n this.parent.y += (point.y - this.lastCenter.y) * this.options.factor;\n this.parent.emit('moved', { viewport: this.parent, type: 'pinch' });\n }\n\n this.lastCenter = point;\n this.moved = true;\n }\n else if (!this.pinching)\n {\n this.parent.emit('pinch-start', this.parent);\n this.pinching = true;\n }\n\n return true;\n }\n\n return false;\n }\n\n public up(): boolean\n {\n if (this.pinching)\n {\n if (this.parent.input.touches.length <= 1)\n {\n this.active = false;\n this.lastCenter = null;\n this.pinching = false;\n this.moved = false;\n this.parent.emit('pinch-end', this.parent);\n\n return true;\n }\n }\n\n return false;\n }\n}\n","import { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Viewport } from '../Viewport';\n\nexport interface ISnapOptions\n{\n /** snap to the top-left of viewport instead of center */\n topLeft?: boolean;\n\n /**\n * Friction/frame to apply if decelerate is active\n *\n * @default 0.8\n */\n friction?: number;\n\n /**\n * @default 1000\n */\n time?: number;\n\n /** Easing function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /**\n * Pause snapping with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Removes this plugin after snapping is complete\n *\n * @default false\n */\n removeOnComplete?: boolean;\n\n /**\n * Removes this plugin if interrupted by any user input\n *\n * @default false\n */\n removeOnInterrupt?: boolean;\n\n /**\n * Starts the snap immediately regardless of whether the viewport is at the desired location\n *\n * @default false\n */\n forceStart?: boolean;\n}\n\nconst DEFAULT_SNAP_OPTIONS: Required = {\n topLeft: false,\n friction: 0.8,\n time: 1000,\n ease: 'easeInOutSine',\n interrupt: true,\n removeOnComplete: false,\n removeOnInterrupt: false,\n forceStart: false\n};\n\n/**\n * @event snap-start(Viewport) emitted each time a snap animation starts\n * @event snap-restart(Viewport) emitted each time a snap resets because of a change in viewport size\n * @event snap-end(Viewport) emitted each time snap reaches its target\n * @event snap-remove(Viewport) emitted if snap plugin is removed\n */\nexport class Snap extends Plugin\n{\n public readonly options: Required;\n public ease?: any;\n public x: number;\n public y: number;\n\n protected percent?: number;\n protected snapping?: { time: number } | null;\n protected deltaX?: number;\n protected deltaY?: number;\n protected startX?: number;\n protected startY?: number;\n\n /**\n * This is called by {@link Viewport.snap}.\n */\n constructor(parent: Viewport, x: number, y: number, options: ISnapOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_SNAP_OPTIONS, options);\n this.ease = ease(options.ease, 'easeInOutSine');\n this.x = x;\n this.y = y;\n\n if (this.options.forceStart)\n {\n this.snapStart();\n }\n }\n\n public snapStart(): void\n {\n this.percent = 0;\n this.snapping = { time: 0 };\n const current = this.options.topLeft ? this.parent.corner : this.parent.center;\n\n this.deltaX = this.x - current.x;\n this.deltaY = this.y - current.y;\n this.startX = current.x;\n this.startY = current.y;\n this.parent.emit('snap-start', this.parent);\n }\n\n public wheel(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap');\n }\n\n return false;\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap');\n }\n else if (this.options.interrupt)\n {\n this.snapping = null;\n }\n\n return false;\n }\n\n public up(): boolean\n {\n if (this.parent.input.count() === 0)\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (decelerate && (decelerate.x || decelerate.y))\n {\n decelerate.percentChangeX = decelerate.percentChangeY = this.options.friction;\n }\n }\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n if (this.options.interrupt && this.parent.input.count() !== 0)\n {\n return;\n }\n if (!this.snapping)\n {\n const current = this.options.topLeft ? this.parent.corner : this.parent.center;\n\n if (current.x !== this.x || current.y !== this.y)\n {\n this.snapStart();\n }\n }\n else\n {\n const snapping = this.snapping;\n\n snapping.time += elapsed;\n let finished;\n let x;\n let y;\n\n const startX = this.startX as number;\n const startY = this.startY as number;\n const deltaX = this.deltaX as number;\n const deltaY = this.deltaY as number;\n\n if (snapping.time > this.options.time)\n {\n finished = true;\n x = startX + deltaX;\n y = startY + deltaY;\n }\n else\n {\n const percent = this.ease(snapping.time, 0, 1, this.options.time);\n\n x = startX + (deltaX * percent);\n y = startY + (deltaY * percent);\n }\n if (this.options.topLeft)\n {\n this.parent.moveCorner(x, y);\n }\n else\n {\n this.parent.moveCenter(x, y);\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'snap' });\n\n if (finished)\n {\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap');\n }\n this.parent.emit('snap-end', this.parent);\n this.snapping = null;\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Point } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link SnapZoom}. */\nexport interface ISnapZoomOptions\n{\n /** the desired width to snap (to maintain aspect ratio, choose only width or height) */\n width?: number;\n\n /** the desired height to snap (to maintain aspect ratio, choose only width or height) */\n height?: number;\n\n /**\n * time for snapping in ms\n *\n * @default 1000\n */\n time?: number;\n\n /** ease function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /** Place this point at center during zoom instead of center of the viewport */\n center?: Point | null;\n\n /**\n * Pause snapping with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Removes this plugin after snapping is complete\n *\n * @default false\n */\n removeOnComplete?: boolean;\n\n /**\n * Removes this plugin if interrupted by any user input\n *\n * @default false\n */\n removeOnInterrupt?: boolean;\n\n /**\n * Starts the snap immediately regardless of whether the viewport is at the desired zoom\n *\n * @default false\n */\n forceStart?: boolean;\n\n /**\n * Zoom but do not move\n *\n * @default false\n */\n noMove?: boolean;\n}\n\nconst DEFAULT_SNAP_ZOOM_OPTIONS: Required = {\n width: 0,\n height: 0,\n time: 1000,\n ease: 'easeInOutSine',\n center: null,\n interrupt: true,\n removeOnComplete: false,\n removeOnInterrupt: false,\n forceStart: false,\n noMove: false\n};\n\n/**\n * @event snap-zoom-start(Viewport) emitted each time a fit animation starts\n * @event snap-zoom-end(Viewport) emitted each time fit reaches its target\n * @event snap-zoom-end(Viewport) emitted each time fit reaches its target\n */\nexport class SnapZoom extends Plugin\n{\n public readonly options: Required;\n\n protected ease: any;\n protected xScale: number;\n protected yScale: number;\n protected xIndependent: boolean;\n protected yIndependent: boolean;\n protected snapping?: {\n time: number;\n startX: number;\n startY: number;\n deltaX: number;\n deltaY: number;\n } | null;\n\n /**\n * This is called by {@link Viewport.snapZoom}.\n */\n constructor(parent: Viewport, options: ISnapZoomOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_SNAP_ZOOM_OPTIONS, options);\n this.ease = ease(this.options.ease);\n\n // Assign defaults for typescript.\n this.xIndependent = false;\n this.yIndependent = false;\n this.xScale = 0;\n this.yScale = 0;\n\n if (this.options.width > 0)\n {\n this.xScale = parent.screenWidth / this.options.width;\n this.xIndependent = true;\n }\n if (this.options.height > 0)\n {\n this.yScale = parent.screenHeight / this.options.height;\n this.yIndependent = true;\n }\n\n this.xScale = this.xIndependent ? (this.xScale as number) : (this.yScale as number);\n this.yScale = this.yIndependent ? (this.yScale as number) : this.xScale;\n\n if (this.options.time === 0)\n {\n // TODO: Fix this\n // @ts-expect-error todo\n parent.container.scale.x = this.xScale;\n\n // TODO: Fix this\n // @ts-expect-error todo\n parent.container.scale.y = this.yScale;\n\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n }\n else if (options.forceStart)\n {\n this.createSnapping();\n }\n }\n\n private createSnapping(): void\n {\n const startWorldScreenWidth = this.parent.worldScreenWidth;\n const startWorldScreenHeight = this.parent.worldScreenHeight;\n const endWorldScreenWidth = this.parent.screenWidth / this.xScale;\n const endWorldScreenHeight = this.parent.screenHeight / this.yScale;\n\n this.snapping = {\n time: 0,\n startX: startWorldScreenWidth,\n startY: startWorldScreenHeight,\n deltaX: endWorldScreenWidth - startWorldScreenWidth,\n deltaY: endWorldScreenHeight - startWorldScreenHeight\n };\n\n this.parent.emit('snap-zoom-start', this.parent);\n }\n\n public resize(): void\n {\n this.snapping = null;\n\n if (this.options.width > 0)\n {\n this.xScale = this.parent.screenWidth / this.options.width;\n }\n if (this.options.height > 0)\n {\n this.yScale = this.parent.screenHeight / this.options.height;\n }\n this.xScale = this.xIndependent ? this.xScale : this.yScale;\n this.yScale = this.yIndependent ? this.yScale : this.xScale;\n }\n\n public wheel(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n\n return false;\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n else if (this.options.interrupt)\n {\n this.snapping = null;\n }\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n if (this.options.interrupt && this.parent.input.count() !== 0)\n {\n return;\n }\n\n let oldCenter: Point | undefined;\n\n if (!this.options.center && !this.options.noMove)\n {\n oldCenter = this.parent.center;\n }\n if (!this.snapping)\n {\n if (this.parent.scale.x !== this.xScale || this.parent.scale.y !== this.yScale)\n {\n this.createSnapping();\n }\n }\n else if (this.snapping)\n {\n const snapping = this.snapping;\n\n snapping.time += elapsed;\n\n if (snapping.time >= this.options.time)\n {\n this.parent.scale.set(this.xScale, this.yScale);\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n this.parent.emit('snap-zoom-end', this.parent);\n this.snapping = null;\n }\n else\n {\n const snapping = this.snapping;\n const worldScreenWidth = this.ease(snapping.time, snapping.startX, snapping.deltaX, this.options.time);\n const worldScreenHeight = this.ease(snapping.time, snapping.startY, snapping.deltaY, this.options.time);\n\n this.parent.scale.x = this.parent.screenWidth / worldScreenWidth;\n this.parent.scale.y = this.parent.screenHeight / worldScreenHeight;\n }\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (!this.options.noMove)\n {\n if (!this.options.center)\n {\n this.parent.moveCenter(oldCenter as Point);\n }\n else\n {\n this.parent.moveCenter(this.options.center);\n }\n }\n }\n }\n\n public resume(): void\n {\n this.snapping = null;\n super.resume();\n }\n}\n","import { Plugin } from './Plugin';\nimport { IPointData, Point } from '@pixi/math';\n\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Wheel}. */\nexport interface IWheelOptions\n{\n /**\n * Percent to scroll with each spin\n *\n * @default 0.1\n */\n percent?: number;\n\n /**\n * smooth the zooming by providing the number of frames to zoom between wheel spins\n *\n * @default false\n */\n smooth?: false | number;\n\n /**\n * Stop smoothing with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Reverse the direction of the scroll\n *\n * @default false\n */\n reverse?: boolean;\n\n /**\n * Place this point at center during zoom instead of current mouse position\n *\n * @default null\n */\n center?: Point | null;\n\n /**\n * Scaling factor for non-DOM_DELTA_PIXEL scrolling events\n *\n * @default 20\n */\n lineHeight?: number;\n\n /**\n * Axis to zoom\n *\n * @default 'all'\n */\n axis?: 'all' | 'x' | 'y';\n}\n\nconst DEFAULT_WHEEL_OPTIONS: Required = {\n percent: 0.1,\n smooth: false,\n interrupt: true,\n reverse: false,\n center: null,\n lineHeight: 20,\n axis: 'all',\n};\n\n/**\n * Plugin for handling wheel scrolling for viewport zoom.\n *\n * @event wheel({wheel: {dx, dy, dz}, event, viewport})\n */\nexport class Wheel extends Plugin\n{\n public readonly options: Required;\n\n protected smoothing?: IPointData | null;\n protected smoothingCenter?: Point | null;\n protected smoothingCount?: number;\n\n /**\n * This is called by {@link Viewport.wheel}.\n */\n constructor(parent: Viewport, options: IWheelOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_WHEEL_OPTIONS, options);\n }\n\n public down(): boolean\n {\n if (this.options.interrupt)\n {\n this.smoothing = null;\n }\n\n return false;\n }\n\n protected isAxisX(): boolean\n {\n return ['all', 'x'].includes(this.options.axis);\n }\n\n protected isAxisY(): boolean\n {\n return ['all', 'y'].includes(this.options.axis);\n }\n\n public update(): void\n {\n if (this.smoothing)\n {\n const point = this.smoothingCenter;\n const change = this.smoothing;\n let oldPoint;\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point as IPointData);\n }\n if (this.isAxisX())\n {\n this.parent.scale.x += change.x;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y += change.y;\n }\n\n this.parent.emit('zoomed', { viewport: this.parent, type: 'wheel' });\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += (point as IPointData).x - newPoint.x;\n this.parent.y += (point as IPointData).y - newPoint.y;\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n (this.smoothingCount as number)++;\n\n if (this.smoothingCount as number >= this.options.smooth)\n {\n this.smoothing = null;\n }\n }\n }\n\n public wheel(e: WheelEvent): boolean | undefined\n {\n if (this.paused)\n {\n return;\n }\n\n const point = this.parent.input.getPointerPosition(e);\n const sign = this.options.reverse ? -1 : 1;\n const step = sign * -e.deltaY * (e.deltaMode ? this.options.lineHeight : 1) / 500;\n const change = Math.pow(2, (1 + this.options.percent) * step);\n\n if (this.options.smooth)\n {\n const original = {\n x: this.smoothing ? this.smoothing.x * (this.options.smooth - (this.smoothingCount as number)) : 0,\n y: this.smoothing ? this.smoothing.y * (this.options.smooth - (this.smoothingCount as number)) : 0\n };\n\n this.smoothing = {\n x: ((this.parent.scale.x + original.x) * change - this.parent.scale.x) / this.options.smooth,\n y: ((this.parent.scale.y + original.y) * change - this.parent.scale.y) / this.options.smooth,\n };\n this.smoothingCount = 0;\n this.smoothingCenter = point;\n }\n else\n {\n let oldPoint: IPointData | undefined;\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point);\n }\n if (this.isAxisX())\n {\n this.parent.scale.x *= change;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y *= change;\n }\n this.parent.emit('zoomed', { viewport: this.parent, type: 'wheel' });\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += point.x - newPoint.x;\n this.parent.y += point.y - newPoint.y;\n }\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n this.parent.emit('wheel', { wheel: { dx: e.deltaX, dy: e.deltaY, dz: e.deltaZ }, event: e, viewport: this.parent });\n\n if (!this.parent.options.passiveWheel)\n {\n return true;\n }\n }\n}\n","import { Point, Rectangle } from '@pixi/math';\n\nimport type { IPointData } from '@pixi/math';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { Viewport } from './Viewport';\n\nexport interface IViewportTouch {\n id: number;\n last: IPointData | null;\n}\n\n/**\n * Handles all input for Viewport\n *\n * @internal\n * @ignore\n * @private\n */\nexport class InputManager\n{\n public readonly viewport: Viewport;\n\n public clickedAvailable?: boolean;\n public isMouseDown?: boolean;\n public last?: Point | null;\n public wheelFunction?: (e: WheelEvent) => void;\n /** List of active touches on viewport */\n public touches: IViewportTouch[];\n\n constructor(viewport: Viewport)\n {\n this.viewport = viewport;\n this.touches = [];\n\n this.addListeners();\n }\n\n /** Add input listeners */\n private addListeners()\n {\n this.viewport.interactive = true;\n if (!this.viewport.forceHitArea)\n {\n this.viewport.hitArea = new Rectangle(0, 0, this.viewport.worldWidth, this.viewport.worldHeight);\n }\n this.viewport.on('pointerdown', this.down, this);\n this.viewport.on('pointermove', this.move, this);\n this.viewport.on('pointerup', this.up, this);\n this.viewport.on('pointerupoutside', this.up, this);\n this.viewport.on('pointercancel', this.up, this);\n this.viewport.on('pointerout', this.up, this);\n this.wheelFunction = (e) => this.handleWheel(e);\n this.viewport.options.divWheel.addEventListener(\n 'wheel',\n this.wheelFunction as any,\n { passive: this.viewport.options.passiveWheel });\n this.isMouseDown = false;\n }\n\n /**\n * Removes all event listeners from viewport\n * (useful for cleanup of wheel when removing viewport)\n */\n public destroy()\n {\n this.viewport.options.divWheel.removeEventListener('wheel', this.wheelFunction as any);\n }\n\n /**\n * handle down events for viewport\n *\n * @param {PIXI.InteractionEvent} event\n */\n public down(event: InteractionEvent)\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n if (event.data.pointerType === 'mouse')\n {\n this.isMouseDown = true;\n }\n else if (!this.get(event.data.pointerId))\n {\n this.touches.push({ id: event.data.pointerId, last: null });\n }\n if (this.count() === 1)\n {\n this.last = event.data.global.clone();\n\n // clicked event does not fire if viewport is decelerating or bouncing\n const decelerate = this.viewport.plugins.get('decelerate', true);\n const bounce = this.viewport.plugins.get('bounce', true);\n\n if ((!decelerate || !decelerate.isActive()) && (!bounce || !bounce.isActive()))\n {\n this.clickedAvailable = true;\n }\n else\n {\n this.clickedAvailable = false;\n }\n }\n else\n {\n this.clickedAvailable = false;\n }\n\n const stop = this.viewport.plugins.down(event);\n\n if (stop && this.viewport.options.stopPropagation)\n {\n event.stopPropagation();\n }\n }\n\n /** Clears all pointer events */\n public clear(): void\n {\n this.isMouseDown = false;\n this.touches = [];\n this.last = null;\n }\n\n /**\n * @param {number} change\n * @returns whether change exceeds threshold\n */\n public checkThreshold(change: number): boolean\n {\n if (Math.abs(change) >= this.viewport.threshold)\n {\n return true;\n }\n\n return false;\n }\n\n /** Handle move events for viewport */\n public move(event: InteractionEvent): void\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n\n const stop = this.viewport.plugins.move(event);\n\n if (this.clickedAvailable && this.last)\n {\n const distX = event.data.global.x - this.last.x;\n const distY = event.data.global.y - this.last.y;\n\n if (this.checkThreshold(distX) || this.checkThreshold(distY))\n {\n this.clickedAvailable = false;\n }\n }\n\n if (stop && this.viewport.options.stopPropagation)\n {\n event.stopPropagation();\n }\n }\n\n /** Handle up events for viewport */\n public up(event: InteractionEvent): void\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n\n if (event.data.pointerType === 'mouse')\n {\n this.isMouseDown = false;\n }\n\n if (event.data.pointerType !== 'mouse')\n {\n this.remove(event.data.pointerId);\n }\n\n const stop = this.viewport.plugins.up(event);\n\n if (this.clickedAvailable && this.count() === 0 && this.last)\n {\n this.viewport.emit('clicked', {\n event,\n screen: this.last,\n world: this.viewport.toWorld(this.last),\n viewport: this\n });\n this.clickedAvailable = false;\n }\n\n if (stop && this.viewport.options.stopPropagation)\n {\n event.stopPropagation();\n }\n }\n\n /** Gets pointer position if this.interaction is set */\n public getPointerPosition(event: WheelEvent): Point\n {\n const point = new Point();\n\n if (this.viewport.options.interaction)\n {\n this.viewport.options.interaction.mapPositionToPoint(point, event.clientX, event.clientY);\n }\n else\n {\n point.x = event.clientX;\n point.y = event.clientY;\n }\n\n return point;\n }\n\n /** Handle wheel events */\n public handleWheel(event: WheelEvent): void\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n\n // do not handle events coming from other elements\n if (this.viewport.options.interaction\n && (this.viewport.options.interaction as any).interactionDOMElement !== event.target)\n {\n return;\n }\n\n // only handle wheel events where the mouse is over the viewport\n const point = this.viewport.toLocal(this.getPointerPosition(event));\n\n if (this.viewport.left <= point.x\n && point.x <= this.viewport.right\n && this.viewport.top <= point.y\n && point.y <= this.viewport.bottom)\n {\n const stop = this.viewport.plugins.wheel(event);\n\n if (stop && !this.viewport.options.passiveWheel)\n {\n event.preventDefault();\n }\n }\n }\n\n public pause(): void\n {\n this.touches = [];\n this.isMouseDown = false;\n }\n\n /** Get touch by id */\n public get(id: number): IViewportTouch | null\n {\n for (const touch of this.touches)\n {\n if (touch.id === id)\n {\n return touch;\n }\n }\n\n return null;\n }\n\n /** Remove touch by number */\n remove(id: number): void\n {\n for (let i = 0; i < this.touches.length; i++)\n {\n if (this.touches[i].id === id)\n {\n this.touches.splice(i, 1);\n\n return;\n }\n }\n }\n\n /**\n * @returns {number} count of mouse/touch pointers that are down on the viewport\n */\n count(): number\n {\n return (this.isMouseDown ? 1 : 0) + this.touches.length;\n }\n}\n","import type {\n Animate,\n Bounce,\n Clamp,\n ClampZoom,\n Decelerate,\n Drag,\n Follow,\n MouseEdges,\n Pinch,\n Plugin,\n Snap,\n SnapZoom,\n Wheel,\n} from './plugins';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { Viewport } from './Viewport';\n\nconst PLUGIN_ORDER = [\n 'drag',\n 'pinch',\n 'wheel',\n 'follow',\n 'mouse-edges',\n 'decelerate',\n 'aniamte',\n 'bounce',\n 'snap-zoom',\n 'clamp-zoom',\n 'snap',\n 'clamp',\n];\n\n/**\n * Use this to access current plugins or add user-defined plugins\n *\n * @public\n */\nexport class PluginManager\n{\n /** Maps mounted plugins by their type */\n public plugins: Partial>;\n\n /**\n * List of plugins mounted\n *\n * This list is kept sorted by the internal priority of plugins (hard-coded).\n */\n public list: Array;\n\n /** The viewport using the plugins managed by `this`. */\n public readonly viewport: Viewport;\n\n /** This is called by {@link Viewport} to initialize the {@link Viewport.plugins plugins}. */\n constructor(viewport: Viewport)\n {\n this.viewport = viewport;\n this.list = [];\n this.plugins = {};\n }\n\n /**\n * Inserts a named plugin or a user plugin into the viewport\n * default plugin order: 'drag', 'pinch', 'wheel', 'follow', 'mouse-edges', 'decelerate', 'bounce',\n * 'snap-zoom', 'clamp-zoom', 'snap', 'clamp'\n *\n * @param {string} name of plugin\n * @param {Plugin} plugin - instantiated Plugin class\n * @param {number} index to insert userPlugin (otherwise inserts it at the end)\n */\n public add(name: string, plugin: Plugin, index: number = PLUGIN_ORDER.length)\n {\n this.plugins[name] = plugin;\n\n const current = PLUGIN_ORDER.indexOf(name);\n\n if (current !== -1)\n {\n PLUGIN_ORDER.splice(current, 1);\n }\n\n PLUGIN_ORDER.splice(index, 0, name);\n this.sort();\n }\n\n public get(name: 'animate', ignorePaused?: boolean): Animate | undefined | null;\n public get(name: 'bounce', ignorePaused?: boolean): Bounce | undefined | null;\n public get(name: 'clamp', ignorePaused?: boolean): Clamp | undefined | null;\n public get(name: 'clamp-zoom', ignorePaused?: boolean): ClampZoom | undefined | null;\n public get(name: 'decelerate', ignorePaused?: boolean): Decelerate | undefined | null;\n public get(name: 'drag', ignorePaused?: boolean): Drag | undefined | null;\n public get(name: 'follow', ignorePaused?: boolean): Follow | undefined | null;\n public get(name: 'mouse-edges', ignorePaused?: boolean): MouseEdges | undefined | null;\n public get(name: 'pinch', ignorePaused?: boolean): Pinch | undefined | null;\n public get(name: 'snap', ignorePaused?: boolean): Snap | undefined | null;\n public get(name: 'snap-zoom', ignorePaused?: boolean): SnapZoom | undefined | null;\n public get(name: 'wheel', ignorePaused?: boolean): Wheel | undefined | null;\n public get(name: string, ignorePaused?: boolean): T | undefined | null;\n\n /**\n * Get plugin\n *\n * @param {string} name of plugin\n * @param {boolean} [ignorePaused] return null if plugin is paused\n */\n public get(name: string, ignorePaused?: boolean): T | undefined | null\n {\n if (ignorePaused)\n {\n if (this.plugins[name]?.paused)\n {\n return null;\n }\n }\n\n return this.plugins[name] as T;\n }\n\n /**\n * Update all active plugins\n *\n * @internal\n * @ignore\n * @param {number} elapsed type in milliseconds since last update\n */\n public update(elapsed: number): void\n {\n for (const plugin of this.list)\n {\n plugin.update(elapsed);\n }\n }\n\n /**\n * Resize all active plugins\n *\n * @internal\n * @ignore\n */\n public resize(): void\n {\n for (const plugin of this.list)\n {\n plugin.resize();\n }\n }\n\n /** Clamps and resets bounce and decelerate (as needed) after manually moving viewport */\n public reset(): void\n {\n for (const plugin of this.list)\n {\n plugin.reset();\n }\n }\n\n /** removes all installed plugins */\n public removeAll(): void\n {\n this.plugins = {};\n this.sort();\n }\n\n /**\n * Removes installed plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public remove(name: string): void\n {\n if (this.plugins[name])\n {\n delete this.plugins[name];\n this.viewport.emit(`${name}-remove`);\n this.sort();\n }\n }\n\n /**\n * Pause plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public pause(name: string): void\n {\n this.plugins[name]?.pause();\n }\n\n /**\n * Resume plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public resume(name: string): void\n {\n this.plugins[name]?.resume();\n }\n\n /**\n * Sort plugins according to PLUGIN_ORDER\n *\n * @internal\n * @ignore\n */\n public sort()\n {\n this.list = [];\n\n for (const plugin of PLUGIN_ORDER)\n {\n if (this.plugins[plugin])\n {\n this.list.push(this.plugins[plugin] as Plugin);\n }\n }\n }\n\n /**\n * Handle down for all plugins\n *\n * @internal\n * @ignore\n */\n public down(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.list)\n {\n if (plugin.down(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle move for all plugins\n *\n * @internal\n * @ignore\n */\n public move(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.viewport.plugins.list)\n {\n if (plugin.move(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle up for all plugins\n *\n * @internal\n * @ignore\n */\n public up(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.list)\n {\n if (plugin.up(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle wheel event for all plugins\n *\n * @internal\n * @ignore\n */\n public wheel(e: WheelEvent): boolean\n {\n let result = false;\n\n for (const plugin of this.list)\n {\n if (plugin.wheel(e))\n {\n result = true;\n }\n }\n\n return result;\n }\n}\n","import { Container } from '@pixi/display';\nimport { Point, Rectangle } from '@pixi/math';\nimport { Ticker } from '@pixi/ticker';\n\nimport { InputManager } from './InputManager';\nimport { PluginManager } from './PluginManager';\nimport {\n Animate,\n Bounce,\n Clamp,\n ClampZoom,\n Decelerate,\n Drag,\n Follow,\n MouseEdges,\n Pinch,\n Snap,\n SnapZoom,\n Wheel\n} from './plugins';\n\nimport type { DisplayObject, IDestroyOptions } from '@pixi/display';\nimport type { IHitArea, InteractionManager } from '@pixi/interaction';\n\n/** Options for {@link Viewport}. */\nexport interface IViewportOptions {\n /** @default window.innerWidth */\n screenWidth?: number;\n\n /** @default window.innerHeight */\n screenHeight?: number;\n\n /** @default this.width */\n worldWidth?: number | null;\n\n /** @default this.height */\n worldHeight?: number | null;\n\n /**\n * Number of pixels to move to trigger an input event (e.g., drag, pinch) or disable a clicked event\n *\n * @default 5\n */\n threshold?: number;\n\n /**\n * Whether the 'wheel' event is set to passive (note: if false, e.preventDefault() will be called when wheel is used over the viewport)\n *\n * @default true\n */\n passiveWheel?: boolean;\n\n /**\n * Whether to stopPropagation of events that impact the viewport (except wheel events, see options.passiveWheel)\n */\n stopPropagation?: boolean;\n\n /**\n * Change the default hitArea from world size to a new value\n */\n forceHitArea?: Rectangle | null;\n\n /**\n * Set this if you want to manually call update() function on each frame\n *\n * @default false\n */\n noTicker?: boolean;\n\n /**\n * InteractionManager, available from instantiated `WebGLRenderer/CanvasRenderer.plugins.interaction`\n *\n * It's used to calculate pointer postion relative to canvas location on screen\n */\n interaction?: InteractionManager | null;\n\n /**\n * Remove oncontextmenu=() => {} from the divWheel element\n */\n disableOnContextMenu?: boolean;\n\n /**\n * div to attach the wheel event\n *\n * @default document.body\n */\n divWheel?: HTMLElement;\n\n /**\n * Use this PIXI.ticker for updates\n *\n * @default PIXI.Ticker.shared\n */\n ticker?: Ticker;\n}\n\nexport interface ICompleteViewportOptions extends IViewportOptions {\n screenWidth: number;\n screenHeight: number;\n threshold: number;\n passiveWheel: boolean;\n stopPropagation: boolean;\n noTicker: boolean;\n ticker: Ticker;\n}\n\nexport interface IViewportTransformState {\n x: number;\n y: number;\n scaleX: number;\n scaleY: number;\n}\n\nconst DEFAULT_VIEWPORT_OPTIONS: ICompleteViewportOptions = {\n screenWidth: window.innerWidth,\n screenHeight: window.innerHeight,\n worldWidth: null,\n worldHeight: null,\n threshold: 5,\n passiveWheel: true,\n stopPropagation: false,\n forceHitArea: null,\n noTicker: false,\n interaction: null,\n disableOnContextMenu: false,\n ticker: Ticker.shared,\n};\n\n/**\n * Main class to use when creating a Viewport\n *\n * @public\n * @fires clicked\n * @fires drag-start\n * @fires drag-end\n * @fires drag-remove\n * @fires pinch-start\n * @fires pinch-end\n * @fires pinch-remove\n * @fires snap-start\n * @fires snap-end\n * @fires snap-remove\n * @fires snap-zoom-start\n * @fires snap-zoom-end\n * @fires snap-zoom-remove\n * @fires bounce-x-start\n * @fires bounce-x-end\n * @fires bounce-y-start\n * @fires bounce-y-end\n * @fires bounce-remove\n * @fires wheel\n * @fires wheel-remove\n * @fires wheel-scroll\n * @fires wheel-scroll-remove\n * @fires mouse-edge-start\n * @fires mouse-edge-end\n * @fires mouse-edge-remove\n * @fires moved\n * @fires moved-end\n * @fires zoomed\n * @fires zoomed-end\n * @fires frame-end\n */\nexport class Viewport extends Container\n{\n /** Flags whether the viewport is being panned */\n public moving?: boolean;\n\n public screenWidth: number;\n public screenHeight: number;\n\n /** Number of pixels to move to trigger an input event (e.g., drag, pinch) or disable a clicked event */\n public threshold: number;\n\n public readonly input: InputManager;\n\n /** Use this to add user plugins or access existing plugins (e.g., to pause, resume, or remove them) */\n public readonly plugins: PluginManager;\n\n /** Flags whether the viewport zoom is being changed. */\n public zooming?: boolean;\n\n public lastViewport?: IViewportTransformState | null;\n\n /** The options passed when creating this viewport, merged with the default values */\n public readonly options: ICompleteViewportOptions & { divWheel: HTMLElement };\n\n private _dirty?: boolean;\n private _forceHitArea?: IHitArea | null;\n private _hitAreaDefault?: Rectangle;\n private _pause?: boolean;\n private readonly tickerFunction?: () => void;\n private _worldWidth?: number | null;\n private _worldHeight?: number | null;\n\n /**\n * @param options\n */\n constructor(options: IViewportOptions = {})\n {\n super();\n this.options = Object.assign(\n {},\n { divWheel: document.body },\n DEFAULT_VIEWPORT_OPTIONS,\n options\n );\n\n this.screenWidth = this.options.screenWidth;\n this.screenHeight = this.options.screenHeight;\n\n this._worldWidth = this.options.worldWidth;\n this._worldHeight = this.options.worldHeight;\n this.forceHitArea = this.options.forceHitArea;\n this.threshold = this.options.threshold;\n\n this.options.divWheel = this.options.divWheel || document.body;\n\n if (this.options.disableOnContextMenu)\n {\n this.options.divWheel.oncontextmenu = (e) => e.preventDefault();\n }\n if (!this.options.noTicker)\n {\n this.tickerFunction = () => this.update(this.options.ticker.elapsedMS);\n this.options.ticker.add(this.tickerFunction);\n }\n\n this.input = new InputManager(this);\n this.plugins = new PluginManager(this);\n }\n\n /** Overrides PIXI.Container's destroy to also remove the 'wheel' and PIXI.Ticker listeners */\n destroy(options: IDestroyOptions)\n {\n if (!this.options.noTicker && this.tickerFunction)\n {\n this.options.ticker.remove(this.tickerFunction);\n }\n\n this.input.destroy();\n super.destroy(options);\n }\n\n /**\n * Update viewport on each frame.\n *\n * By default, you do not need to call this unless you set `options.noTicker=true`.\n *\n * @param {number} elapsed time in milliseconds since last update\n */\n update(elapsed: number): void\n {\n if (!this.pause)\n {\n this.plugins.update(elapsed);\n\n if (this.lastViewport)\n {\n // Check for moved-end event\n if (this.lastViewport.x !== this.x || this.lastViewport.y !== this.y)\n {\n this.moving = true;\n }\n else if (this.moving)\n {\n this.emit('moved-end', this);\n this.moving = false;\n }\n\n // Check for zoomed-end event\n if (this.lastViewport.scaleX !== this.scale.x || this.lastViewport.scaleY !== this.scale.y)\n {\n this.zooming = true;\n }\n else if (this.zooming)\n {\n this.emit('zoomed-end', this);\n this.zooming = false;\n }\n }\n\n if (!this.forceHitArea)\n {\n this._hitAreaDefault = new Rectangle(this.left, this.top, this.worldScreenWidth, this.worldScreenHeight);\n this.hitArea = this._hitAreaDefault;\n }\n\n this._dirty = this._dirty || !this.lastViewport\n || this.lastViewport.x !== this.x || this.lastViewport.y !== this.y\n || this.lastViewport.scaleX !== this.scale.x || this.lastViewport.scaleY !== this.scale.y;\n\n this.lastViewport = {\n x: this.x,\n y: this.y,\n scaleX: this.scale.x,\n scaleY: this.scale.y\n };\n this.emit('frame-end', this);\n }\n }\n\n /** Use this to set screen and world sizes, needed for pinch/wheel/clamp/bounce. */\n resize(\n screenWidth = window.innerWidth,\n screenHeight = window.innerHeight,\n worldWidth: number,\n worldHeight: number\n ): void\n {\n this.screenWidth = screenWidth;\n this.screenHeight = screenHeight;\n\n if (typeof worldWidth !== 'undefined')\n {\n this._worldWidth = worldWidth;\n }\n if (typeof worldHeight !== 'undefined')\n {\n this._worldHeight = worldHeight;\n }\n\n this.plugins.resize();\n this.dirty = true;\n }\n\n /** World width, in pixels */\n get worldWidth(): number\n {\n if (this._worldWidth)\n {\n return this._worldWidth;\n }\n\n return this.width / this.scale.x;\n }\n set worldWidth(value)\n {\n this._worldWidth = value;\n this.plugins.resize();\n }\n\n /** World height, in pixels */\n get worldHeight(): number\n {\n if (this._worldHeight)\n {\n return this._worldHeight;\n }\n\n return this.height / this.scale.y;\n }\n set worldHeight(value)\n {\n this._worldHeight = value;\n this.plugins.resize();\n }\n\n /** Get visible world bounds of viewport */\n public getVisibleBounds(): Rectangle\n {\n return new Rectangle(this.left, this.top, this.worldScreenWidth, this.worldScreenHeight);\n }\n\n /** Change coordinates from screen to world */\n public toWorld(x: number, y: number): Point;\n /** Change coordinates from screen to world */\n public toWorld(screenPoint: Point): Point;\n\n public toWorld(x: number | Point, y?: number): Point\n {\n if (arguments.length === 2)\n {\n return this.toLocal(new Point(x as number, y));\n }\n\n return this.toLocal(x as Point);\n }\n\n /** Change coordinates from world to screen */\n public toScreen(x: number, y: number): Point;\n /** Change coordinates from world to screen */\n public toScreen(worldPoint: Point): Point;\n\n public toScreen(x: number | Point, y?: number): Point\n {\n if (arguments.length === 2)\n {\n return this.toGlobal(new Point(x as number, y));\n }\n\n return this.toGlobal(x as Point);\n }\n\n /** Screen width in world coordinates */\n get worldScreenWidth(): number\n {\n return this.screenWidth / this.scale.x;\n }\n\n /** Screen height in world coordinates */\n get worldScreenHeight(): number\n {\n return this.screenHeight / this.scale.y;\n }\n\n /** World width in screen coordinates */\n get screenWorldWidth(): number\n {\n return this.worldWidth * this.scale.x;\n }\n\n /** World height in screen coordinates */\n get screenWorldHeight()\n {\n return this.worldHeight * this.scale.y;\n }\n\n /** Center of screen in world coordinates */\n get center(): Point\n {\n return new Point(\n this.worldScreenWidth / 2 - this.x / this.scale.x,\n this.worldScreenHeight / 2 - this.y / this.scale.y,\n );\n }\n set center(value: Point)\n {\n this.moveCenter(value);\n }\n\n /** Move center of viewport to (x, y) */\n public moveCenter(x: number, y: number): this;\n\n /** Move center of viewport to {@code center}. */\n public moveCenter(center: Point): this;\n\n public moveCenter(...args: [number, number] | [Point]): this\n {\n let x: number;\n let y: number;\n\n if (typeof args[0] === 'number')\n {\n x = args[0];\n y = args[1] as number;\n }\n else\n {\n x = args[0].x;\n y = args[0].y;\n }\n\n const newX = (this.worldScreenWidth / 2 - x) * this.scale.x;\n const newY = (this.worldScreenHeight / 2 - y) * this.scale.y;\n\n if (this.x !== newX || this.y !== newY)\n {\n this.position.set(newX, newY);\n this.plugins.reset();\n this.dirty = true;\n }\n\n return this;\n }\n\n /** Top-left corner of Viewport */\n get corner(): Point\n {\n return new Point(-this.x / this.scale.x, -this.y / this.scale.y);\n }\n set corner(value: Point)\n {\n this.moveCorner(value);\n }\n\n /** Move viewport's top-left corner; also clamps and resets decelerate and bounce (as needed) */\n public moveCorner(x: number, y: number): this;\n\n /** move viewport's top-left corner; also clamps and resets decelerate and bounce (as needed) */\n public moveCorner(center: Point): this;\n\n public moveCorner(...args: [number, number] | [Point])\n {\n let x;\n let y;\n\n if (args.length === 1)\n {\n x = -args[0].x * this.scale.x;\n y = -args[0].y * this.scale.y;\n }\n else\n {\n x = -args[0] * this.scale.x;\n y = -args[1] * this.scale.y;\n }\n\n if (x !== this.x || y !== this.y)\n {\n this.position.set(x, y);\n this.plugins.reset();\n this.dirty = true;\n }\n\n return this;\n }\n\n /** Get how many world pixels fit in screen's width */\n get screenWidthInWorldPixels()\n {\n return this.screenWidth / this.scale.x;\n }\n\n /** Get how many world pixels fit on screen's height */\n get screenHeightInWorldPixels()\n {\n return this.screenHeight / this.scale.y;\n }\n\n /**\n * Find the scale value that fits a world width on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param width - Width in world pixels\n * @return - scale\n */\n findFitWidth(width: number): number\n {\n return this.screenWidth / width;\n }\n\n /**\n * Finds the scale value that fits a world height on the screens\n * does not change the viewport (use fit... to change)\n *\n * @param height - Height in world pixels\n * @return - scale\n */\n findFitHeight(height: number): number\n {\n return this.screenHeight / height;\n }\n\n /**\n * Finds the scale value that fits the smaller of a world width and world height on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param {number} width in world pixels\n * @param {number} height in world pixels\n * @returns {number} scale\n */\n findFit(width: number, height: number): number\n {\n const scaleX = this.screenWidth / width;\n const scaleY = this.screenHeight / height;\n\n return Math.min(scaleX, scaleY);\n }\n\n /**\n * Finds the scale value that fits the larger of a world width and world height on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param {number} width in world pixels\n * @param {number} height in world pixels\n * @returns {number} scale\n */\n findCover(width: number, height: number): number\n {\n const scaleX = this.screenWidth / width;\n const scaleY = this.screenHeight / height;\n\n return Math.max(scaleX, scaleY);\n }\n\n /**\n * Change zoom so the width fits in the viewport\n *\n * @param width - width in world coordinates\n * @param center - maintain the same center\n * @param scaleY - whether to set scaleY=scaleX\n * @param noClamp - whether to disable clamp-zoom\n * @returns {Viewport} this\n */\n fitWidth(width = this.worldWidth, center?: boolean, scaleY = true, noClamp?: boolean): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.x = this.screenWidth / width;\n\n if (scaleY)\n {\n this.scale.y = this.scale.x;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (!noClamp && clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so the height fits in the viewport\n *\n * @param {number} [height=this.worldHeight] in world coordinates\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @param {boolean} [scaleX=true] whether to set scaleX = scaleY\n * @param {boolean} [noClamp] whether to disable clamp-zoom\n * @returns {Viewport} this\n */\n fitHeight(height = this.worldHeight, center?: boolean, scaleX = true, noClamp?: boolean)\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.y = this.screenHeight / height;\n\n if (scaleX)\n {\n this.scale.x = this.scale.y;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (!noClamp && clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so it fits the entire world in the viewport\n *\n * @param {boolean} center maintain the same center of the screen after zoom\n * @returns {Viewport} this\n */\n fitWorld(center?: boolean): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n\n this.scale.x = this.screenWidth / this.worldWidth;\n this.scale.y = this.screenHeight / this.worldHeight;\n\n if (this.scale.x < this.scale.y)\n {\n this.scale.y = this.scale.x;\n }\n else\n {\n this.scale.x = this.scale.y;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so it fits the size or the entire world in the viewport\n *\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @param {number} [width=this.worldWidth] desired width\n * @param {number} [height=this.worldHeight] desired height\n * @returns {Viewport} this\n */\n fit(center?: boolean, width = this.worldWidth, height = this.worldHeight): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n\n this.scale.x = this.screenWidth / width;\n this.scale.y = this.screenHeight / height;\n\n if (this.scale.x < this.scale.y)\n {\n this.scale.y = this.scale.x;\n }\n else\n {\n this.scale.x = this.scale.y;\n }\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n // eslint-disable-next-line\n // @ts-ignore\n set visible(value: boolean)\n {\n if (!value)\n {\n this.input.clear();\n }\n\n super.visible = value;\n }\n\n /**\n * Zoom viewport to specific value.\n *\n * @param {number} scale value (e.g., 1 would be 100%, 0.25 would be 25%)\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n setZoom(scale: number, center?: boolean): this\n {\n let save;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.set(scale);\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Zoom viewport by a certain percent (in both x and y direction).\n *\n * @param {number} percent change (e.g., 0.25 would increase a starting scale of 1.0 to 1.25)\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n zoomPercent(percent: number, center?: boolean): this\n {\n return this.setZoom(this.scale.x + this.scale.x * percent, center);\n }\n\n /**\n * Zoom viewport by increasing/decreasing width by a certain number of pixels.\n *\n * @param {number} change in pixels\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n zoom(change: number, center?: boolean)\n {\n this.fitWidth(change + this.worldScreenWidth, center);\n\n return this;\n }\n\n /** Changes scale of viewport and maintains center of viewport */\n get scaled(): number\n {\n return this.scale.x;\n }\n set scaled(scale: number)\n {\n this.setZoom(scale, true);\n }\n\n /**\n * @param {SnapZoomOptions} options\n */\n snapZoom(options: any): this\n {\n this.plugins.add('snap-zoom', new SnapZoom(this, options));\n\n return this;\n }\n\n /** Is container out of world bounds */\n OOB(): {\n left: boolean;\n right: boolean;\n top: boolean;\n bottom: boolean;\n cornerPoint: Point;\n }\n {\n return {\n left: this.left < 0,\n right: this.right > this.worldWidth,\n top: this.top < 0,\n bottom: this.bottom > this.worldHeight,\n cornerPoint: new Point(\n this.worldWidth * this.scale.x - this.screenWidth,\n this.worldHeight * this.scale.y - this.screenHeight\n )\n };\n }\n\n /** World coordinates of the right edge of the screen */\n get right(): number\n {\n return -this.x / this.scale.x + this.worldScreenWidth;\n }\n set right(value: number)\n {\n this.x = -value * this.scale.x + this.screenWidth;\n this.plugins.reset();\n }\n\n /** World coordinates of the left edge of the screen */\n get left(): number\n {\n return -this.x / this.scale.x;\n }\n set left(value: number)\n {\n this.x = -value * this.scale.x;\n this.plugins.reset();\n }\n\n /** World coordinates of the top edge of the screen */\n get top(): number\n {\n return -this.y / this.scale.y;\n }\n set top(value: number)\n {\n this.y = -value * this.scale.y;\n this.plugins.reset();\n }\n\n /** World coordinates of the bottom edge of the screen */\n get bottom(): number\n {\n return -this.y / this.scale.y + this.worldScreenHeight;\n }\n set bottom(value: number)\n {\n this.y = -value * this.scale.y + this.screenHeight;\n this.plugins.reset();\n }\n\n /**\n * Determines whether the viewport is dirty (i.e., needs to be renderered to the screen because of a change)\n */\n get dirty(): boolean\n {\n return !!this._dirty;\n }\n set dirty(value: boolean)\n {\n this._dirty = value;\n }\n\n /**\n * Permanently changes the Viewport's hitArea\n *\n * NOTE: if not set then hitArea = PIXI.Rectangle(Viewport.left, Viewport.top, Viewport.worldScreenWidth, Viewport.worldScreenHeight)\n */\n get forceHitArea(): IHitArea | null | undefined\n {\n return this._forceHitArea;\n }\n set forceHitArea(value: IHitArea | null | undefined)\n {\n if (value)\n {\n this._forceHitArea = value;\n this.hitArea = value;\n }\n else\n {\n this._forceHitArea = null;\n this.hitArea = new Rectangle(0, 0, this.worldWidth, this.worldHeight);\n }\n }\n\n /**\n * Enable one-finger touch to drag\n *\n * NOTE: if you expect users to use right-click dragging, you should enable `viewport.options.disableOnContextMenu`\n * to avoid the context menu popping up on each right-click drag.\n *\n * @param {DragOptions} [options]\n * @returns {Viewport} this\n */\n public drag(options: any): this\n {\n this.plugins.add('drag', new Drag(this, options));\n\n return this;\n }\n\n /**\n * Clamp to world boundaries or other provided boundaries\n *\n * NOTES:\n * clamp is disabled if called with no options; use { direction: 'all' } for all edge clamping\n * screenWidth, screenHeight, worldWidth, and worldHeight needs to be set for this to work properly\n *\n * @param {ClampOptions} [options]\n * @returns {Viewport} this\n */\n public clamp(options: any): this\n {\n this.plugins.add('clamp', new Clamp(this, options));\n\n return this;\n }\n\n /**\n * Decelerate after a move\n *\n * NOTE: this fires 'moved' event during deceleration\n *\n * @param {DecelerateOptions} [options]\n * @return {Viewport} this\n */\n public decelerate(options: any): this\n {\n this.plugins.add('decelerate', new Decelerate(this, options));\n\n return this;\n }\n\n /**\n * Bounce on borders\n * NOTES:\n * screenWidth, screenHeight, worldWidth, and worldHeight needs to be set for this to work properly\n * fires 'moved', 'bounce-x-start', 'bounce-y-start', 'bounce-x-end', and 'bounce-y-end' events\n * @param {object} [options]\n * @param {string} [options.sides=all] all, horizontal, vertical, or combination of top, bottom, right, left (e.g., 'top-bottom-right')\n * @param {number} [options.friction=0.5] friction to apply to decelerate if active\n * @param {number} [options.time=150] time in ms to finish bounce\n * @param {object} [options.bounceBox] use this bounceBox instead of (0, 0, viewport.worldWidth, viewport.worldHeight)\n * @param {number} [options.bounceBox.x=0]\n * @param {number} [options.bounceBox.y=0]\n * @param {number} [options.bounceBox.width=viewport.worldWidth]\n * @param {number} [options.bounceBox.height=viewport.worldHeight]\n * @param {string|function} [options.ease=easeInOutSine] ease function or name (see http://easings.net/ for supported names)\n * @param {string} [options.underflow=center] (top/bottom/center and left/right/center, or center) where to place world if too small for screen\n * @return {Viewport} this\n */\n public bounce(options: any): this\n {\n this.plugins.add('bounce', new Bounce(this, options));\n\n return this;\n }\n\n /**\n * Enable pinch to zoom and two-finger touch to drag\n *\n * @param {PinchOptions} [options]\n * @return {Viewport} this\n */\n public pinch(options: any): this\n {\n this.plugins.add('pinch', new Pinch(this, options));\n\n return this;\n }\n\n /**\n * Snap to a point\n *\n * @param {number} x\n * @param {number} y\n * @param {SnapOptions} [options]\n * @return {Viewport} this\n */\n public snap(x: number, y: number, options: any): this\n {\n this.plugins.add('snap', new Snap(this, x, y, options));\n\n return this;\n }\n\n /**\n * Follow a target\n *\n * NOTES:\n * uses the (x, y) as the center to follow; for PIXI.Sprite to work properly, use sprite.anchor.set(0.5)\n * options.acceleration is not perfect as it doesn't know the velocity of the target\n * it adds acceleration to the start of movement and deceleration to the end of movement when the target is stopped\n * fires 'moved' event\n * @param {PIXI.DisplayObject} target to follow\n * @param {FollowOptions} [options]\n * @returns {Viewport} this\n */\n public follow(target: DisplayObject, options: any): this\n {\n this.plugins.add('follow', new Follow(this, target, options));\n\n return this;\n }\n\n /**\n * Zoom using mouse wheel\n *\n * @param {WheelOptions} [options]\n * @return {Viewport} this\n */\n public wheel(options: any): this\n {\n this.plugins.add('wheel', new Wheel(this, options));\n\n return this;\n }\n\n /**\n * Animate the position and/or scale of the viewport\n *\n * @param {AnimateOptions} options\n * @returns {Viewport} this\n */\n public animate(options: any): this\n {\n this.plugins.add('animate', new Animate(this, options));\n\n return this;\n }\n\n /**\n * Enable clamping of zoom to constraints\n *\n * The minWidth/Height settings are how small the world can get (as it would appear on the screen)\n * before clamping. The maxWidth/maxHeight is how larger the world can scale (as it would appear on\n * the screen) before clamping.\n *\n * For example, if you have a world size of 1000 x 1000 and a screen size of 100 x 100, if you set\n * minWidth/Height = 100 then the world will not be able to zoom smaller than the screen size (ie,\n * zooming out so it appears smaller than the screen). Similarly, if you set maxWidth/Height = 100\n * the world will not be able to zoom larger than the screen size (ie, zooming in so it appears\n * larger than the screen).\n *\n * @param {ClampZoomOptions} [options]\n * @return {Viewport} this\n */\n public clampZoom(options: any): this\n {\n this.plugins.add('clamp-zoom', new ClampZoom(this, options));\n\n return this;\n }\n\n /**\n * Scroll viewport when mouse hovers near one of the edges or radius-distance from center of screen.\n *\n * NOTE: fires 'moved' event\n *\n * @param {MouseEdgesOptions} [options]\n */\n public mouseEdges(options: any): this\n {\n this.plugins.add('mouse-edges', new MouseEdges(this, options));\n\n return this;\n }\n\n /** Pause viewport (including animation updates such as decelerate) */\n get pause(): boolean\n {\n return !!this._pause;\n }\n set pause(value: boolean)\n {\n this._pause = value;\n\n this.lastViewport = null;\n this.moving = false;\n this.zooming = false;\n\n if (value)\n {\n this.input.pause();\n }\n }\n\n /**\n * Move the viewport so the bounding box is visible\n *\n * @param x - left\n * @param y - top\n * @param width\n * @param height\n * @param resizeToFit - Resize the viewport so the box fits within the viewport\n */\n public ensureVisible(x: number, y: number, width: number, height: number, resizeToFit?: boolean)\n {\n if (resizeToFit && (width > this.worldScreenWidth || height > this.worldScreenHeight))\n {\n this.fit(true, width, height);\n this.emit('zoomed', { viewport: this, type: 'ensureVisible' });\n }\n let moved = false;\n\n if (x < this.left)\n {\n this.left = x;\n moved = true;\n }\n else if (x + width > this.right)\n {\n this.right = x + width;\n moved = true;\n }\n if (y < this.top)\n {\n this.top = y;\n moved = true;\n }\n else if (y + height > this.bottom)\n {\n this.bottom = y + height;\n moved = true;\n }\n if (moved)\n {\n this.emit('moved', { viewport: this, type: 'ensureVisible' });\n }\n }\n}\n\n/**\n * Fires after a mouse or touch click\n * @event Viewport#clicked\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a drag starts\n * @event Viewport#drag-start\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a drag ends\n * @event Viewport#drag-end\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a pinch starts\n * @event Viewport#pinch-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a pinch end\n * @event Viewport#pinch-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap starts\n * @event Viewport#snap-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap ends\n * @event Viewport#snap-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap-zoom starts\n * @event Viewport#snap-zoom-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap-zoom ends\n * @event Viewport#snap-zoom-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce starts in the x direction\n * @event Viewport#bounce-x-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce ends in the x direction\n * @event Viewport#bounce-x-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce starts in the y direction\n * @event Viewport#bounce-y-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce ends in the y direction\n * @event Viewport#bounce-y-end\n * @type {Viewport}\n */\n\n/**\n * Fires when for a mouse wheel event\n * @event Viewport#wheel\n * @type {object}\n * @property {object} wheel\n * @property {number} wheel.dx\n * @property {number} wheel.dy\n * @property {number} wheel.dz\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a wheel-scroll occurs\n * @event Viewport#wheel-scroll\n * @type {Viewport}\n */\n\n/**\n * Fires when a mouse-edge starts to scroll\n * @event Viewport#mouse-edge-start\n * @type {Viewport}\n */\n\n/**\n * Fires when the mouse-edge scrolling ends\n * @event Viewport#mouse-edge-end\n * @type {Viewport}\n */\n\n/**\n * Fires when viewport moves through UI interaction, deceleration, ensureVisible, or follow\n * @event Viewport#moved\n * @type {object}\n * @property {Viewport} viewport\n * @property {string} type - (drag, snap, pinch, follow, bounce-x, bounce-y,\n * clamp-x, clamp-y, decelerate, mouse-edges, wheel, ensureVisible)\n */\n\n/**\n * Fires when viewport moves through UI interaction, deceleration, ensureVisible, or follow\n * @event Viewport#zoomed\n * @type {object}\n * @property {Viewport} viewport\n * @property {string} type (drag-zoom, pinch, wheel, clamp-zoom, ensureVisible)\n */\n\n/**\n * Fires when viewport stops moving\n * @event Viewport#moved-end\n * @type {Viewport}\n */\n\n/**\n * Fires when viewport stops zooming\n * @event Viewport#zoomed-end\n * @type {Viewport}\n */\n\n/**\n* Fires at the end of an update frame\n* @event Viewport#frame-end\n* @type {Viewport}\n*/\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;IAGA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;;IC1FA;AAGA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;ICpBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;ygBC1RA;AACA;AACA;AACA;AACA;AACA;IACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;AACA;AACA;AACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;;IC3XA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;AACA;IACA;IACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;IC1RA;IACA;IACA;IACA;IACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;ICpGA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;ICrRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;IC1eA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;ICvJA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;ICxRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;;IC/JA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;ICxNA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;;ICrRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;;IC3NA;IACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;;ACrSA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;;ICpRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;"} \ No newline at end of file +{"version":3,"file":"viewport.min.js","sources":["../src/plugins/Plugin.ts","../src/ease.ts","../src/plugins/Animate.ts","../src/plugins/Bounce.ts","../src/plugins/Clamp.ts","../src/plugins/ClampZoom.ts","../src/plugins/Decelerate.ts","../src/plugins/Drag.ts","../src/plugins/Follow.ts","../src/plugins/MouseEdges.ts","../src/plugins/Pinch.ts","../src/plugins/Snap.ts","../src/plugins/SnapZoom.ts","../src/plugins/Wheel.ts","../src/InputManager.ts","../src/PluginManager.ts","../src/Viewport.ts"],"sourcesContent":["import type { InteractionEvent } from '@pixi/interaction';\nimport type { Viewport } from '../Viewport';\n\n/**\n * Derive this class to create user-defined plugins\n *\n * @public\n */\nexport class Plugin\n{\n /** The viewport to which this plugin is attached. */\n public readonly parent: Viewport;\n\n /**\n * Flags whether this plugin has been \"paused\".\n *\n * @see Plugin#pause\n * @see Plugin#resume\n */\n public paused: boolean;\n\n /** @param {Viewport} parent */\n constructor(parent: Viewport)\n {\n this.parent = parent;\n this.paused = false;\n }\n\n /** Called when plugin is removed */\n public destroy()\n {\n // Override for implementation\n }\n\n /** Handler for pointerdown PIXI event */\n public down(_e: InteractionEvent): boolean\n {\n return false;\n }\n\n /** Handler for pointermove PIXI event */\n public move(_e: InteractionEvent): boolean\n {\n return false;\n }\n\n /** Handler for pointerup PIXI event */\n public up(_e: InteractionEvent): boolean\n {\n return false;\n }\n\n /** Handler for wheel event on div */\n public wheel(_e: WheelEvent): boolean | undefined\n {\n return false;\n }\n\n /**\n * Called on each tick\n * @param {number} elapsed time in millisecond since last update\n */\n public update(_delta: number): void\n {\n // Override for implementation\n }\n\n /** Called when the viewport is resized */\n public resize()\n {\n // Override for implementation\n }\n\n /** Called when the viewport is manually moved */\n public reset(): void\n {\n // Override for implementation\n }\n\n /** Pause the plugin */\n public pause(): void\n {\n this.paused = true;\n }\n\n /** Un-pause the plugin */\n public resume(): void\n {\n this.paused = false;\n }\n}\n","// eslint-disable-next-line\n// @ts-expect-error Penner seems to have no typings.\nimport Penner from 'penner';\n\n/**\n * Returns correct Penner equation using string or Function.\n *\n * @internal\n * @ignore\n * @param {(function|string)} [ease]\n * @param {defaults} default penner equation to use if none is provided\n */\nexport default function ease(ease: any, defaults?: any): any\n{\n if (!ease)\n {\n return Penner[defaults]\n }\n else if (typeof ease === 'function')\n {\n return ease\n }\n else if (typeof ease === 'string')\n {\n return Penner[ease]\n }\n}","import { Point } from '@pixi/math';\nimport { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Animate}. */\nexport interface IAnimateOptions {\n /** Time to animate */\n time?: number;\n\n /** Position to move the viewport to */\n position?: Point;\n\n /**\n * Desired viewport width in world pixels\n *\n * (use instead of scale; aspect ratio is maintained if height is not provided)\n */\n width?: number;\n\n /**\n * Desired viewport height in world pixels\n *\n * (use instead of scale; aspect ratio is maintained if width is not provided)\n */\n height?: number;\n\n /** Scale to change zoom (scale.x = scale.y) */\n scale?: number;\n\n /** Independently change zoom in x-direction */\n scaleX?: number;\n\n /** Independently change zoom in y-direction */\n scaleY?: number;\n\n /** Easing function to use */\n ease?: any;\n\n /** Callback to invoke when the animation completes */\n callbackOnComplete?: (viewport: Viewport) => void;\n\n /** Removes this plugin if interrupted by any user input */\n removeOnInterrupt?: boolean;\n}\n\nconst DEFAULT_ANIMATE_OPTIONS = {\n removeOnInterrupt: false,\n ease: 'linear',\n time: 1000,\n};\n\n/**\n * Animation plugin.\n *\n * @see Viewport#animate\n * @fires animate-end\n */\nexport class Animate extends Plugin\n{\n public readonly options: IAnimateOptions & { ease: any; time: number };\n\n /** The starting x-coordinate of the viewport. */\n protected startX?: number;\n\n /** The starting y-coordinate of the viewport. */\n protected startY?: number;\n\n /** The change in the x-coordinate of the viewport through the animation.*/\n protected deltaX?: number;\n\n /** The change in the y-coordinate of the viewport through the animation. */\n protected deltaY?: number;\n\n /** Marks whether the center of the viewport is preserved in the animation. */\n protected keepCenter!: boolean;\n\n /** The starting viewport width. */\n protected startWidth: number | null = null;\n\n /** The starting viewport height. */\n protected startHeight: number | null = null;\n\n /** The change in the viewport's width through the animation. */\n protected deltaWidth: number | null = null;\n\n /** The change in the viewport's height through the animation. */\n protected deltaHeight: number | null = null;\n\n /** The viewport's width post-animation. */\n protected width: number | null = null;\n\n /** The viewport's height post-animation. */\n protected height: number | null = null;\n\n /** The time since the animation started. */\n protected time = 0;\n\n /**\n * This is called by {@link Viewport.animate}.\n *\n * @param parent\n * @param options\n */\n constructor(parent: Viewport, options: IAnimateOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_ANIMATE_OPTIONS, options);\n this.options.ease = ease(this.options.ease);\n\n this.setupPosition();\n this.setupZoom();\n\n this.time = 0;\n }\n\n /**\n * Setup `startX`, `startY`, `deltaX`, `deltaY`, `keepCenter`.\n *\n * This is called during construction.\n */\n protected setupPosition(): void\n {\n if (typeof this.options.position !== 'undefined')\n {\n this.startX = this.parent.center.x;\n this.startY = this.parent.center.y;\n this.deltaX = this.options.position.x - this.parent.center.x;\n this.deltaY = this.options.position.y - this.parent.center.y;\n this.keepCenter = false;\n }\n else\n {\n this.keepCenter = true;\n }\n }\n\n /**\n * Setup `startWidth, `startHeight`, `deltaWidth, `deltaHeight, `width`, `height`.\n *\n * This is called during construction.\n */\n protected setupZoom(): void\n {\n this.width = null;\n this.height = null;\n\n if (typeof this.options.scale !== 'undefined')\n {\n this.width = this.parent.screenWidth / this.options.scale;\n }\n else if (typeof this.options.scaleX !== 'undefined' || typeof this.options.scaleY !== 'undefined')\n {\n if (typeof this.options.scaleX !== 'undefined')\n {\n // screenSizeInWorldPixels = screenWidth / scale\n this.width = this.parent.screenWidth / this.options.scaleX;\n }\n if (typeof this.options.scaleY !== 'undefined')\n {\n this.height = this.parent.screenHeight / this.options.scaleY;\n }\n }\n else\n {\n if (typeof this.options.width !== 'undefined')\n {\n this.width = this.options.width;\n }\n if (typeof this.options.height !== 'undefined')\n {\n this.height = this.options.height;\n }\n }\n\n if (this.width !== null)\n {\n this.startWidth = this.parent.screenWidthInWorldPixels;\n this.deltaWidth = this.width - this.startWidth;\n }\n if (this.height !== null)\n {\n this.startHeight = this.parent.screenHeightInWorldPixels;\n this.deltaHeight = this.height - this.startHeight;\n }\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('animate');\n }\n\n return false;\n }\n\n public complete(): void\n {\n this.parent.plugins.remove('animate');\n if (this.width !== null)\n {\n this.parent.fitWidth(this.width, this.keepCenter, this.height === null);\n }\n if (this.height !== null)\n {\n this.parent.fitHeight(this.height, this.keepCenter, this.width === null);\n }\n if (!this.keepCenter)\n {\n this.parent.moveCenter(this.options.position as Point);\n }\n\n this.parent.emit('animate-end', this.parent);\n\n if (this.options.callbackOnComplete)\n {\n this.options.callbackOnComplete(this.parent);\n }\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n this.time += elapsed;\n\n if (this.time >= this.options.time)\n {\n this.complete();\n }\n else\n {\n const originalZoom = new Point(this.parent.scale.x, this.parent.scale.y);\n const percent = this.options.ease(this.time, 0, 1, this.options.time);\n\n if (this.width !== null)\n {\n const startWidth = this.startWidth as number;\n const deltaWidth = this.deltaWidth as number;\n\n this.parent.fitWidth(\n startWidth + (deltaWidth * percent),\n this.keepCenter,\n this.height === null);\n }\n if (this.height !== null)\n {\n const startHeight = this.startHeight as number;\n const deltaHeight = this.deltaHeight as number;\n\n this.parent.fitHeight(\n startHeight + (deltaHeight * percent),\n this.keepCenter,\n this.width === null);\n }\n if (this.width === null)\n {\n this.parent.scale.x = this.parent.scale.y;\n }\n else if (this.height === null)\n {\n this.parent.scale.y = this.parent.scale.x;\n }\n if (!this.keepCenter)\n {\n const startX = this.startX as number;\n const startY = this.startY as number;\n const deltaX = this.deltaX as number;\n const deltaY = this.deltaY as number;\n const original = new Point(this.parent.x, this.parent.y);\n\n this.parent.moveCenter(startX + (deltaX * percent), startY + (deltaY * percent));\n this.parent.emit('moved', { viewport: this.parent, original, type: 'animate' });\n }\n if (this.width || this.height)\n {\n this.parent.emit('zoomed', { viewport: this.parent, original: originalZoom, type: 'animate' });\n }\n }\n }\n}\n","import { Point, Rectangle } from '@pixi/math';\nimport { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Drag } from './Drag';\nimport type { IDecelerateOptions } from './Decelerate';\nimport type { Pinch } from './Pinch';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Bounce}. */\nexport interface IBounceOptions {\n /** \"all\", \"horizontal\", \"vertical\", or combination of \"top\", \"bottom\", \"right\", \"left\" (e.g., 'top-bottom-right') */\n sides?:\n 'all'\n | 'horizontal'\n | 'vertical'\n | string;\n\n /** Friction to apply to decelerate if active */\n friction?: number;\n\n /** Time in ms to finish bounce */\n time?: number;\n\n /** Use this bounceBox instead of (0, 0, viewport.worldWidth, viewport.worldHeight) */\n bounceBox?: Rectangle | null;\n\n /** Ease function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /** (top/bottom/center and left/right/center, or center) where to place world if too small for screen */\n underflow?: 'center' | string;\n}\n\n/** Bounce state along an axis */\nexport interface IBounceState {\n /** Elapsed time since bounce started */\n time: number;\n\n /** Starting coordinate */\n start: number;\n\n /** Change in coordinate through bounce */\n delta: number;\n\n /** Ending coordinate */\n end: number;\n}\n\nconst DEFAULT_BOUNCE_OPTIONS: Required = {\n sides: 'all',\n friction: 0.5,\n time: 150,\n ease: 'easeInOutSine',\n underflow: 'center',\n bounceBox: null\n};\n\n/**\n * @fires bounce-start-x\n * @fires bounce.end-x\n * @fires bounce-start-y\n * @fires bounce-end-y\n * @public\n */\nexport class Bounce extends Plugin\n{\n /** The options passed to initialize this plugin, cannot be modified again. */\n public readonly options: Readonly>;\n\n /** Holds whether to bounce from left side. */\n public readonly left: boolean ;\n\n /** Holds whether to bounce from top side. */\n public readonly top: boolean;\n\n /** Holds whether to bounce from right side. */\n public readonly right: boolean;\n\n /** Holds whether to bounce from bottom side. */\n public readonly bottom: boolean;\n\n /** Direction of underflow along x-axis. */\n public readonly underflowX: -1 | 0 | 1;\n\n /** Direction of underflow along y-axis. */\n public readonly underflowY: -1 | 0 | 1;\n\n /** Easing */\n protected ease: any;\n\n /** Bounce state along x-axis */\n protected toX!: IBounceState | null;\n\n /** Bounce state along y-axis */\n protected toY!: IBounceState | null;\n\n /**\n * This is called by {@link Viewport.bounce}.\n */\n constructor(parent: Viewport, options: IBounceOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_BOUNCE_OPTIONS, options);\n this.ease = ease(this.options.ease, 'easeInOutSine');\n\n if (this.options.sides)\n {\n if (this.options.sides === 'all')\n {\n this.top = this.bottom = this.left = this.right = true;\n }\n else if (this.options.sides === 'horizontal')\n {\n this.right = this.left = true;\n this.top = this.bottom = false;\n }\n else if (this.options.sides === 'vertical')\n {\n this.left = this.right = false;\n this.top = this.bottom = true;\n }\n else\n {\n this.top = this.options.sides.indexOf('top') !== -1;\n this.bottom = this.options.sides.indexOf('bottom') !== -1;\n this.left = this.options.sides.indexOf('left') !== -1;\n this.right = this.options.sides.indexOf('right') !== -1;\n }\n } else {\n this.left = this.top = this.right = this.bottom = false;\n }\n\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'center')\n {\n this.underflowX = 0;\n this.underflowY = 0;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n }\n\n this.reset();\n }\n\n public isActive(): boolean\n {\n return this.toX !== null || this.toY !== null;\n }\n\n public down(): boolean\n {\n this.toX = this.toY = null;\n\n return false;\n }\n\n public up(): boolean\n {\n this.bounce();\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n this.bounce();\n\n if (this.toX)\n {\n const toX = this.toX;\n\n toX.time += elapsed;\n this.parent.emit('moved', { viewport: this.parent, type: 'bounce-x' });\n\n if (toX.time >= this.options.time)\n {\n this.parent.x = toX.end;\n this.toX = null;\n this.parent.emit('bounce-x-end', this.parent);\n }\n else\n {\n this.parent.x = this.ease(toX.time, toX.start, toX.delta, this.options.time);\n }\n }\n\n if (this.toY)\n {\n const toY = this.toY;\n\n toY.time += elapsed;\n this.parent.emit('moved', { viewport: this.parent, type: 'bounce-y' });\n\n if (toY.time >= this.options.time)\n {\n this.parent.y = toY.end;\n this.toY = null;\n this.parent.emit('bounce-y-end', this.parent);\n }\n else\n {\n this.parent.y = this.ease(toY.time, toY.start, toY.delta, this.options.time);\n }\n }\n }\n\n /** @internal */\n protected calcUnderflowX(): number\n {\n let x: number;\n\n switch (this.underflowX)\n {\n case -1:\n x = 0;\n break;\n case 1:\n x = (this.parent.screenWidth - this.parent.screenWorldWidth);\n break;\n default:\n x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n }\n\n return x;\n }\n\n /** @internal */\n protected calcUnderflowY(): number\n {\n let y: number;\n\n switch (this.underflowY)\n {\n case -1:\n y = 0;\n break;\n case 1:\n y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n break;\n default:\n y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n }\n\n return y;\n }\n\n private oob(): Record<'left' | 'right' | 'top' | 'bottom', boolean> & Record<'topLeft' | 'bottomRight', Point>\n {\n const box = this.options.bounceBox;\n\n if (box)\n {\n const x1 = typeof box.x === 'undefined' ? 0 : box.x;\n const y1 = typeof box.y === 'undefined' ? 0 : box.y;\n const width = typeof box.width === 'undefined' ? this.parent.worldWidth : box.width;\n const height = typeof box.height === 'undefined' ? this.parent.worldHeight : box.height;\n\n return {\n left: this.parent.left < x1,\n right: this.parent.right > width,\n top: this.parent.top < y1,\n bottom: this.parent.bottom > height,\n topLeft: new Point(\n x1 * this.parent.scale.x,\n y1 * this.parent.scale.y\n ),\n bottomRight: new Point(\n width * this.parent.scale.x - this.parent.screenWidth,\n height * this.parent.scale.y - this.parent.screenHeight\n )\n };\n }\n\n return {\n left: this.parent.left < 0,\n right: this.parent.right > this.parent.worldWidth,\n top: this.parent.top < 0,\n bottom: this.parent.bottom > this.parent.worldHeight,\n topLeft: new Point(0, 0),\n bottomRight: new Point(\n this.parent.worldWidth * this.parent.scale.x - this.parent.screenWidth,\n this.parent.worldHeight * this.parent.scale.y - this.parent.screenHeight\n )\n };\n }\n\n public bounce(): void\n {\n if (this.paused)\n {\n return;\n }\n\n let oob;\n let decelerate: undefined | null | {\n percentChangeX?: number;\n percentChangeY?: number;\n x?: number;\n y?: number;\n options?: IDecelerateOptions\n } = this.parent.plugins.get('decelerate', true) as any;\n\n if (decelerate && (decelerate.x || decelerate.y))\n {\n if ((decelerate.x && decelerate.percentChangeX === decelerate.options?.friction) || (decelerate.y && decelerate.percentChangeY === decelerate.options?.friction))\n {\n oob = this.oob();\n if ((oob.left && this.left) || (oob.right && this.right))\n {\n decelerate.percentChangeX = this.options.friction;\n }\n if ((oob.top && this.top) || (oob.bottom && this.bottom))\n {\n decelerate.percentChangeY = this.options.friction;\n }\n }\n }\n const drag: Partial = this.parent.plugins.get('drag', true) || {};\n const pinch: Partial = this.parent.plugins.get('pinch', true) || {};\n\n decelerate = decelerate || {};\n\n if (!drag?.active && !pinch?.active && ((!this.toX || !this.toY) && (!decelerate.x || !decelerate.y)))\n {\n oob = oob || this.oob();\n const topLeft = oob.topLeft;\n const bottomRight = oob.bottomRight;\n\n if (!this.toX && !decelerate.x)\n {\n let x = null;\n\n if (oob.left && this.left)\n {\n x = (this.parent.screenWorldWidth < this.parent.screenWidth) ? this.calcUnderflowX() : -topLeft.x;\n }\n else if (oob.right && this.right)\n {\n x = (this.parent.screenWorldWidth < this.parent.screenWidth) ? this.calcUnderflowX() : -bottomRight.x;\n }\n if (x !== null && this.parent.x !== x)\n {\n this.toX = { time: 0, start: this.parent.x, delta: x - this.parent.x, end: x };\n this.parent.emit('bounce-x-start', this.parent);\n }\n }\n if (!this.toY && !decelerate.y)\n {\n let y = null;\n\n if (oob.top && this.top)\n {\n y = (this.parent.screenWorldHeight < this.parent.screenHeight) ? this.calcUnderflowY() : -topLeft.y;\n }\n else if (oob.bottom && this.bottom)\n {\n y = (this.parent.screenWorldHeight < this.parent.screenHeight) ? this.calcUnderflowY() : -bottomRight.y;\n }\n if (y !== null && this.parent.y !== y)\n {\n this.toY = { time: 0, start: this.parent.y, delta: y - this.parent.y, end: y };\n this.parent.emit('bounce-y-start', this.parent);\n }\n }\n }\n }\n\n public reset(): void\n {\n this.toX = this.toY = null;\n this.bounce();\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\n\n/**\n * There are three ways to clamp:\n * 1. direction: 'all' = the world is clamped to its world boundaries, ie, you cannot drag any part of the world offscreen\n * direction: 'x' | 'y' = only the x or y direction is clamped to its world boundary\n * 2. left, right, top, bottom = true | number = the world is clamped to the world's pixel location for each side;\n * if any of these are set to true, then the location is set to the boundary [0, viewport.worldWidth/viewport.worldHeight]\n * eg: to allow the world to be completely dragged offscreen, set [-viewport.worldWidth, -viewport.worldHeight, viewport.worldWidth * 2, viewport.worldHeight * 2]\n *\n * Underflow determines what happens when the world is smaller than the viewport\n * 1. none = the world is clamped but there is no special behavior\n * 2. center = the world is centered on the viewport\n * 3. combination of top/bottom/center and left/right/center (case insensitive) = the world is stuck to the appropriate boundaries\n *\n */\nexport interface IClampOptions\n{\n /**\n * Clamp left; true = 0\n *\n * @default false\n */\n left?: number | boolean | null;\n\n /**\n * Clamp top; true = 0\n *\n * @default false\n */\n top?: number | boolean | null;\n\n /**\n * Clamp right; true = viewport.worldWidth\n *\n * @default false\n */\n right?: number | boolean | null;\n\n /**\n * Clamp bottom; true = viewport.worldHeight\n *\n * @default false\n */\n bottom?: number | boolean | null;\n\n /**\n * (all, x, or y) using clamps of [0, viewport.worldWidth/viewport.worldHeight]; replaces left/right/top/bottom if set\n *\n * @default null\n */\n direction?: 'all' | 'x' | 'y' | null;\n\n /**\n * Where to place world if too small for screen (e.g., top-right, center, none, bottomleft)\n *\n * @default 'center'\n */\n underflow?: 'center' | string;\n}\n\nconst DEFAULT_CLAMP_OPTIONS: Required = {\n left: false,\n right: false,\n top: false,\n bottom: false,\n direction: null,\n underflow: 'center'\n};\n\n/**\n * Plugin to clamp the viewport to a specific world bounding box.\n *\n * @public\n */\nexport class Clamp extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Required;\n\n /** Last state of viewport */\n protected last: {\n x: number | null;\n y: number | null;\n scaleX: number | null;\n scaleY: number | null;\n };\n\n protected noUnderflow!: boolean;\n protected underflowX!: -1 | 0 | 1;\n protected underflowY!: -1 | 0 | 1;\n\n /**\n * This is called by {@link Viewport.clamp}.\n */\n constructor(parent: Viewport, options : IClampOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_CLAMP_OPTIONS, options);\n\n if (this.options.direction)\n {\n this.options.left = this.options.direction === 'x' || this.options.direction === 'all' ? true : null;\n this.options.right = this.options.direction === 'x' || this.options.direction === 'all' ? true : null;\n this.options.top = this.options.direction === 'y' || this.options.direction === 'all' ? true : null;\n this.options.bottom = this.options.direction === 'y' || this.options.direction === 'all' ? true : null;\n }\n\n this.parseUnderflow();\n this.last = { x: null, y: null, scaleX: null, scaleY: null };\n this.update();\n }\n\n private parseUnderflow()\n {\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'none')\n {\n this.noUnderflow = true;\n }\n else if (clamp === 'center')\n {\n this.underflowX = this.underflowY = 0;\n this.noUnderflow = false;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n this.noUnderflow = false;\n }\n }\n\n public move(): boolean\n {\n this.update();\n\n return false;\n }\n\n public update(): void\n {\n if (this.paused)\n {\n return;\n }\n\n // only clamp on change\n if (this.parent.x === this.last.x\n && this.parent.y === this.last.y\n && this.parent.scale.x === this.last.scaleX\n && this.parent.scale.y === this.last.scaleY)\n {\n return;\n }\n const original = { x: this.parent.x, y: this.parent.y };\n // TODO: Fix\n const decelerate: any = (this.parent.plugins as any).decelerate || {};\n\n if (this.options.left !== null || this.options.right !== null)\n {\n let moved = false;\n\n if (!this.noUnderflow && this.parent.screenWorldWidth < this.parent.screenWidth)\n {\n switch (this.underflowX)\n {\n case -1:\n if (this.parent.x !== 0)\n {\n this.parent.x = 0;\n moved = true;\n }\n break;\n case 1:\n if (this.parent.x !== this.parent.screenWidth - this.parent.screenWorldWidth)\n {\n this.parent.x = this.parent.screenWidth - this.parent.screenWorldWidth;\n moved = true;\n }\n break;\n default:\n if (this.parent.x !== (this.parent.screenWidth - this.parent.screenWorldWidth) / 2)\n {\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n moved = true;\n }\n }\n }\n else\n {\n if (this.options.left !== null)\n {\n if (this.parent.left < (this.options.left === true ? 0 : this.options.left))\n {\n this.parent.x = -(this.options.left === true ? 0 : this.options.left) * this.parent.scale.x;\n decelerate.x = 0;\n moved = true;\n }\n }\n if (this.options.right !== null)\n {\n if (this.parent.right > (this.options.right === true ? this.parent.worldWidth : this.options.right))\n {\n this.parent.x = -(this.options.right === true ? this.parent.worldWidth : this.options.right) * this.parent.scale.x + this.parent.screenWidth;\n decelerate.x = 0;\n moved = true;\n }\n }\n }\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, original, type: 'clamp-x' });\n }\n }\n if (this.options.top !== null || this.options.bottom !== null)\n {\n let moved = false;\n\n if (!this.noUnderflow && this.parent.screenWorldHeight < this.parent.screenHeight)\n {\n switch (this.underflowY)\n {\n case -1:\n if (this.parent.y !== 0)\n {\n this.parent.y = 0;\n moved = true;\n }\n break;\n case 1:\n if (this.parent.y !== this.parent.screenHeight - this.parent.screenWorldHeight)\n {\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n moved = true;\n }\n break;\n default:\n if (this.parent.y !== (this.parent.screenHeight - this.parent.screenWorldHeight) / 2)\n {\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n moved = true;\n }\n }\n }\n else\n {\n if (this.options.top !== null)\n {\n if (this.parent.top < (this.options.top === true ? 0 : this.options.top))\n {\n this.parent.y = -(this.options.top === true ? 0 : this.options.top)\n * this.parent.scale.y;\n decelerate.y = 0;\n moved = true;\n }\n }\n if (this.options.bottom !== null)\n {\n if (this.parent.bottom > (this.options.bottom === true ? this.parent.worldHeight : this.options.bottom))\n {\n this.parent.y = -(this.options.bottom === true ? this.parent.worldHeight : this.options.bottom)\n * this.parent.scale.y + this.parent.screenHeight;\n decelerate.y = 0;\n moved = true;\n }\n }\n }\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, original, type: 'clamp-y' });\n }\n }\n this.last.x = this.parent.x;\n this.last.y = this.parent.y;\n this.last.scaleX = this.parent.scale.x;\n this.last.scaleY = this.parent.scale.y;\n }\n\n public reset(): void\n {\n this.update();\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\n\n/**\n * Options for {@link ClampZoom}.\n *\n * Use either minimum width/height or minimum scale\n */\nexport interface IClampZoomOptions\n{\n /** Minimum width */\n minWidth?: number | null;\n\n /** Minimum height */\n minHeight?: number | null;\n\n /** Maximum width */\n maxWidth?: number | null;\n\n /** Maximum height */\n maxHeight?: number | null;\n\n /** Minimum scale */\n minScale?: number | null | IScale;\n\n /** Maximum scale */\n maxScale?: number | null | IScale;\n}\n\nconst DEFAULT_CLAMP_ZOOM_OPTIONS: Required = {\n minWidth: null,\n minHeight: null,\n maxWidth: null,\n maxHeight: null,\n minScale: null,\n maxScale: null\n};\n\n/**\n * Plugin to clamp the viewport's zoom to a specific range.\n *\n * @public\n */\nexport class ClampZoom extends Plugin\n{\n public readonly options: Required;\n\n /**\n * This is called by {@link Viewport.clampZoom}.\n */\n constructor(parent: Viewport, options = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_CLAMP_ZOOM_OPTIONS, options);\n\n this.clamp();\n }\n\n public resize(): void\n {\n this.clamp();\n }\n\n /** Clamp the viewport scale zoom) */\n public clamp(): void\n {\n if (this.paused)\n {\n return;\n }\n\n if (this.options.minWidth || this.options.minHeight || this.options.maxWidth || this.options.maxHeight)\n {\n let width = this.parent.worldScreenWidth;\n let height = this.parent.worldScreenHeight;\n\n if (this.options.minWidth !== null && width < this.options.minWidth)\n {\n const original = this.parent.scale.x;\n\n this.parent.fitWidth(this.options.minWidth, false, false, true);\n this.parent.scale.y *= this.parent.scale.x / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.maxWidth !== null && width > this.options.maxWidth)\n {\n const original = this.parent.scale.x;\n\n this.parent.fitWidth(this.options.maxWidth, false, false, true);\n this.parent.scale.y *= this.parent.scale.x / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.minHeight !== null && height < this.options.minHeight)\n {\n const original = this.parent.scale.y;\n\n this.parent.fitHeight(this.options.minHeight, false, false, true);\n this.parent.scale.x *= this.parent.scale.y / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.maxHeight !== null && height > this.options.maxHeight)\n {\n const original = this.parent.scale.y;\n\n this.parent.fitHeight(this.options.maxHeight, false, false, true);\n this.parent.scale.x *= this.parent.scale.y / original;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n }\n else\n if (this.options.minScale || this.options.maxScale)\n {\n const minScale: IScale = { x: null, y: null };\n const maxScale: IScale = { x: null, y: null };\n\n if (typeof this.options.minScale === 'number')\n {\n minScale.x = this.options.minScale;\n minScale.y = this.options.minScale;\n }\n else if (this.options.minScale !== null)\n {\n const optsMinScale = this.options.minScale as IScale;\n\n minScale.x = typeof optsMinScale.x === 'undefined' ? null : optsMinScale.x;\n minScale.y = typeof optsMinScale.y === 'undefined' ? null : optsMinScale.y;\n }\n\n if (typeof this.options.maxScale === 'number')\n {\n maxScale.x = this.options.maxScale;\n maxScale.y = this.options.maxScale;\n }\n else if (this.options.maxScale !== null)\n {\n const optsMaxScale = this.options.maxScale as IScale;\n\n maxScale.x = typeof optsMaxScale.x === 'undefined' ? null : optsMaxScale.x;\n maxScale.y = typeof optsMaxScale.y === 'undefined' ? null : optsMaxScale.y;\n }\n\n let scaleX = this.parent.scale.x;\n let scaleY = this.parent.scale.y;\n\n if (minScale.x !== null && scaleX < minScale.x)\n {\n scaleX = minScale.x;\n }\n if (maxScale.x !== null && scaleX > maxScale.x)\n {\n scaleX = maxScale.x;\n }\n if (minScale.y !== null && scaleY < minScale.y)\n {\n scaleY = minScale.y;\n }\n if (maxScale.y !== null && scaleY > maxScale.y)\n {\n scaleY = maxScale.y;\n }\n if (scaleX !== this.parent.scale.x || scaleY !== this.parent.scale.y)\n {\n this.parent.scale.set(scaleX, scaleY);\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n }\n }\n\n public reset(): void\n {\n this.clamp();\n }\n}\n\n/** This allows independent x and y values for min/maxScale */\nexport interface IScale {\n x: null | number\n y: null | number\n}\n","import { Plugin } from './Plugin';\n\nimport type { Point } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\nexport interface IDecelerateOptions {\n /**\n * Percent to decelerate after movement. This should be between 0 and 1, exclusive.\n *\n * @default 0.95\n */\n friction?: number;\n\n /**\n * Percent to decelerate when past boundaries (only applicable when viewport.bounce() is active)\n *\n * @default 0.8\n */\n bounce?: number;\n\n /**\n * Minimum velocity before stopping/reversing acceleration\n *\n * @default 0.01\n */\n minSpeed?: number;\n}\n\n/** Viewport position snapshot that's saved by {@link DeceleratePlugin} to estimate panning velocity. */\nexport interface IDecelerateSnapshot {\n /** x-coordinate of the viewport. */\n x: number;\n\n /** y-coordinate of the viewport. */\n y: number;\n\n /** Time at which this snapshot was taken. */\n time: number;\n}\n\nconst DEFAULT_DECELERATE_OPTIONS: Required = {\n friction: 0.98,\n bounce: 0.8,\n minSpeed: 0.01\n};\n\n/**\n * Time period of decay (1 frame)\n *\n * @internal\n * @ignore\n */\nconst TP = 16;\n\n/**\n * Plugin to decelerate viewport velocity smoothly after panning ends.\n *\n * @public\n */\nexport class Decelerate extends Plugin\n{\n /** Options used to initialize this plugin. */\n public readonly options: Required;\n\n /**\n * x-component of the velocity of viewport provided by this plugin, at the current time.\n *\n * This is measured in px/frame, where a frame is normalized to 16 milliseconds.\n */\n public x!: number | null;\n\n /**\n * y-component of the velocity of the viewport provided by this plugin, at the current time.\n *\n * This is measured in px/frame, where a frame is normalized to 16 milliseconds.\n */\n public y!: number | null;\n\n /**\n * The decay factor for the x-component of the viewport.\n *\n * The viewport's velocity decreased by this amount each 16 milliseconds.\n */\n public percentChangeX!: number;\n\n /**\n * The decay factor for the y-component of the viewport.\n *\n * The viewport's velocity decreased by this amount each 16 milliseconds.\n */\n public percentChangeY!: number;\n\n /** Saved list of recent viewport position snapshots, to estimate velocity. */\n protected saved: Array;\n\n /** The time since the user released panning of the viewport. */\n protected timeSinceRelease: number;\n\n /**\n * This is called by {@link Viewport.decelerate}.\n */\n constructor(parent: Viewport, options: IDecelerateOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_DECELERATE_OPTIONS, options);\n this.saved = [];\n this.timeSinceRelease = 0;\n\n this.reset();\n this.parent.on('moved', (data) => this.moved(data));\n }\n\n public down(): boolean\n {\n this.saved = [];\n this.x = this.y = null;\n\n return false;\n }\n\n public isActive(): boolean\n {\n return !!(this.x || this.y);\n }\n\n public move(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n const count = this.parent.input.count();\n\n if (count === 1 || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n this.saved.push({ x: this.parent.x, y: this.parent.y, time: performance.now() });\n\n if (this.saved.length > 60)\n {\n this.saved.splice(0, 30);\n }\n }\n\n // Silently recording viewport positions\n return false;\n }\n\n /** Listener to viewport's \"moved\" event. */\n protected moved(data: { type: 'clamp-x' | 'clamp-y'; original: Point }): void\n {\n if (this.saved.length)\n {\n const last = this.saved[this.saved.length - 1];\n\n if (data.type === 'clamp-x')\n {\n if (last.x === data.original.x)\n {\n last.x = this.parent.x;\n }\n }\n else if (data.type === 'clamp-y')\n {\n if (last.y === data.original.y)\n {\n last.y = this.parent.y;\n }\n }\n }\n }\n\n public up(): boolean\n {\n if (this.parent.input.count() === 0 && this.saved.length)\n {\n const now = performance.now();\n\n for (const save of this.saved)\n {\n if (save.time >= now - 100)\n {\n const time = now - save.time;\n\n this.x = (this.parent.x - save.x) / time;\n this.y = (this.parent.y - save.y) / time;\n this.percentChangeX = this.percentChangeY = this.options.friction;\n this.timeSinceRelease = 0;\n break;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Manually activate deceleration, starting from the (x, y) velocity components passed in the options.\n *\n * @param {object} options\n * @param {number} [options.x] - Specify x-component of initial velocity.\n * @param {number} [options.y] - Specify y-component of initial velocity.\n */\n public activate(options: { x?: number; y?: number; }): void\n {\n options = options || {};\n\n if (typeof options.x !== 'undefined')\n {\n this.x = options.x;\n this.percentChangeX = this.options.friction;\n }\n if (typeof options.y !== 'undefined')\n {\n this.y = options.y;\n this.percentChangeY = this.options.friction;\n }\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n /*\n * See https://github.com/davidfig/pixi-viewport/issues/271 for math.\n *\n * The viewport velocity (this.x, this.y) decays exponentially by the the decay factor\n * (this.percentChangeX, this.percentChangeY) each frame. This velocity function is integrated\n * to calculate the displacement.\n */\n\n const moved = this.x || this.y;\n\n const ti = this.timeSinceRelease;\n const tf = this.timeSinceRelease + elapsed;\n\n if (this.x)\n {\n const k = this.percentChangeX;\n const lnk = Math.log(k);\n\n // Apply velocity delta on the viewport x-coordinate.\n this.parent.x += ((this.x * TP) / lnk) * (Math.pow(k, tf / TP) - Math.pow(k, ti / TP));\n\n // Apply decay on x-component of velocity\n this.x *= Math.pow(this.percentChangeX, elapsed / TP);\n }\n if (this.y)\n {\n const k = this.percentChangeY;\n const lnk = Math.log(k);\n\n // Apply velocity delta on the viewport y-coordinate.\n this.parent.y += ((this.y * TP) / lnk) * (Math.pow(k, tf / TP) - Math.pow(k, ti / TP));\n\n // Apply decay on y-component of velocity\n this.y *= Math.pow(this.percentChangeY, elapsed / TP);\n }\n\n this.timeSinceRelease += elapsed;\n\n // End decelerate velocity once it goes under a certain amount of precision.\n if (Math.abs(this.x || 0) < this.options.minSpeed)\n {\n this.x = 0;\n }\n if (Math.abs(this.y || 0) < this.options.minSpeed)\n {\n this.y = 0;\n }\n\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, type: 'decelerate' });\n }\n }\n\n public reset(): void\n {\n this.x = this.y = null;\n }\n}\n","import { Point } from '@pixi/math';\nimport { Plugin } from './Plugin';\n\nimport type { Decelerate } from './Decelerate';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { IPointData } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Drag}. */\nexport interface IDragOptions {\n /**\n * direction to drag\n *\n * @default \"all\"\n */\n direction?: string;\n\n /**\n * whether click to drag is active\n *\n * @default true\n */\n pressDrag?: boolean;\n\n /**\n * Use wheel to scroll in direction (unless wheel plugin is active)\n *\n * @default true\n */\n wheel?: boolean;\n\n /**\n * number of pixels to scroll with each wheel spin\n *\n * @default 1\n */\n wheelScroll?: number;\n\n /**\n * reverse the direction of the wheel scroll\n *\n * @default false\n */\n reverse?: boolean;\n\n /**\n * clamp wheel(to avoid weird bounce with mouse wheel). Can be 'x' or 'y' or `true`.\n *\n * @default false\n */\n clampWheel?: boolean | string;\n\n /**\n * where to place world if too small for screen\n *\n * @default \"center\"\n */\n underflow?: string;\n\n /**\n * factor to multiply drag to increase the speed of movement\n *\n * @default 1\n */\n factor?: number;\n\n /**\n * Changes which mouse buttons trigger drag.\n *\n * Use: 'all', 'left', right' 'middle', or some combination, like, 'middle-right'; you may want to set\n * `viewport.options.disableOnContextMenu` if you want to use right-click dragging.\n *\n * @default \"all\"\n */\n mouseButtons?: 'all' | string;\n\n /**\n * Array containing {@link key|https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code} codes of\n * keys that can be pressed for the drag to be triggered, e.g.: ['ShiftLeft', 'ShiftRight'}.\n *\n * @default null\n */\n keyToPress?: string[] | null;\n\n /**\n * Ignore keyToPress for touch events.\n *\n * @default false\n */\n ignoreKeyToPressOnTouch?: boolean;\n\n /**\n * Scaling factor for non-DOM_DELTA_PIXEL scrolling events.\n *\n * @default 20\n */\n lineHeight?: number;\n}\n\nconst DEFAULT_DRAG_OPTIONS: Required = {\n direction: 'all',\n pressDrag: true,\n wheel: true,\n wheelScroll: 1,\n reverse: false,\n clampWheel: false,\n underflow: 'center',\n factor: 1,\n mouseButtons: 'all',\n keyToPress: null,\n ignoreKeyToPressOnTouch: false,\n lineHeight: 20,\n};\n\n/**\n * Plugin to enable panning/dragging of the viewport to move around.\n *\n * @public\n */\nexport class Drag extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Readonly>;\n\n /** Flags when viewport is moving. */\n protected moved: boolean;\n\n /** Factor to apply from {@link IDecelerateOptions}'s reverse. */\n protected reverse: 1 | -1;\n\n /** Holds whether dragging is enabled along the x-axis. */\n protected xDirection: boolean\n\n /** Holds whether dragging is enabled along the y-axis. */\n protected yDirection: boolean;\n\n /** Flags whether the keys required to drag are pressed currently. */\n protected keyIsPressed: boolean;\n\n /** Holds whether the left, center, and right buttons are required to pan. */\n protected mouse!: [boolean, boolean, boolean]\n\n /** Underflow factor along x-axis */\n protected underflowX!: -1 | 0 | 1;\n\n /** Underflow factor along y-axis */\n protected underflowY!: -1 | 0 | 1;\n\n /** Last pointer position while panning. */\n protected last?: IPointData | null\n\n /** The ID of the pointer currently panning the viewport. */\n protected current?: number;\n\n /**\n * This is called by {@link Viewport.drag}.\n */\n constructor(parent: Viewport, options = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_DRAG_OPTIONS, options);\n this.moved = false;\n this.reverse = this.options.reverse ? 1 : -1;\n this.xDirection = !this.options.direction || this.options.direction === 'all' || this.options.direction === 'x';\n this.yDirection = !this.options.direction || this.options.direction === 'all' || this.options.direction === 'y';\n this.keyIsPressed = false;\n\n this.parseUnderflow();\n this.mouseButtons(this.options.mouseButtons);\n\n if (this.options.keyToPress)\n {\n this.handleKeyPresses(this.options.keyToPress);\n }\n }\n\n /**\n * Handles keypress events and set the keyIsPressed boolean accordingly\n *\n * @param {array} codes - key codes that can be used to trigger drag event\n */\n protected handleKeyPresses(codes: string[]): void\n {\n window.addEventListener('keydown', (e) =>\n {\n if (codes.includes(e.code))\n { this.keyIsPressed = true; }\n });\n\n window.addEventListener('keyup', (e) =>\n {\n if (codes.includes(e.code))\n { this.keyIsPressed = false; }\n });\n }\n\n /**\n * initialize mousebuttons array\n * @param {string} buttons\n */\n protected mouseButtons(buttons: string): void\n {\n if (!buttons || buttons === 'all')\n {\n this.mouse = [true, true, true];\n }\n else\n {\n this.mouse = [\n buttons.indexOf('left') !== -1,\n buttons.indexOf('middle') !== -1,\n buttons.indexOf('right') !== -1\n ];\n }\n }\n\n protected parseUnderflow(): void\n {\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'center')\n {\n this.underflowX = 0;\n this.underflowY = 0;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n }\n }\n\n /**\n * @param {PIXI.InteractionEvent} event\n * @returns {boolean}\n */\n protected checkButtons(event: InteractionEvent): boolean\n {\n const isMouse = event.data.pointerType === 'mouse';\n const count = this.parent.input.count();\n\n if ((count === 1) || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n if (!isMouse || this.mouse[event.data.button])\n {\n return true;\n }\n }\n\n return false;\n }\n\n /**\n * @param {PIXI.InteractionEvent} event\n * @returns {boolean}\n */\n protected checkKeyPress(event: InteractionEvent): boolean\n {\n return (!this.options.keyToPress\n || this.keyIsPressed\n || (this.options.ignoreKeyToPressOnTouch && event.data.pointerType === 'touch'));\n }\n\n public down(event: InteractionEvent): boolean\n {\n if (this.paused || !this.options.pressDrag)\n {\n return false;\n }\n if (this.checkButtons(event) && this.checkKeyPress(event))\n {\n this.last = { x: event.data.global.x, y: event.data.global.y };\n this.current = event.data.pointerId;\n\n return true;\n }\n this.last = null;\n\n return false;\n }\n\n get active(): boolean\n {\n return this.moved;\n }\n\n public move(event: InteractionEvent): boolean\n {\n if (this.paused || !this.options.pressDrag)\n {\n return false;\n }\n if (this.last && this.current === event.data.pointerId)\n {\n const x = event.data.global.x;\n const y = event.data.global.y;\n const count = this.parent.input.count();\n\n if (count === 1 || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n const distX = x - this.last.x;\n const distY = y - this.last.y;\n\n if (this.moved\n || ((this.xDirection && this.parent.input.checkThreshold(distX))\n || (this.yDirection && this.parent.input.checkThreshold(distY))))\n {\n const newPoint = { x, y };\n\n if (this.xDirection)\n {\n this.parent.x += (newPoint.x - this.last.x) * this.options.factor;\n }\n if (this.yDirection)\n {\n this.parent.y += (newPoint.y - this.last.y) * this.options.factor;\n }\n this.last = newPoint;\n if (!this.moved)\n {\n this.parent.emit('drag-start', {\n event,\n screen: new Point(this.last.x, this.last.y),\n world: this.parent.toWorld(new Point(this.last.x, this.last.y)),\n viewport: this.parent\n });\n }\n this.moved = true;\n this.parent.emit('moved', { viewport: this.parent, type: 'drag' });\n\n return true;\n }\n }\n else\n {\n this.moved = false;\n }\n }\n\n return false;\n }\n\n public up(event: InteractionEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n const touches = this.parent.input.touches;\n\n if (touches.length === 1)\n {\n const pointer = touches[0];\n\n if (pointer.last)\n {\n this.last = { x: pointer.last.x, y: pointer.last.y };\n this.current = pointer.id;\n }\n this.moved = false;\n\n return true;\n }\n else if (this.last)\n {\n if (this.moved)\n {\n const screen = new Point(this.last.x, this.last.y);\n\n this.parent.emit('drag-end', {\n event, screen,\n world: this.parent.toWorld(screen),\n viewport: this.parent,\n });\n this.last = null;\n this.moved = false;\n\n return true;\n }\n }\n\n return false;\n }\n\n public wheel(event: WheelEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n if (this.options.wheel)\n {\n const wheel = this.parent.plugins.get('wheel', true);\n\n if (!wheel)\n {\n const step = event.deltaMode ? this.options.lineHeight : 1;\n\n if (this.xDirection)\n {\n this.parent.x += event.deltaX * step * this.options.wheelScroll * this.reverse;\n }\n if (this.yDirection)\n {\n this.parent.y += event.deltaY * step * this.options.wheelScroll * this.reverse;\n }\n if (this.options.clampWheel)\n {\n this.clamp();\n }\n this.parent.emit('wheel-scroll', this.parent);\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n if (!this.parent.options.passiveWheel)\n {\n event.preventDefault();\n }\n\n return true;\n }\n }\n\n return false;\n }\n\n public resume()\n {\n this.last = null;\n this.paused = false;\n }\n\n public clamp()\n {\n const decelerate: Partial = this.parent.plugins.get('decelerate', true) || {};\n\n if (this.options.clampWheel !== 'y')\n {\n if (this.parent.screenWorldWidth < this.parent.screenWidth)\n {\n switch (this.underflowX)\n {\n case -1:\n this.parent.x = 0;\n break;\n case 1:\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth);\n break;\n default:\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n }\n }\n else\n if (this.parent.left < 0)\n {\n this.parent.x = 0;\n decelerate.x = 0;\n }\n else if (this.parent.right > this.parent.worldWidth)\n {\n this.parent.x = -this.parent.worldWidth * this.parent.scale.x + this.parent.screenWidth;\n decelerate.x = 0;\n }\n }\n if (this.options.clampWheel !== 'x')\n {\n if (this.parent.screenWorldHeight < this.parent.screenHeight)\n {\n switch (this.underflowY)\n {\n case -1:\n this.parent.y = 0;\n break;\n case 1:\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n break;\n default:\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n }\n }\n else\n {\n if (this.parent.top < 0)\n {\n this.parent.y = 0;\n decelerate.y = 0;\n }\n if (this.parent.bottom > this.parent.worldHeight)\n {\n this.parent.y = -this.parent.worldHeight * this.parent.scale.y + this.parent.screenHeight;\n decelerate.y = 0;\n }\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { DisplayObject } from '@pixi/display';\nimport type { IPointData } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Follow}. */\nexport interface IFollowOptions\n{\n /**\n * Speed to follow in px/frame (0 = teleport to location)\n *\n * @default 9\n */\n speed?: number;\n\n /**\n * Set acceleration to accelerate and decelerate at this rate; speed cannot be 0 to use acceleration\n *\n * @default null\n */\n acceleration?: number | null;\n\n /**\n * Radius (in world coordinates) of center circle where movement is allowed without moving the viewport\n *\n * @default null\n */\n radius?: number | null;\n}\n\nconst DEFAULT_FOLLOW_OPTIONS: Required = {\n speed: 0,\n acceleration: null,\n radius: null\n};\n\n/**\n * Plugin to follow a display-object.\n *\n * @see Viewport.follow\n * @public\n */\nexport class Follow extends Plugin\n{\n /** The options used to initialize this plugin. */\n public readonly options: Required;\n\n /** The target this plugin will make the viewport follow. */\n public target: DisplayObject;\n\n /** The velocity provided the viewport by following, at the current time. */\n protected velocity: IPointData;\n\n /**\n * This is called by {@link Viewport.follow}.\n *\n * @param parent\n * @param target - target to follow\n * @param options\n */\n constructor(parent: Viewport, target: DisplayObject, options: IFollowOptions = {})\n {\n super(parent);\n\n this.target = target;\n this.options = Object.assign({}, DEFAULT_FOLLOW_OPTIONS, options);\n this.velocity = { x: 0, y: 0 };\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n const center = this.parent.center;\n let toX = this.target.x;\n let toY = this.target.y;\n\n if (this.options.radius)\n {\n const distance = Math.sqrt(Math.pow(this.target.y - center.y, 2) + Math.pow(this.target.x - center.x, 2));\n\n if (distance > this.options.radius)\n {\n const angle = Math.atan2(this.target.y - center.y, this.target.x - center.x);\n\n toX = this.target.x - (Math.cos(angle) * this.options.radius);\n toY = this.target.y - (Math.sin(angle) * this.options.radius);\n }\n else\n {\n return;\n }\n }\n\n const deltaX = toX - center.x;\n const deltaY = toY - center.y;\n\n if (deltaX || deltaY)\n {\n if (this.options.speed)\n {\n if (this.options.acceleration)\n {\n const angle = Math.atan2(toY - center.y, toX - center.x);\n const distance = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));\n\n if (distance)\n {\n const decelerationDistance = (Math.pow(this.velocity.x, 2) + Math.pow(this.velocity.y, 2)) / (2 * this.options.acceleration);\n\n if (distance > decelerationDistance)\n {\n this.velocity = {\n x: Math.min(this.velocity.x + this.options.acceleration * elapsed, this.options.speed),\n y: Math.min(this.velocity.y + this.options.acceleration * elapsed, this.options.speed)\n };\n }\n else\n {\n this.velocity = {\n x: Math.max(this.velocity.x - this.options.acceleration * this.options.speed, 0),\n y: Math.max(this.velocity.y - this.options.acceleration * this.options.speed, 0)\n };\n }\n const changeX = Math.cos(angle) * this.velocity.x;\n const changeY = Math.sin(angle) * this.velocity.y;\n const x = Math.abs(changeX) > Math.abs(deltaX) ? toX : center.x + changeX;\n const y = Math.abs(changeY) > Math.abs(deltaY) ? toY : center.y + changeY;\n\n this.parent.moveCenter(x, y);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n else\n {\n const angle = Math.atan2(toY - center.y, toX - center.x);\n const changeX = Math.cos(angle) * this.options.speed;\n const changeY = Math.sin(angle) * this.options.speed;\n const x = Math.abs(changeX) > Math.abs(deltaX) ? toX : center.x + changeX;\n const y = Math.abs(changeY) > Math.abs(deltaY) ? toY : center.y + changeY;\n\n this.parent.moveCenter(x, y);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n else\n {\n this.parent.moveCenter(toX, toY);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\nimport type { InteractionEvent } from '@pixi/interaction';\n\n/** Insets for mouse edges scrolling regions */\nexport interface IMouseEdgesInsets {\n /** Distance from center of screen in screen pixels */\n radius?: number | null;\n\n /** Distance from all sides in screen pixels */\n distance?: number | null;\n\n /** Alternatively, set top distance (leave unset for no top scroll) */\n top?: number | null;\n\n /** Alternatively, set bottom distance (leave unset for no top scroll) */\n bottom?: number | null;\n\n /** Alternatively, set left distance (leave unset for no top scroll) */\n left?: number | null;\n\n /** Alternatively, set right distance (leave unset for no top scroll) */\n right?: number | null;\n}\n\n/** Options for {@link MouseEdges}. */\nexport interface IMouseEdgesOptions extends IMouseEdgesInsets {\n /** Speed in pixels/frame to scroll viewport */\n speed?: number;\n\n /** Reverse direction of scroll */\n reverse?: boolean;\n\n /** Don't use decelerate plugin even if it's installed */\n noDecelerate?: boolean;\n\n /**\n * If using radius, use linear movement (+/- 1, +/- 1) instead of angled movement.\n *\n * (Math.cos(angle from center), Math.sin(angle from center))\n */\n linear?: boolean;\n\n /** Allows plugin to continue working even when there's a `mousedown` event. */\n allowButtons?: boolean;\n}\n\nconst MOUSE_EDGES_OPTIONS: Required = {\n radius: null,\n distance: null,\n top: null,\n bottom: null,\n left: null,\n right: null,\n speed: 8,\n reverse: false,\n noDecelerate: false,\n linear: false,\n allowButtons: false,\n};\n\n/**\n * Scroll viewport when mouse hovers near one of the edges.\n *\n * @event mouse-edge-start(Viewport) emitted when mouse-edge starts\n * @event mouse-edge-end(Viewport) emitted when mouse-edge ends\n */\nexport class MouseEdges extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Readonly>;\n\n /** Factor from reverse option. */\n protected readonly reverse: -1 | 1;\n\n /** Radius squared */\n protected readonly radiusSquared: number | null;\n\n /** Scroll region size on the left side. */\n protected left!: number | null;\n\n /** Scroll region size on the top size. */\n protected top!: number | null;\n\n /** Scroll region size on the right side. */\n protected right!: number | null;\n\n /** Scroll region size on the bottom side. */\n protected bottom!: number | null;\n\n protected horizontal?: number | null;\n\n protected vertical?: number | null;\n\n /**\n * This is called by {@link Viewport.mouseEdges}.\n */\n constructor(parent: Viewport, options: IMouseEdgesOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, MOUSE_EDGES_OPTIONS, options);\n this.reverse = this.options.reverse ? 1 : -1;\n this.radiusSquared = typeof this.options.radius === 'number' ? Math.pow(this.options.radius, 2) : null;\n\n this.resize();\n }\n\n public resize(): void\n {\n const distance = this.options.distance;\n\n if (distance !== null)\n {\n this.left = distance;\n this.top = distance;\n this.right = this.parent.worldScreenWidth - distance;\n this.bottom = this.parent.worldScreenHeight - distance;\n }\n else if (!this.options.radius)\n {\n this.left = this.options.left;\n this.top = this.options.top;\n this.right = this.options.right === null ? null : this.parent.worldScreenWidth - this.options.right;\n this.bottom = this.options.bottom === null ? null : this.parent.worldScreenHeight - this.options.bottom;\n }\n }\n\n public down(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if (!this.options.allowButtons)\n {\n this.horizontal = this.vertical = null;\n }\n\n return false;\n }\n\n public move(event: InteractionEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if ((event.data.pointerType !== 'mouse' && event.data.identifier !== 1)\n || (!this.options.allowButtons && event.data.buttons !== 0))\n {\n return false;\n }\n\n const x = event.data.global.x;\n const y = event.data.global.y;\n\n if (this.radiusSquared)\n {\n const center = this.parent.toScreen(this.parent.center);\n const distance = Math.pow(center.x - x, 2) + Math.pow(center.y - y, 2);\n\n if (distance >= this.radiusSquared)\n {\n const angle = Math.atan2(center.y - y, center.x - x);\n\n if (this.options.linear)\n {\n this.horizontal = Math.round(Math.cos(angle)) * this.options.speed * this.reverse * (60 / 1000);\n this.vertical = Math.round(Math.sin(angle)) * this.options.speed * this.reverse * (60 / 1000);\n }\n else\n {\n this.horizontal = Math.cos(angle) * this.options.speed * this.reverse * (60 / 1000);\n this.vertical = Math.sin(angle) * this.options.speed * this.reverse * (60 / 1000);\n }\n }\n else\n {\n if (this.horizontal)\n {\n this.decelerateHorizontal();\n }\n if (this.vertical)\n {\n this.decelerateVertical();\n }\n\n this.horizontal = this.vertical = 0;\n }\n }\n else\n {\n if (this.left !== null && x < this.left)\n {\n this.horizontal = Number(this.reverse) * this.options.speed * (60 / 1000);\n }\n else if (this.right !== null && x > this.right)\n {\n this.horizontal = -1 * this.reverse * this.options.speed * (60 / 1000);\n }\n else\n {\n this.decelerateHorizontal();\n this.horizontal = 0;\n }\n if (this.top !== null && y < this.top)\n {\n this.vertical = Number(this.reverse) * this.options.speed * (60 / 1000);\n }\n else if (this.bottom !== null && y > this.bottom)\n {\n this.vertical = -1 * this.reverse * this.options.speed * (60 / 1000);\n }\n else\n {\n this.decelerateVertical();\n this.vertical = 0;\n }\n }\n\n return false;\n }\n\n private decelerateHorizontal(): void\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (this.horizontal && decelerate && !this.options.noDecelerate)\n {\n decelerate.activate({ x: (this.horizontal * this.options.speed * this.reverse) / (1000 / 60) });\n }\n }\n\n private decelerateVertical(): void\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (this.vertical && decelerate && !this.options.noDecelerate)\n {\n decelerate.activate({ y: (this.vertical * this.options.speed * this.reverse) / (1000 / 60) });\n }\n }\n\n public up(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if (this.horizontal)\n {\n this.decelerateHorizontal();\n }\n if (this.vertical)\n {\n this.decelerateVertical();\n }\n this.horizontal = this.vertical = null;\n\n return false;\n }\n\n public update(): void\n {\n if (this.paused)\n {\n return;\n }\n\n if (this.horizontal || this.vertical)\n {\n const center = this.parent.center;\n\n if (this.horizontal)\n {\n center.x += this.horizontal * this.options.speed;\n }\n if (this.vertical)\n {\n center.y += this.vertical * this.options.speed;\n }\n\n this.parent.moveCenter(center);\n this.parent.emit('moved', { viewport: this.parent, type: 'mouse-edges' });\n }\n }\n}\n","import { Plugin } from './Plugin';\nimport { Point } from '@pixi/math';\n\nimport type { IPointData } from '@pixi/math';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { IViewportTouch } from '../InputManager';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Pinch}. */\nexport interface IPinchOptions\n{\n /** Disable two-finger dragging. */\n noDrag?: boolean;\n\n /**\n * Percent to modify pinch speed.\n *\n * @default 1\n */\n percent?: number;\n\n /**\n * Factor to multiply two-finger drag to increase speed of movement\n *\n * @default 1\n */\n factor?: number;\n\n /** Place this point at center during zoom instead of center of two fingers */\n center?: Point | null;\n\n /** Axis to zoom */\n axis?: 'all' | 'x' | 'y';\n}\n\nconst DEFAULT_PINCH_OPTIONS: Required = {\n noDrag: false,\n percent: 1,\n center: null,\n factor: 1,\n axis: 'all',\n};\n\n/**\n * Plugin for enabling two-finger pinching (or dragging).\n *\n * @public\n */\nexport class Pinch extends Plugin\n{\n /** Options used to initialize this plugin. */\n public readonly options: Required;\n\n /** Flags whether this plugin is active, i.e. a pointer is down on the viewport. */\n public active = false;\n\n /** Flags whether the viewport is being pinched. */\n public pinching = false;\n\n protected moved = false;\n protected lastCenter?: IPointData | null;\n\n /**\n * This is called by {@link Viewport.pinch}.\n */\n constructor(parent: Viewport, options: IPinchOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_PINCH_OPTIONS, options);\n }\n\n public down(): boolean\n {\n if (this.parent.input.count() >= 2)\n {\n this.active = true;\n return true;\n }\n\n return false;\n }\n\n public isAxisX(): boolean\n {\n return ['all', 'x'].includes(this.options.axis);\n }\n\n public isAxisY(): boolean\n {\n return ['all', 'y'].includes(this.options.axis);\n }\n\n public move(e: InteractionEvent): boolean\n {\n if (this.paused || !this.active)\n {\n return false;\n }\n\n const x = e.data.global.x;\n const y = e.data.global.y;\n\n const pointers = this.parent.input.touches;\n\n if (pointers.length >= 2)\n {\n const first = pointers[0] as IViewportTouch;\n const second = pointers[1] as IViewportTouch;\n const last = (first.last && second.last)\n ? Math.sqrt(Math.pow(second.last.x - first.last.x, 2) + Math.pow(second.last.y - first.last.y, 2))\n : null;\n\n if (first.id === e.data.pointerId)\n {\n first.last = { x, y, data: e.data } as IPointData;\n }\n else if (second.id === e.data.pointerId)\n {\n second.last = { x, y, data: e.data } as IPointData;\n }\n if (last)\n {\n let oldPoint: IPointData | undefined;\n\n const point = {\n x: (first.last as IPointData).x +\n ((second.last as IPointData).x - (first.last as IPointData).x) / 2,\n y: (first.last as IPointData).y +\n ((second.last as IPointData).y - (first.last as IPointData).y) / 2,\n };\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point);\n }\n let dist = Math.sqrt(Math.pow(\n (second.last as IPointData).x - (first.last as IPointData).x, 2) +\n Math.pow((second.last as IPointData).y - (first.last as IPointData).y, 2));\n\n dist = dist === 0 ? dist = 0.0000000001 : dist;\n\n const change = (1 - last / dist) * this.options.percent\n * (this.isAxisX() ? this.parent.scale.x : this.parent.scale.y);\n\n if (this.isAxisX())\n {\n this.parent.scale.x += change;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y += change;\n }\n\n this.parent.emit('zoomed', { viewport: this.parent, type: 'pinch', center: point });\n\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += (point.x - newPoint.x) * this.options.factor;\n this.parent.y += (point.y - newPoint.y) * this.options.factor;\n this.parent.emit('moved', { viewport: this.parent, type: 'pinch' });\n }\n if (!this.options.noDrag && this.lastCenter)\n {\n this.parent.x += (point.x - this.lastCenter.x) * this.options.factor;\n this.parent.y += (point.y - this.lastCenter.y) * this.options.factor;\n this.parent.emit('moved', { viewport: this.parent, type: 'pinch' });\n }\n\n this.lastCenter = point;\n this.moved = true;\n }\n else if (!this.pinching)\n {\n this.parent.emit('pinch-start', this.parent);\n this.pinching = true;\n }\n\n return true;\n }\n\n return false;\n }\n\n public up(): boolean\n {\n if (this.pinching)\n {\n if (this.parent.input.touches.length <= 1)\n {\n this.active = false;\n this.lastCenter = null;\n this.pinching = false;\n this.moved = false;\n this.parent.emit('pinch-end', this.parent);\n\n return true;\n }\n }\n\n return false;\n }\n}\n","import { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Viewport } from '../Viewport';\n\nexport interface ISnapOptions\n{\n /** snap to the top-left of viewport instead of center */\n topLeft?: boolean;\n\n /**\n * Friction/frame to apply if decelerate is active\n *\n * @default 0.8\n */\n friction?: number;\n\n /**\n * @default 1000\n */\n time?: number;\n\n /** Easing function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /**\n * Pause snapping with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Removes this plugin after snapping is complete\n *\n * @default false\n */\n removeOnComplete?: boolean;\n\n /**\n * Removes this plugin if interrupted by any user input\n *\n * @default false\n */\n removeOnInterrupt?: boolean;\n\n /**\n * Starts the snap immediately regardless of whether the viewport is at the desired location\n *\n * @default false\n */\n forceStart?: boolean;\n}\n\nconst DEFAULT_SNAP_OPTIONS: Required = {\n topLeft: false,\n friction: 0.8,\n time: 1000,\n ease: 'easeInOutSine',\n interrupt: true,\n removeOnComplete: false,\n removeOnInterrupt: false,\n forceStart: false\n};\n\n/**\n * @event snap-start(Viewport) emitted each time a snap animation starts\n * @event snap-restart(Viewport) emitted each time a snap resets because of a change in viewport size\n * @event snap-end(Viewport) emitted each time snap reaches its target\n * @event snap-remove(Viewport) emitted if snap plugin is removed\n */\nexport class Snap extends Plugin\n{\n public readonly options: Required;\n public ease?: any;\n public x: number;\n public y: number;\n\n protected percent?: number;\n protected snapping?: { time: number } | null;\n protected deltaX?: number;\n protected deltaY?: number;\n protected startX?: number;\n protected startY?: number;\n\n /**\n * This is called by {@link Viewport.snap}.\n */\n constructor(parent: Viewport, x: number, y: number, options: ISnapOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_SNAP_OPTIONS, options);\n this.ease = ease(options.ease, 'easeInOutSine');\n this.x = x;\n this.y = y;\n\n if (this.options.forceStart)\n {\n this.snapStart();\n }\n }\n\n public snapStart(): void\n {\n this.percent = 0;\n this.snapping = { time: 0 };\n const current = this.options.topLeft ? this.parent.corner : this.parent.center;\n\n this.deltaX = this.x - current.x;\n this.deltaY = this.y - current.y;\n this.startX = current.x;\n this.startY = current.y;\n this.parent.emit('snap-start', this.parent);\n }\n\n public wheel(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap');\n }\n\n return false;\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap');\n }\n else if (this.options.interrupt)\n {\n this.snapping = null;\n }\n\n return false;\n }\n\n public up(): boolean\n {\n if (this.parent.input.count() === 0)\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (decelerate && (decelerate.x || decelerate.y))\n {\n decelerate.percentChangeX = decelerate.percentChangeY = this.options.friction;\n }\n }\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n if (this.options.interrupt && this.parent.input.count() !== 0)\n {\n return;\n }\n if (!this.snapping)\n {\n const current = this.options.topLeft ? this.parent.corner : this.parent.center;\n\n if (current.x !== this.x || current.y !== this.y)\n {\n this.snapStart();\n }\n }\n else\n {\n const snapping = this.snapping;\n\n snapping.time += elapsed;\n let finished;\n let x;\n let y;\n\n const startX = this.startX as number;\n const startY = this.startY as number;\n const deltaX = this.deltaX as number;\n const deltaY = this.deltaY as number;\n\n if (snapping.time > this.options.time)\n {\n finished = true;\n x = startX + deltaX;\n y = startY + deltaY;\n }\n else\n {\n const percent = this.ease(snapping.time, 0, 1, this.options.time);\n\n x = startX + (deltaX * percent);\n y = startY + (deltaY * percent);\n }\n if (this.options.topLeft)\n {\n this.parent.moveCorner(x, y);\n }\n else\n {\n this.parent.moveCenter(x, y);\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'snap' });\n\n if (finished)\n {\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap');\n }\n this.parent.emit('snap-end', this.parent);\n this.snapping = null;\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Point } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link SnapZoom}. */\nexport interface ISnapZoomOptions\n{\n /** the desired width to snap (to maintain aspect ratio, choose only width or height) */\n width?: number;\n\n /** the desired height to snap (to maintain aspect ratio, choose only width or height) */\n height?: number;\n\n /**\n * time for snapping in ms\n *\n * @default 1000\n */\n time?: number;\n\n /** ease function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /** Place this point at center during zoom instead of center of the viewport */\n center?: Point | null;\n\n /**\n * Pause snapping with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Removes this plugin after snapping is complete\n *\n * @default false\n */\n removeOnComplete?: boolean;\n\n /**\n * Removes this plugin if interrupted by any user input\n *\n * @default false\n */\n removeOnInterrupt?: boolean;\n\n /**\n * Starts the snap immediately regardless of whether the viewport is at the desired zoom\n *\n * @default false\n */\n forceStart?: boolean;\n\n /**\n * Zoom but do not move\n *\n * @default false\n */\n noMove?: boolean;\n}\n\nconst DEFAULT_SNAP_ZOOM_OPTIONS: Required = {\n width: 0,\n height: 0,\n time: 1000,\n ease: 'easeInOutSine',\n center: null,\n interrupt: true,\n removeOnComplete: false,\n removeOnInterrupt: false,\n forceStart: false,\n noMove: false\n};\n\n/**\n * @event snap-zoom-start(Viewport) emitted each time a fit animation starts\n * @event snap-zoom-end(Viewport) emitted each time fit reaches its target\n * @event snap-zoom-end(Viewport) emitted each time fit reaches its target\n */\nexport class SnapZoom extends Plugin\n{\n public readonly options: Required;\n\n protected ease: any;\n protected xScale: number;\n protected yScale: number;\n protected xIndependent: boolean;\n protected yIndependent: boolean;\n protected snapping?: {\n time: number;\n startX: number;\n startY: number;\n deltaX: number;\n deltaY: number;\n } | null;\n\n /**\n * This is called by {@link Viewport.snapZoom}.\n */\n constructor(parent: Viewport, options: ISnapZoomOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_SNAP_ZOOM_OPTIONS, options);\n this.ease = ease(this.options.ease);\n\n // Assign defaults for typescript.\n this.xIndependent = false;\n this.yIndependent = false;\n this.xScale = 0;\n this.yScale = 0;\n\n if (this.options.width > 0)\n {\n this.xScale = parent.screenWidth / this.options.width;\n this.xIndependent = true;\n }\n if (this.options.height > 0)\n {\n this.yScale = parent.screenHeight / this.options.height;\n this.yIndependent = true;\n }\n\n this.xScale = this.xIndependent ? (this.xScale as number) : (this.yScale as number);\n this.yScale = this.yIndependent ? (this.yScale as number) : this.xScale;\n\n if (this.options.time === 0)\n {\n // TODO: Fix this\n // @ts-expect-error todo\n parent.container.scale.x = this.xScale;\n\n // TODO: Fix this\n // @ts-expect-error todo\n parent.container.scale.y = this.yScale;\n\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n }\n else if (options.forceStart)\n {\n this.createSnapping();\n }\n }\n\n private createSnapping(): void\n {\n const startWorldScreenWidth = this.parent.worldScreenWidth;\n const startWorldScreenHeight = this.parent.worldScreenHeight;\n const endWorldScreenWidth = this.parent.screenWidth / this.xScale;\n const endWorldScreenHeight = this.parent.screenHeight / this.yScale;\n\n this.snapping = {\n time: 0,\n startX: startWorldScreenWidth,\n startY: startWorldScreenHeight,\n deltaX: endWorldScreenWidth - startWorldScreenWidth,\n deltaY: endWorldScreenHeight - startWorldScreenHeight\n };\n\n this.parent.emit('snap-zoom-start', this.parent);\n }\n\n public resize(): void\n {\n this.snapping = null;\n\n if (this.options.width > 0)\n {\n this.xScale = this.parent.screenWidth / this.options.width;\n }\n if (this.options.height > 0)\n {\n this.yScale = this.parent.screenHeight / this.options.height;\n }\n this.xScale = this.xIndependent ? this.xScale : this.yScale;\n this.yScale = this.yIndependent ? this.yScale : this.xScale;\n }\n\n public wheel(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n\n return false;\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n else if (this.options.interrupt)\n {\n this.snapping = null;\n }\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n if (this.options.interrupt && this.parent.input.count() !== 0)\n {\n return;\n }\n\n let oldCenter: Point | undefined;\n\n if (!this.options.center && !this.options.noMove)\n {\n oldCenter = this.parent.center;\n }\n if (!this.snapping)\n {\n if (this.parent.scale.x !== this.xScale || this.parent.scale.y !== this.yScale)\n {\n this.createSnapping();\n }\n }\n else if (this.snapping)\n {\n const snapping = this.snapping;\n\n snapping.time += elapsed;\n\n if (snapping.time >= this.options.time)\n {\n this.parent.scale.set(this.xScale, this.yScale);\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n this.parent.emit('snap-zoom-end', this.parent);\n this.snapping = null;\n }\n else\n {\n const snapping = this.snapping;\n const worldScreenWidth = this.ease(snapping.time, snapping.startX, snapping.deltaX, this.options.time);\n const worldScreenHeight = this.ease(snapping.time, snapping.startY, snapping.deltaY, this.options.time);\n\n this.parent.scale.x = this.parent.screenWidth / worldScreenWidth;\n this.parent.scale.y = this.parent.screenHeight / worldScreenHeight;\n }\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (!this.options.noMove)\n {\n if (!this.options.center)\n {\n this.parent.moveCenter(oldCenter as Point);\n }\n else\n {\n this.parent.moveCenter(this.options.center);\n }\n }\n }\n }\n\n public resume(): void\n {\n this.snapping = null;\n super.resume();\n }\n}\n","import { Plugin } from './Plugin';\nimport { IPointData, Point } from '@pixi/math';\n\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Wheel}. */\nexport interface IWheelOptions\n{\n /**\n * Percent to scroll with each spin\n *\n * @default 0.1\n */\n percent?: number;\n\n /**\n * smooth the zooming by providing the number of frames to zoom between wheel spins\n *\n * @default false\n */\n smooth?: false | number;\n\n /**\n * Stop smoothing with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Reverse the direction of the scroll\n *\n * @default false\n */\n reverse?: boolean;\n\n /**\n * Place this point at center during zoom instead of current mouse position\n *\n * @default null\n */\n center?: Point | null;\n\n /**\n * Scaling factor for non-DOM_DELTA_PIXEL scrolling events\n *\n * @default 20\n */\n lineHeight?: number;\n\n /**\n * Axis to zoom\n *\n * @default 'all'\n */\n axis?: 'all' | 'x' | 'y';\n}\n\nconst DEFAULT_WHEEL_OPTIONS: Required = {\n percent: 0.1,\n smooth: false,\n interrupt: true,\n reverse: false,\n center: null,\n lineHeight: 20,\n axis: 'all',\n};\n\n/**\n * Plugin for handling wheel scrolling for viewport zoom.\n *\n * @event wheel({wheel: {dx, dy, dz}, event, viewport})\n */\nexport class Wheel extends Plugin\n{\n public readonly options: Required;\n\n protected smoothing?: IPointData | null;\n protected smoothingCenter?: Point | null;\n protected smoothingCount?: number;\n\n /**\n * This is called by {@link Viewport.wheel}.\n */\n constructor(parent: Viewport, options: IWheelOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_WHEEL_OPTIONS, options);\n }\n\n public down(): boolean\n {\n if (this.options.interrupt)\n {\n this.smoothing = null;\n }\n\n return false;\n }\n\n protected isAxisX(): boolean\n {\n return ['all', 'x'].includes(this.options.axis);\n }\n\n protected isAxisY(): boolean\n {\n return ['all', 'y'].includes(this.options.axis);\n }\n\n public update(): void\n {\n if (this.smoothing)\n {\n const point = this.smoothingCenter;\n const change = this.smoothing;\n let oldPoint;\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point as IPointData);\n }\n if (this.isAxisX())\n {\n this.parent.scale.x += change.x;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y += change.y;\n }\n\n this.parent.emit('zoomed', { viewport: this.parent, type: 'wheel' });\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += (point as IPointData).x - newPoint.x;\n this.parent.y += (point as IPointData).y - newPoint.y;\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n (this.smoothingCount as number)++;\n\n if (this.smoothingCount as number >= this.options.smooth)\n {\n this.smoothing = null;\n }\n }\n }\n\n public wheel(e: WheelEvent): boolean | undefined\n {\n if (this.paused)\n {\n return;\n }\n\n const point = this.parent.input.getPointerPosition(e);\n const sign = this.options.reverse ? -1 : 1;\n const step = sign * -e.deltaY * (e.deltaMode ? this.options.lineHeight : 1) / 500;\n const change = Math.pow(2, (1 + this.options.percent) * step);\n\n if (this.options.smooth)\n {\n const original = {\n x: this.smoothing ? this.smoothing.x * (this.options.smooth - (this.smoothingCount as number)) : 0,\n y: this.smoothing ? this.smoothing.y * (this.options.smooth - (this.smoothingCount as number)) : 0\n };\n\n this.smoothing = {\n x: ((this.parent.scale.x + original.x) * change - this.parent.scale.x) / this.options.smooth,\n y: ((this.parent.scale.y + original.y) * change - this.parent.scale.y) / this.options.smooth,\n };\n this.smoothingCount = 0;\n this.smoothingCenter = point;\n }\n else\n {\n let oldPoint: IPointData | undefined;\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point);\n }\n if (this.isAxisX())\n {\n this.parent.scale.x *= change;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y *= change;\n }\n this.parent.emit('zoomed', { viewport: this.parent, type: 'wheel' });\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += point.x - newPoint.x;\n this.parent.y += point.y - newPoint.y;\n }\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n this.parent.emit('wheel', { wheel: { dx: e.deltaX, dy: e.deltaY, dz: e.deltaZ }, event: e, viewport: this.parent });\n\n if (!this.parent.options.passiveWheel)\n {\n return true;\n }\n }\n}\n","import { Point, Rectangle } from '@pixi/math';\n\nimport type { IPointData } from '@pixi/math';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { Viewport } from './Viewport';\n\nexport interface IViewportTouch {\n id: number;\n last: IPointData | null;\n}\n\n/**\n * Handles all input for Viewport\n *\n * @internal\n * @ignore\n * @private\n */\nexport class InputManager\n{\n public readonly viewport: Viewport;\n\n public clickedAvailable?: boolean;\n public isMouseDown?: boolean;\n public last?: Point | null;\n public wheelFunction?: (e: WheelEvent) => void;\n /** List of active touches on viewport */\n public touches: IViewportTouch[];\n\n constructor(viewport: Viewport)\n {\n this.viewport = viewport;\n this.touches = [];\n\n this.addListeners();\n }\n\n /** Add input listeners */\n private addListeners()\n {\n this.viewport.interactive = true;\n if (!this.viewport.forceHitArea)\n {\n this.viewport.hitArea = new Rectangle(0, 0, this.viewport.worldWidth, this.viewport.worldHeight);\n }\n this.viewport.on('pointerdown', this.down, this);\n this.viewport.on('pointermove', this.move, this);\n this.viewport.on('pointerup', this.up, this);\n this.viewport.on('pointerupoutside', this.up, this);\n this.viewport.on('pointercancel', this.up, this);\n this.viewport.on('pointerout', this.up, this);\n this.wheelFunction = (e) => this.handleWheel(e);\n this.viewport.options.divWheel.addEventListener(\n 'wheel',\n this.wheelFunction as any,\n { passive: this.viewport.options.passiveWheel });\n this.isMouseDown = false;\n }\n\n /**\n * Removes all event listeners from viewport\n * (useful for cleanup of wheel when removing viewport)\n */\n public destroy()\n {\n this.viewport.options.divWheel.removeEventListener('wheel', this.wheelFunction as any);\n }\n\n /**\n * handle down events for viewport\n *\n * @param {PIXI.InteractionEvent} event\n */\n public down(event: InteractionEvent)\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n if (event.data.pointerType === 'mouse')\n {\n this.isMouseDown = true;\n }\n else if (!this.get(event.data.pointerId))\n {\n this.touches.push({ id: event.data.pointerId, last: null });\n }\n if (this.count() === 1)\n {\n this.last = event.data.global.clone();\n\n // clicked event does not fire if viewport is decelerating or bouncing\n const decelerate = this.viewport.plugins.get('decelerate', true);\n const bounce = this.viewport.plugins.get('bounce', true);\n\n if ((!decelerate || !decelerate.isActive()) && (!bounce || !bounce.isActive()))\n {\n this.clickedAvailable = true;\n }\n else\n {\n this.clickedAvailable = false;\n }\n }\n else\n {\n this.clickedAvailable = false;\n }\n\n const stop = this.viewport.plugins.down(event);\n\n if (stop && this.viewport.options.stopPropagation)\n {\n event.stopPropagation();\n }\n }\n\n /** Clears all pointer events */\n public clear(): void\n {\n this.isMouseDown = false;\n this.touches = [];\n this.last = null;\n }\n\n /**\n * @param {number} change\n * @returns whether change exceeds threshold\n */\n public checkThreshold(change: number): boolean\n {\n if (Math.abs(change) >= this.viewport.threshold)\n {\n return true;\n }\n\n return false;\n }\n\n /** Handle move events for viewport */\n public move(event: InteractionEvent): void\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n\n const stop = this.viewport.plugins.move(event);\n\n if (this.clickedAvailable && this.last)\n {\n const distX = event.data.global.x - this.last.x;\n const distY = event.data.global.y - this.last.y;\n\n if (this.checkThreshold(distX) || this.checkThreshold(distY))\n {\n this.clickedAvailable = false;\n }\n }\n\n if (stop && this.viewport.options.stopPropagation)\n {\n event.stopPropagation();\n }\n }\n\n /** Handle up events for viewport */\n public up(event: InteractionEvent): void\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n\n if (event.data.pointerType === 'mouse')\n {\n this.isMouseDown = false;\n }\n\n if (event.data.pointerType !== 'mouse')\n {\n this.remove(event.data.pointerId);\n }\n\n const stop = this.viewport.plugins.up(event);\n\n if (this.clickedAvailable && this.count() === 0 && this.last)\n {\n this.viewport.emit('clicked', {\n event,\n screen: this.last,\n world: this.viewport.toWorld(this.last),\n viewport: this\n });\n this.clickedAvailable = false;\n }\n\n if (stop && this.viewport.options.stopPropagation)\n {\n event.stopPropagation();\n }\n }\n\n /** Gets pointer position if this.interaction is set */\n public getPointerPosition(event: WheelEvent): Point\n {\n const point = new Point();\n\n if (this.viewport.options.interaction)\n {\n this.viewport.options.interaction.mapPositionToPoint(point, event.clientX, event.clientY);\n }\n else\n {\n point.x = event.clientX;\n point.y = event.clientY;\n }\n\n return point;\n }\n\n /** Handle wheel events */\n public handleWheel(event: WheelEvent): void\n {\n if (this.viewport.pause || !this.viewport.worldVisible)\n {\n return;\n }\n\n // do not handle events coming from other elements\n if (this.viewport.options.interaction\n && (this.viewport.options.interaction as any).interactionDOMElement !== event.target)\n {\n return;\n }\n\n // only handle wheel events where the mouse is over the viewport\n const point = this.viewport.toLocal(this.getPointerPosition(event));\n\n if (this.viewport.left <= point.x\n && point.x <= this.viewport.right\n && this.viewport.top <= point.y\n && point.y <= this.viewport.bottom)\n {\n const stop = this.viewport.plugins.wheel(event);\n\n if (stop && !this.viewport.options.passiveWheel)\n {\n event.preventDefault();\n }\n }\n }\n\n public pause(): void\n {\n this.touches = [];\n this.isMouseDown = false;\n }\n\n /** Get touch by id */\n public get(id: number): IViewportTouch | null\n {\n for (const touch of this.touches)\n {\n if (touch.id === id)\n {\n return touch;\n }\n }\n\n return null;\n }\n\n /** Remove touch by number */\n remove(id: number): void\n {\n for (let i = 0; i < this.touches.length; i++)\n {\n if (this.touches[i].id === id)\n {\n this.touches.splice(i, 1);\n\n return;\n }\n }\n }\n\n /**\n * @returns {number} count of mouse/touch pointers that are down on the viewport\n */\n count(): number\n {\n return (this.isMouseDown ? 1 : 0) + this.touches.length;\n }\n}\n","import type {\n Animate,\n Bounce,\n Clamp,\n ClampZoom,\n Decelerate,\n Drag,\n Follow,\n MouseEdges,\n Pinch,\n Plugin,\n Snap,\n SnapZoom,\n Wheel,\n} from './plugins';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { Viewport } from './Viewport';\n\nconst PLUGIN_ORDER = [\n 'drag',\n 'pinch',\n 'wheel',\n 'follow',\n 'mouse-edges',\n 'decelerate',\n 'aniamte',\n 'bounce',\n 'snap-zoom',\n 'clamp-zoom',\n 'snap',\n 'clamp',\n];\n\n/**\n * Use this to access current plugins or add user-defined plugins\n *\n * @public\n */\nexport class PluginManager\n{\n /** Maps mounted plugins by their type */\n public plugins: Partial>;\n\n /**\n * List of plugins mounted\n *\n * This list is kept sorted by the internal priority of plugins (hard-coded).\n */\n public list: Array;\n\n /** The viewport using the plugins managed by `this`. */\n public readonly viewport: Viewport;\n\n /** This is called by {@link Viewport} to initialize the {@link Viewport.plugins plugins}. */\n constructor(viewport: Viewport)\n {\n this.viewport = viewport;\n this.list = [];\n this.plugins = {};\n }\n\n /**\n * Inserts a named plugin or a user plugin into the viewport\n * default plugin order: 'drag', 'pinch', 'wheel', 'follow', 'mouse-edges', 'decelerate', 'bounce',\n * 'snap-zoom', 'clamp-zoom', 'snap', 'clamp'\n *\n * @param {string} name of plugin\n * @param {Plugin} plugin - instantiated Plugin class\n * @param {number} index to insert userPlugin (otherwise inserts it at the end)\n */\n public add(name: string, plugin: Plugin, index: number = PLUGIN_ORDER.length)\n {\n this.plugins[name] = plugin;\n\n const current = PLUGIN_ORDER.indexOf(name);\n\n if (current !== -1)\n {\n PLUGIN_ORDER.splice(current, 1);\n }\n\n PLUGIN_ORDER.splice(index, 0, name);\n this.sort();\n }\n\n public get(name: 'animate', ignorePaused?: boolean): Animate | undefined | null;\n public get(name: 'bounce', ignorePaused?: boolean): Bounce | undefined | null;\n public get(name: 'clamp', ignorePaused?: boolean): Clamp | undefined | null;\n public get(name: 'clamp-zoom', ignorePaused?: boolean): ClampZoom | undefined | null;\n public get(name: 'decelerate', ignorePaused?: boolean): Decelerate | undefined | null;\n public get(name: 'drag', ignorePaused?: boolean): Drag | undefined | null;\n public get(name: 'follow', ignorePaused?: boolean): Follow | undefined | null;\n public get(name: 'mouse-edges', ignorePaused?: boolean): MouseEdges | undefined | null;\n public get(name: 'pinch', ignorePaused?: boolean): Pinch | undefined | null;\n public get(name: 'snap', ignorePaused?: boolean): Snap | undefined | null;\n public get(name: 'snap-zoom', ignorePaused?: boolean): SnapZoom | undefined | null;\n public get(name: 'wheel', ignorePaused?: boolean): Wheel | undefined | null;\n public get(name: string, ignorePaused?: boolean): T | undefined | null;\n\n /**\n * Get plugin\n *\n * @param {string} name of plugin\n * @param {boolean} [ignorePaused] return null if plugin is paused\n */\n public get(name: string, ignorePaused?: boolean): T | undefined | null\n {\n if (ignorePaused)\n {\n if (this.plugins[name]?.paused)\n {\n return null;\n }\n }\n\n return this.plugins[name] as T;\n }\n\n /**\n * Update all active plugins\n *\n * @internal\n * @ignore\n * @param {number} elapsed type in milliseconds since last update\n */\n public update(elapsed: number): void\n {\n for (const plugin of this.list)\n {\n plugin.update(elapsed);\n }\n }\n\n /**\n * Resize all active plugins\n *\n * @internal\n * @ignore\n */\n public resize(): void\n {\n for (const plugin of this.list)\n {\n plugin.resize();\n }\n }\n\n /** Clamps and resets bounce and decelerate (as needed) after manually moving viewport */\n public reset(): void\n {\n for (const plugin of this.list)\n {\n plugin.reset();\n }\n }\n\n /** removes all installed plugins */\n public removeAll(): void\n {\n this.plugins = {};\n this.sort();\n }\n\n /**\n * Removes installed plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public remove(name: string): void\n {\n if (this.plugins[name])\n {\n delete this.plugins[name];\n this.viewport.emit(`${name}-remove`);\n this.sort();\n }\n }\n\n /**\n * Pause plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public pause(name: string): void\n {\n this.plugins[name]?.pause();\n }\n\n /**\n * Resume plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public resume(name: string): void\n {\n this.plugins[name]?.resume();\n }\n\n /**\n * Sort plugins according to PLUGIN_ORDER\n *\n * @internal\n * @ignore\n */\n public sort()\n {\n this.list = [];\n\n for (const plugin of PLUGIN_ORDER)\n {\n if (this.plugins[plugin])\n {\n this.list.push(this.plugins[plugin] as Plugin);\n }\n }\n }\n\n /**\n * Handle down for all plugins\n *\n * @internal\n * @ignore\n */\n public down(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.list)\n {\n if (plugin.down(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle move for all plugins\n *\n * @internal\n * @ignore\n */\n public move(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.viewport.plugins.list)\n {\n if (plugin.move(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle up for all plugins\n *\n * @internal\n * @ignore\n */\n public up(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.list)\n {\n if (plugin.up(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle wheel event for all plugins\n *\n * @internal\n * @ignore\n */\n public wheel(e: WheelEvent): boolean\n {\n let result = false;\n\n for (const plugin of this.list)\n {\n if (plugin.wheel(e))\n {\n result = true;\n }\n }\n\n return result;\n }\n}\n","import { Container } from '@pixi/display';\nimport { Point, Rectangle } from '@pixi/math';\nimport { Ticker } from '@pixi/ticker';\n\nimport { InputManager } from './InputManager';\nimport { PluginManager } from './PluginManager';\nimport {\n Animate,\n Bounce,\n Clamp,\n ClampZoom,\n Decelerate,\n Drag,\n Follow,\n MouseEdges,\n Pinch,\n Snap,\n SnapZoom,\n Wheel\n} from './plugins';\n\nimport type { DisplayObject, IDestroyOptions } from '@pixi/display';\nimport type { IHitArea, InteractionManager } from '@pixi/interaction';\n\n/** Options for {@link Viewport}. */\nexport interface IViewportOptions {\n /** @default window.innerWidth */\n screenWidth?: number;\n\n /** @default window.innerHeight */\n screenHeight?: number;\n\n /** @default this.width */\n worldWidth?: number | null;\n\n /** @default this.height */\n worldHeight?: number | null;\n\n /**\n * Number of pixels to move to trigger an input event (e.g., drag, pinch) or disable a clicked event\n *\n * @default 5\n */\n threshold?: number;\n\n /**\n * Whether the 'wheel' event is set to passive (note: if false, e.preventDefault() will be called when wheel is used over the viewport)\n *\n * @default true\n */\n passiveWheel?: boolean;\n\n /**\n * Whether to stopPropagation of events that impact the viewport (except wheel events, see options.passiveWheel)\n */\n stopPropagation?: boolean;\n\n /**\n * Change the default hitArea from world size to a new value\n */\n forceHitArea?: Rectangle | null;\n\n /**\n * Set this if you want to manually call update() function on each frame\n *\n * @default false\n */\n noTicker?: boolean;\n\n /**\n * InteractionManager, available from instantiated `WebGLRenderer/CanvasRenderer.plugins.interaction`\n *\n * It's used to calculate pointer postion relative to canvas location on screen\n */\n interaction?: InteractionManager | null;\n\n /**\n * Remove oncontextmenu=() => {} from the divWheel element\n */\n disableOnContextMenu?: boolean;\n\n /**\n * div to attach the wheel event\n *\n * @default document.body\n */\n divWheel?: HTMLElement;\n\n /**\n * Use this PIXI.ticker for updates\n *\n * @default PIXI.Ticker.shared\n */\n ticker?: Ticker;\n}\n\nexport interface ICompleteViewportOptions extends IViewportOptions {\n screenWidth: number;\n screenHeight: number;\n threshold: number;\n passiveWheel: boolean;\n stopPropagation: boolean;\n noTicker: boolean;\n ticker: Ticker;\n}\n\nexport interface IViewportTransformState {\n x: number;\n y: number;\n scaleX: number;\n scaleY: number;\n}\n\nconst DEFAULT_VIEWPORT_OPTIONS: ICompleteViewportOptions = {\n screenWidth: window.innerWidth,\n screenHeight: window.innerHeight,\n worldWidth: null,\n worldHeight: null,\n threshold: 5,\n passiveWheel: true,\n stopPropagation: false,\n forceHitArea: null,\n noTicker: false,\n interaction: null,\n disableOnContextMenu: false,\n ticker: Ticker.shared,\n};\n\n/**\n * Main class to use when creating a Viewport\n *\n * @public\n * @fires clicked\n * @fires drag-start\n * @fires drag-end\n * @fires drag-remove\n * @fires pinch-start\n * @fires pinch-end\n * @fires pinch-remove\n * @fires snap-start\n * @fires snap-end\n * @fires snap-remove\n * @fires snap-zoom-start\n * @fires snap-zoom-end\n * @fires snap-zoom-remove\n * @fires bounce-x-start\n * @fires bounce-x-end\n * @fires bounce-y-start\n * @fires bounce-y-end\n * @fires bounce-remove\n * @fires wheel\n * @fires wheel-remove\n * @fires wheel-scroll\n * @fires wheel-scroll-remove\n * @fires mouse-edge-start\n * @fires mouse-edge-end\n * @fires mouse-edge-remove\n * @fires moved\n * @fires moved-end\n * @fires zoomed\n * @fires zoomed-end\n * @fires frame-end\n */\nexport class Viewport extends Container\n{\n /** Flags whether the viewport is being panned */\n public moving?: boolean;\n\n public screenWidth: number;\n public screenHeight: number;\n\n /** Number of pixels to move to trigger an input event (e.g., drag, pinch) or disable a clicked event */\n public threshold: number;\n\n public readonly input: InputManager;\n\n /** Use this to add user plugins or access existing plugins (e.g., to pause, resume, or remove them) */\n public readonly plugins: PluginManager;\n\n /** Flags whether the viewport zoom is being changed. */\n public zooming?: boolean;\n\n public lastViewport?: IViewportTransformState | null;\n\n /** The options passed when creating this viewport, merged with the default values */\n public readonly options: ICompleteViewportOptions & { divWheel: HTMLElement };\n\n private _dirty?: boolean;\n private _forceHitArea?: IHitArea | null;\n private _hitAreaDefault?: Rectangle;\n private _pause?: boolean;\n private readonly tickerFunction?: () => void;\n private _worldWidth?: number | null;\n private _worldHeight?: number | null;\n\n /**\n * @param options\n */\n constructor(options: IViewportOptions = {})\n {\n super();\n this.options = Object.assign(\n {},\n { divWheel: document.body },\n DEFAULT_VIEWPORT_OPTIONS,\n options\n );\n\n this.screenWidth = this.options.screenWidth;\n this.screenHeight = this.options.screenHeight;\n\n this._worldWidth = this.options.worldWidth;\n this._worldHeight = this.options.worldHeight;\n this.forceHitArea = this.options.forceHitArea;\n this.threshold = this.options.threshold;\n\n this.options.divWheel = this.options.divWheel || document.body;\n\n if (this.options.disableOnContextMenu)\n {\n this.options.divWheel.oncontextmenu = (e) => e.preventDefault();\n }\n if (!this.options.noTicker)\n {\n this.tickerFunction = () => this.update(this.options.ticker.elapsedMS);\n this.options.ticker.add(this.tickerFunction);\n }\n\n this.input = new InputManager(this);\n this.plugins = new PluginManager(this);\n }\n\n /** Overrides PIXI.Container's destroy to also remove the 'wheel' and PIXI.Ticker listeners */\n destroy(options: IDestroyOptions)\n {\n if (!this.options.noTicker && this.tickerFunction)\n {\n this.options.ticker.remove(this.tickerFunction);\n }\n\n this.input.destroy();\n super.destroy(options);\n }\n\n /**\n * Update viewport on each frame.\n *\n * By default, you do not need to call this unless you set `options.noTicker=true`.\n *\n * @param {number} elapsed time in milliseconds since last update\n */\n update(elapsed: number): void\n {\n if (!this.pause)\n {\n this.plugins.update(elapsed);\n\n if (this.lastViewport)\n {\n // Check for moved-end event\n if (this.lastViewport.x !== this.x || this.lastViewport.y !== this.y)\n {\n this.moving = true;\n }\n else if (this.moving)\n {\n this.emit('moved-end', this);\n this.moving = false;\n }\n\n // Check for zoomed-end event\n if (this.lastViewport.scaleX !== this.scale.x || this.lastViewport.scaleY !== this.scale.y)\n {\n this.zooming = true;\n }\n else if (this.zooming)\n {\n this.emit('zoomed-end', this);\n this.zooming = false;\n }\n }\n\n if (!this.forceHitArea)\n {\n this._hitAreaDefault = new Rectangle(this.left, this.top, this.worldScreenWidth, this.worldScreenHeight);\n this.hitArea = this._hitAreaDefault;\n }\n\n this._dirty = this._dirty || !this.lastViewport\n || this.lastViewport.x !== this.x || this.lastViewport.y !== this.y\n || this.lastViewport.scaleX !== this.scale.x || this.lastViewport.scaleY !== this.scale.y;\n\n this.lastViewport = {\n x: this.x,\n y: this.y,\n scaleX: this.scale.x,\n scaleY: this.scale.y\n };\n this.emit('frame-end', this);\n }\n }\n\n /** Use this to set screen and world sizes, needed for pinch/wheel/clamp/bounce. */\n resize(\n screenWidth = window.innerWidth,\n screenHeight = window.innerHeight,\n worldWidth: number,\n worldHeight: number\n ): void\n {\n this.screenWidth = screenWidth;\n this.screenHeight = screenHeight;\n\n if (typeof worldWidth !== 'undefined')\n {\n this._worldWidth = worldWidth;\n }\n if (typeof worldHeight !== 'undefined')\n {\n this._worldHeight = worldHeight;\n }\n\n this.plugins.resize();\n this.dirty = true;\n }\n\n /** World width, in pixels */\n get worldWidth(): number\n {\n if (this._worldWidth)\n {\n return this._worldWidth;\n }\n\n return this.width / this.scale.x;\n }\n set worldWidth(value)\n {\n this._worldWidth = value;\n this.plugins.resize();\n }\n\n /** World height, in pixels */\n get worldHeight(): number\n {\n if (this._worldHeight)\n {\n return this._worldHeight;\n }\n\n return this.height / this.scale.y;\n }\n set worldHeight(value)\n {\n this._worldHeight = value;\n this.plugins.resize();\n }\n\n /** Get visible world bounds of viewport */\n public getVisibleBounds(): Rectangle\n {\n return new Rectangle(this.left, this.top, this.worldScreenWidth, this.worldScreenHeight);\n }\n\n /** Change coordinates from screen to world */\n public toWorld(x: number, y: number): Point;\n /** Change coordinates from screen to world */\n public toWorld(screenPoint: Point): Point;\n\n public toWorld(x: number | Point, y?: number): Point\n {\n if (arguments.length === 2)\n {\n return this.toLocal(new Point(x as number, y));\n }\n\n return this.toLocal(x as Point);\n }\n\n /** Change coordinates from world to screen */\n public toScreen(x: number, y: number): Point;\n /** Change coordinates from world to screen */\n public toScreen(worldPoint: Point): Point;\n\n public toScreen(x: number | Point, y?: number): Point\n {\n if (arguments.length === 2)\n {\n return this.toGlobal(new Point(x as number, y));\n }\n\n return this.toGlobal(x as Point);\n }\n\n /** Screen width in world coordinates */\n get worldScreenWidth(): number\n {\n return this.screenWidth / this.scale.x;\n }\n\n /** Screen height in world coordinates */\n get worldScreenHeight(): number\n {\n return this.screenHeight / this.scale.y;\n }\n\n /** World width in screen coordinates */\n get screenWorldWidth(): number\n {\n return this.worldWidth * this.scale.x;\n }\n\n /** World height in screen coordinates */\n get screenWorldHeight()\n {\n return this.worldHeight * this.scale.y;\n }\n\n /** Center of screen in world coordinates */\n get center(): Point\n {\n return new Point(\n this.worldScreenWidth / 2 - this.x / this.scale.x,\n this.worldScreenHeight / 2 - this.y / this.scale.y,\n );\n }\n set center(value: Point)\n {\n this.moveCenter(value);\n }\n\n /** Move center of viewport to (x, y) */\n public moveCenter(x: number, y: number): this;\n\n /** Move center of viewport to {@code center}. */\n public moveCenter(center: Point): this;\n\n public moveCenter(...args: [number, number] | [Point]): this\n {\n let x: number;\n let y: number;\n\n if (typeof args[0] === 'number')\n {\n x = args[0];\n y = args[1] as number;\n }\n else\n {\n x = args[0].x;\n y = args[0].y;\n }\n\n const newX = (this.worldScreenWidth / 2 - x) * this.scale.x;\n const newY = (this.worldScreenHeight / 2 - y) * this.scale.y;\n\n if (this.x !== newX || this.y !== newY)\n {\n this.position.set(newX, newY);\n this.plugins.reset();\n this.dirty = true;\n }\n\n return this;\n }\n\n /** Top-left corner of Viewport */\n get corner(): Point\n {\n return new Point(-this.x / this.scale.x, -this.y / this.scale.y);\n }\n set corner(value: Point)\n {\n this.moveCorner(value);\n }\n\n /** Move viewport's top-left corner; also clamps and resets decelerate and bounce (as needed) */\n public moveCorner(x: number, y: number): this;\n\n /** move viewport's top-left corner; also clamps and resets decelerate and bounce (as needed) */\n public moveCorner(center: Point): this;\n\n public moveCorner(...args: [number, number] | [Point])\n {\n let x;\n let y;\n\n if (args.length === 1)\n {\n x = -args[0].x * this.scale.x;\n y = -args[0].y * this.scale.y;\n }\n else\n {\n x = -args[0] * this.scale.x;\n y = -args[1] * this.scale.y;\n }\n\n if (x !== this.x || y !== this.y)\n {\n this.position.set(x, y);\n this.plugins.reset();\n this.dirty = true;\n }\n\n return this;\n }\n\n /** Get how many world pixels fit in screen's width */\n get screenWidthInWorldPixels()\n {\n return this.screenWidth / this.scale.x;\n }\n\n /** Get how many world pixels fit on screen's height */\n get screenHeightInWorldPixels()\n {\n return this.screenHeight / this.scale.y;\n }\n\n /**\n * Find the scale value that fits a world width on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param width - Width in world pixels\n * @return - scale\n */\n findFitWidth(width: number): number\n {\n return this.screenWidth / width;\n }\n\n /**\n * Finds the scale value that fits a world height on the screens\n * does not change the viewport (use fit... to change)\n *\n * @param height - Height in world pixels\n * @return - scale\n */\n findFitHeight(height: number): number\n {\n return this.screenHeight / height;\n }\n\n /**\n * Finds the scale value that fits the smaller of a world width and world height on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param {number} width in world pixels\n * @param {number} height in world pixels\n * @returns {number} scale\n */\n findFit(width: number, height: number): number\n {\n const scaleX = this.screenWidth / width;\n const scaleY = this.screenHeight / height;\n\n return Math.min(scaleX, scaleY);\n }\n\n /**\n * Finds the scale value that fits the larger of a world width and world height on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param {number} width in world pixels\n * @param {number} height in world pixels\n * @returns {number} scale\n */\n findCover(width: number, height: number): number\n {\n const scaleX = this.screenWidth / width;\n const scaleY = this.screenHeight / height;\n\n return Math.max(scaleX, scaleY);\n }\n\n /**\n * Change zoom so the width fits in the viewport\n *\n * @param width - width in world coordinates\n * @param center - maintain the same center\n * @param scaleY - whether to set scaleY=scaleX\n * @param noClamp - whether to disable clamp-zoom\n * @returns {Viewport} this\n */\n fitWidth(width = this.worldWidth, center?: boolean, scaleY = true, noClamp?: boolean): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.x = this.screenWidth / width;\n\n if (scaleY)\n {\n this.scale.y = this.scale.x;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (!noClamp && clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so the height fits in the viewport\n *\n * @param {number} [height=this.worldHeight] in world coordinates\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @param {boolean} [scaleX=true] whether to set scaleX = scaleY\n * @param {boolean} [noClamp] whether to disable clamp-zoom\n * @returns {Viewport} this\n */\n fitHeight(height = this.worldHeight, center?: boolean, scaleX = true, noClamp?: boolean)\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.y = this.screenHeight / height;\n\n if (scaleX)\n {\n this.scale.x = this.scale.y;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (!noClamp && clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so it fits the entire world in the viewport\n *\n * @param {boolean} center maintain the same center of the screen after zoom\n * @returns {Viewport} this\n */\n fitWorld(center?: boolean): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n\n this.scale.x = this.screenWidth / this.worldWidth;\n this.scale.y = this.screenHeight / this.worldHeight;\n\n if (this.scale.x < this.scale.y)\n {\n this.scale.y = this.scale.x;\n }\n else\n {\n this.scale.x = this.scale.y;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so it fits the size or the entire world in the viewport\n *\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @param {number} [width=this.worldWidth] desired width\n * @param {number} [height=this.worldHeight] desired height\n * @returns {Viewport} this\n */\n fit(center?: boolean, width = this.worldWidth, height = this.worldHeight): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n\n this.scale.x = this.screenWidth / width;\n this.scale.y = this.screenHeight / height;\n\n if (this.scale.x < this.scale.y)\n {\n this.scale.y = this.scale.x;\n }\n else\n {\n this.scale.x = this.scale.y;\n }\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n // eslint-disable-next-line\n // @ts-ignore\n set visible(value: boolean)\n {\n if (!value)\n {\n this.input.clear();\n }\n\n super.visible = value;\n }\n\n /**\n * Zoom viewport to specific value.\n *\n * @param {number} scale value (e.g., 1 would be 100%, 0.25 would be 25%)\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n setZoom(scale: number, center?: boolean): this\n {\n let save;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.set(scale);\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Zoom viewport by a certain percent (in both x and y direction).\n *\n * @param {number} percent change (e.g., 0.25 would increase a starting scale of 1.0 to 1.25)\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n zoomPercent(percent: number, center?: boolean): this\n {\n return this.setZoom(this.scale.x + this.scale.x * percent, center);\n }\n\n /**\n * Zoom viewport by increasing/decreasing width by a certain number of pixels.\n *\n * @param {number} change in pixels\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n zoom(change: number, center?: boolean)\n {\n this.fitWidth(change + this.worldScreenWidth, center);\n\n return this;\n }\n\n /** Changes scale of viewport and maintains center of viewport */\n get scaled(): number\n {\n return this.scale.x;\n }\n set scaled(scale: number)\n {\n this.setZoom(scale, true);\n }\n\n /**\n * @param {SnapZoomOptions} options\n */\n snapZoom(options: any): this\n {\n this.plugins.add('snap-zoom', new SnapZoom(this, options));\n\n return this;\n }\n\n /** Is container out of world bounds */\n OOB(): {\n left: boolean;\n right: boolean;\n top: boolean;\n bottom: boolean;\n cornerPoint: Point;\n }\n {\n return {\n left: this.left < 0,\n right: this.right > this.worldWidth,\n top: this.top < 0,\n bottom: this.bottom > this.worldHeight,\n cornerPoint: new Point(\n this.worldWidth * this.scale.x - this.screenWidth,\n this.worldHeight * this.scale.y - this.screenHeight\n )\n };\n }\n\n /** World coordinates of the right edge of the screen */\n get right(): number\n {\n return -this.x / this.scale.x + this.worldScreenWidth;\n }\n set right(value: number)\n {\n this.x = -value * this.scale.x + this.screenWidth;\n this.plugins.reset();\n }\n\n /** World coordinates of the left edge of the screen */\n get left(): number\n {\n return -this.x / this.scale.x;\n }\n set left(value: number)\n {\n this.x = -value * this.scale.x;\n this.plugins.reset();\n }\n\n /** World coordinates of the top edge of the screen */\n get top(): number\n {\n return -this.y / this.scale.y;\n }\n set top(value: number)\n {\n this.y = -value * this.scale.y;\n this.plugins.reset();\n }\n\n /** World coordinates of the bottom edge of the screen */\n get bottom(): number\n {\n return -this.y / this.scale.y + this.worldScreenHeight;\n }\n set bottom(value: number)\n {\n this.y = -value * this.scale.y + this.screenHeight;\n this.plugins.reset();\n }\n\n /**\n * Determines whether the viewport is dirty (i.e., needs to be renderered to the screen because of a change)\n */\n get dirty(): boolean\n {\n return !!this._dirty;\n }\n set dirty(value: boolean)\n {\n this._dirty = value;\n }\n\n /**\n * Permanently changes the Viewport's hitArea\n *\n * NOTE: if not set then hitArea = PIXI.Rectangle(Viewport.left, Viewport.top, Viewport.worldScreenWidth, Viewport.worldScreenHeight)\n */\n get forceHitArea(): IHitArea | null | undefined\n {\n return this._forceHitArea;\n }\n set forceHitArea(value: IHitArea | null | undefined)\n {\n if (value)\n {\n this._forceHitArea = value;\n this.hitArea = value;\n }\n else\n {\n this._forceHitArea = null;\n this.hitArea = new Rectangle(0, 0, this.worldWidth, this.worldHeight);\n }\n }\n\n /**\n * Enable one-finger touch to drag\n *\n * NOTE: if you expect users to use right-click dragging, you should enable `viewport.options.disableOnContextMenu`\n * to avoid the context menu popping up on each right-click drag.\n *\n * @param {DragOptions} [options]\n * @returns {Viewport} this\n */\n public drag(options: any): this\n {\n this.plugins.add('drag', new Drag(this, options));\n\n return this;\n }\n\n /**\n * Clamp to world boundaries or other provided boundaries\n *\n * NOTES:\n * clamp is disabled if called with no options; use { direction: 'all' } for all edge clamping\n * screenWidth, screenHeight, worldWidth, and worldHeight needs to be set for this to work properly\n *\n * @param {ClampOptions} [options]\n * @returns {Viewport} this\n */\n public clamp(options: any): this\n {\n this.plugins.add('clamp', new Clamp(this, options));\n\n return this;\n }\n\n /**\n * Decelerate after a move\n *\n * NOTE: this fires 'moved' event during deceleration\n *\n * @param {DecelerateOptions} [options]\n * @return {Viewport} this\n */\n public decelerate(options: any): this\n {\n this.plugins.add('decelerate', new Decelerate(this, options));\n\n return this;\n }\n\n /**\n * Bounce on borders\n * NOTES:\n * screenWidth, screenHeight, worldWidth, and worldHeight needs to be set for this to work properly\n * fires 'moved', 'bounce-x-start', 'bounce-y-start', 'bounce-x-end', and 'bounce-y-end' events\n * @param {object} [options]\n * @param {string} [options.sides=all] all, horizontal, vertical, or combination of top, bottom, right, left (e.g., 'top-bottom-right')\n * @param {number} [options.friction=0.5] friction to apply to decelerate if active\n * @param {number} [options.time=150] time in ms to finish bounce\n * @param {object} [options.bounceBox] use this bounceBox instead of (0, 0, viewport.worldWidth, viewport.worldHeight)\n * @param {number} [options.bounceBox.x=0]\n * @param {number} [options.bounceBox.y=0]\n * @param {number} [options.bounceBox.width=viewport.worldWidth]\n * @param {number} [options.bounceBox.height=viewport.worldHeight]\n * @param {string|function} [options.ease=easeInOutSine] ease function or name (see http://easings.net/ for supported names)\n * @param {string} [options.underflow=center] (top/bottom/center and left/right/center, or center) where to place world if too small for screen\n * @return {Viewport} this\n */\n public bounce(options: any): this\n {\n this.plugins.add('bounce', new Bounce(this, options));\n\n return this;\n }\n\n /**\n * Enable pinch to zoom and two-finger touch to drag\n *\n * @param {PinchOptions} [options]\n * @return {Viewport} this\n */\n public pinch(options: any): this\n {\n this.plugins.add('pinch', new Pinch(this, options));\n\n return this;\n }\n\n /**\n * Snap to a point\n *\n * @param {number} x\n * @param {number} y\n * @param {SnapOptions} [options]\n * @return {Viewport} this\n */\n public snap(x: number, y: number, options: any): this\n {\n this.plugins.add('snap', new Snap(this, x, y, options));\n\n return this;\n }\n\n /**\n * Follow a target\n *\n * NOTES:\n * uses the (x, y) as the center to follow; for PIXI.Sprite to work properly, use sprite.anchor.set(0.5)\n * options.acceleration is not perfect as it doesn't know the velocity of the target\n * it adds acceleration to the start of movement and deceleration to the end of movement when the target is stopped\n * fires 'moved' event\n * @param {PIXI.DisplayObject} target to follow\n * @param {FollowOptions} [options]\n * @returns {Viewport} this\n */\n public follow(target: DisplayObject, options: any): this\n {\n this.plugins.add('follow', new Follow(this, target, options));\n\n return this;\n }\n\n /**\n * Zoom using mouse wheel\n *\n * @param {WheelOptions} [options]\n * @return {Viewport} this\n */\n public wheel(options: any): this\n {\n this.plugins.add('wheel', new Wheel(this, options));\n\n return this;\n }\n\n /**\n * Animate the position and/or scale of the viewport\n *\n * @param {AnimateOptions} options\n * @returns {Viewport} this\n */\n public animate(options: any): this\n {\n this.plugins.add('animate', new Animate(this, options));\n\n return this;\n }\n\n /**\n * Enable clamping of zoom to constraints\n *\n * The minWidth/Height settings are how small the world can get (as it would appear on the screen)\n * before clamping. The maxWidth/maxHeight is how larger the world can scale (as it would appear on\n * the screen) before clamping.\n *\n * For example, if you have a world size of 1000 x 1000 and a screen size of 100 x 100, if you set\n * minWidth/Height = 100 then the world will not be able to zoom smaller than the screen size (ie,\n * zooming out so it appears smaller than the screen). Similarly, if you set maxWidth/Height = 100\n * the world will not be able to zoom larger than the screen size (ie, zooming in so it appears\n * larger than the screen).\n *\n * @param {ClampZoomOptions} [options]\n * @return {Viewport} this\n */\n public clampZoom(options: any): this\n {\n this.plugins.add('clamp-zoom', new ClampZoom(this, options));\n\n return this;\n }\n\n /**\n * Scroll viewport when mouse hovers near one of the edges or radius-distance from center of screen.\n *\n * NOTE: fires 'moved' event\n *\n * @param {MouseEdgesOptions} [options]\n */\n public mouseEdges(options: any): this\n {\n this.plugins.add('mouse-edges', new MouseEdges(this, options));\n\n return this;\n }\n\n /** Pause viewport (including animation updates such as decelerate) */\n get pause(): boolean\n {\n return !!this._pause;\n }\n set pause(value: boolean)\n {\n this._pause = value;\n\n this.lastViewport = null;\n this.moving = false;\n this.zooming = false;\n\n if (value)\n {\n this.input.pause();\n }\n }\n\n /**\n * Move the viewport so the bounding box is visible\n *\n * @param x - left\n * @param y - top\n * @param width\n * @param height\n * @param resizeToFit - Resize the viewport so the box fits within the viewport\n */\n public ensureVisible(x: number, y: number, width: number, height: number, resizeToFit?: boolean)\n {\n if (resizeToFit && (width > this.worldScreenWidth || height > this.worldScreenHeight))\n {\n this.fit(true, width, height);\n this.emit('zoomed', { viewport: this, type: 'ensureVisible' });\n }\n let moved = false;\n\n if (x < this.left)\n {\n this.left = x;\n moved = true;\n }\n else if (x + width > this.right)\n {\n this.right = x + width;\n moved = true;\n }\n if (y < this.top)\n {\n this.top = y;\n moved = true;\n }\n else if (y + height > this.bottom)\n {\n this.bottom = y + height;\n moved = true;\n }\n if (moved)\n {\n this.emit('moved', { viewport: this, type: 'ensureVisible' });\n }\n }\n}\n\n/**\n * Fires after a mouse or touch click\n * @event Viewport#clicked\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a drag starts\n * @event Viewport#drag-start\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a drag ends\n * @event Viewport#drag-end\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a pinch starts\n * @event Viewport#pinch-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a pinch end\n * @event Viewport#pinch-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap starts\n * @event Viewport#snap-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap ends\n * @event Viewport#snap-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap-zoom starts\n * @event Viewport#snap-zoom-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap-zoom ends\n * @event Viewport#snap-zoom-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce starts in the x direction\n * @event Viewport#bounce-x-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce ends in the x direction\n * @event Viewport#bounce-x-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce starts in the y direction\n * @event Viewport#bounce-y-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce ends in the y direction\n * @event Viewport#bounce-y-end\n * @type {Viewport}\n */\n\n/**\n * Fires when for a mouse wheel event\n * @event Viewport#wheel\n * @type {object}\n * @property {object} wheel\n * @property {number} wheel.dx\n * @property {number} wheel.dy\n * @property {number} wheel.dz\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a wheel-scroll occurs\n * @event Viewport#wheel-scroll\n * @type {Viewport}\n */\n\n/**\n * Fires when a mouse-edge starts to scroll\n * @event Viewport#mouse-edge-start\n * @type {Viewport}\n */\n\n/**\n * Fires when the mouse-edge scrolling ends\n * @event Viewport#mouse-edge-end\n * @type {Viewport}\n */\n\n/**\n * Fires when viewport moves through UI interaction, deceleration, ensureVisible, or follow\n * @event Viewport#moved\n * @type {object}\n * @property {Viewport} viewport\n * @property {string} type - (drag, snap, pinch, follow, bounce-x, bounce-y,\n * clamp-x, clamp-y, decelerate, mouse-edges, wheel, ensureVisible)\n */\n\n/**\n * Fires when viewport moves through UI interaction, deceleration, ensureVisible, or follow\n * @event Viewport#zoomed\n * @type {object}\n * @property {Viewport} viewport\n * @property {string} type (drag-zoom, pinch, wheel, clamp-zoom, ensureVisible)\n */\n\n/**\n * Fires when viewport stops moving\n * @event Viewport#moved-end\n * @type {Viewport}\n */\n\n/**\n * Fires when viewport stops zooming\n * @event Viewport#zoomed-end\n * @type {Viewport}\n */\n\n/**\n* Fires at the end of an update frame\n* @event Viewport#frame-end\n* @type {Viewport}\n*/\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;IAGA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;;IC1FA;AAGA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;ICpBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;ygBC1RA;AACA;AACA;AACA;AACA;AACA;IACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;AACA;AACA;AACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;;IC3XA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;AACA;IACA;IACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;IC1RA;IACA;IACA;IACA;IACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;;IC7IA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;ICrRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;IC1eA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;ICvJA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;ICxRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;;IC/JA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;ICxNA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;;ICrRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;;IC3NA;IACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;;ACrSA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;;ICpRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;IAEA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;AACA;AACA;AACA;AACA;AACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;AACA;IACA;IACA;IACA;IACA;IACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;"} \ No newline at end of file diff --git a/dist/viewport.min.min.js b/dist/viewport.min.min.js index 35ecc036..f14c6218 100644 --- a/dist/viewport.min.min.js +++ b/dist/viewport.min.min.js @@ -1,3 +1,3 @@ /* eslint-disable */ -this.PIXI=this.PIXI||{},function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports,require("@pixi/math"),require("penner"),require("@pixi/display"),require("@pixi/ticker")):"function"==typeof define&&define.amd?define(["exports","@pixi/math","penner","@pixi/display","@pixi/ticker"],i):i((t="undefined"!=typeof globalThis?globalThis:t||self).pixi_viewport={},t.PIXI,t.Penner,t.PIXI,t.PIXI)}(this,(function(t,i,e,s,h){"use strict";function n(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var o=n(e);class r{constructor(t){this.parent=t,this.paused=!1}destroy(){}down(t){return!1}move(t){return!1}up(t){return!1}wheel(t){return!1}update(t){}resize(){}reset(){}pause(){this.paused=!0}resume(){this.paused=!1}}function a(t,i){return t?"function"==typeof t?t:"string"==typeof t?o.default[t]:void 0:o.default[i]}const p={removeOnInterrupt:!1,ease:"linear",time:1e3};class l extends r{__init(){this.startWidth=null}__init2(){this.startHeight=null}__init3(){this.deltaWidth=null}__init4(){this.deltaHeight=null}__init5(){this.width=null}__init6(){this.height=null}__init7(){this.time=0}constructor(t,i={}){super(t),l.prototype.__init.call(this),l.prototype.__init2.call(this),l.prototype.__init3.call(this),l.prototype.__init4.call(this),l.prototype.__init5.call(this),l.prototype.__init6.call(this),l.prototype.__init7.call(this),this.options=Object.assign({},p,i),this.options.ease=a(this.options.ease),this.setupPosition(),this.setupZoom(),this.time=0}setupPosition(){void 0!==this.options.position?(this.startX=this.parent.center.x,this.startY=this.parent.center.y,this.deltaX=this.options.position.x-this.parent.center.x,this.deltaY=this.options.position.y-this.parent.center.y,this.keepCenter=!1):this.keepCenter=!0}setupZoom(){this.width=null,this.height=null,void 0!==this.options.scale?this.width=this.parent.screenWidth/this.options.scale:void 0!==this.options.scaleX||void 0!==this.options.scaleY?(void 0!==this.options.scaleX&&(this.width=this.parent.screenWidth/this.options.scaleX),void 0!==this.options.scaleY&&(this.height=this.parent.screenHeight/this.options.scaleY)):(void 0!==this.options.width&&(this.width=this.options.width),void 0!==this.options.height&&(this.height=this.options.height)),null!==this.width&&(this.startWidth=this.parent.screenWidthInWorldPixels,this.deltaWidth=this.width-this.startWidth),null!==this.height&&(this.startHeight=this.parent.screenHeightInWorldPixels,this.deltaHeight=this.height-this.startHeight)}down(){return this.options.removeOnInterrupt&&this.parent.plugins.remove("animate"),!1}complete(){this.parent.plugins.remove("animate"),null!==this.width&&this.parent.fitWidth(this.width,this.keepCenter,null===this.height),null!==this.height&&this.parent.fitHeight(this.height,this.keepCenter,null===this.width),this.keepCenter||this.parent.moveCenter(this.options.position),this.parent.emit("animate-end",this.parent),this.options.callbackOnComplete&&this.options.callbackOnComplete(this.parent)}update(t){if(!this.paused)if(this.time+=t,this.time>=this.options.time)this.complete();else{const t=new i.Point(this.parent.scale.x,this.parent.scale.y),e=this.options.ease(this.time,0,1,this.options.time);if(null!==this.width){const t=this.startWidth,i=this.deltaWidth;this.parent.fitWidth(t+i*e,this.keepCenter,null===this.height)}if(null!==this.height){const t=this.startHeight,i=this.deltaHeight;this.parent.fitHeight(t+i*e,this.keepCenter,null===this.width)}if(null===this.width?this.parent.scale.x=this.parent.scale.y:null===this.height&&(this.parent.scale.y=this.parent.scale.x),!this.keepCenter){const t=this.startX,s=this.startY,h=this.deltaX,n=this.deltaY,o=new i.Point(this.parent.x,this.parent.y);this.parent.moveCenter(t+h*e,s+n*e),this.parent.emit("moved",{viewport:this.parent,original:o,type:"animate"})}(this.width||this.height)&&this.parent.emit("zoomed",{viewport:this.parent,original:t,type:"animate"})}}}function c(t){let i=void 0,e=t[0],s=1;for(;se.call(i,...t))),i=void 0)}return e}const d={sides:"all",friction:.5,time:150,ease:"easeInOutSine",underflow:"center",bounceBox:null};class u extends r{constructor(t,i={}){super(t),this.options=Object.assign({},d,i),this.ease=a(this.options.ease,"easeInOutSine"),this.options.sides?"all"===this.options.sides?this.top=this.bottom=this.left=this.right=!0:"horizontal"===this.options.sides?(this.right=this.left=!0,this.top=this.bottom=!1):"vertical"===this.options.sides?(this.left=this.right=!1,this.top=this.bottom=!0):(this.top=-1!==this.options.sides.indexOf("top"),this.bottom=-1!==this.options.sides.indexOf("bottom"),this.left=-1!==this.options.sides.indexOf("left"),this.right=-1!==this.options.sides.indexOf("right")):this.left=this.top=this.right=this.bottom=!1;const e=this.options.underflow.toLowerCase();"center"===e?(this.underflowX=0,this.underflowY=0):(this.underflowX=-1!==e.indexOf("left")?-1:-1!==e.indexOf("right")?1:0,this.underflowY=-1!==e.indexOf("top")?-1:-1!==e.indexOf("bottom")?1:0),this.reset()}isActive(){return null!==this.toX||null!==this.toY}down(){return this.toX=this.toY=null,!1}up(){return this.bounce(),!1}update(t){if(!this.paused){if(this.bounce(),this.toX){const i=this.toX;i.time+=t,this.parent.emit("moved",{viewport:this.parent,type:"bounce-x"}),i.time>=this.options.time?(this.parent.x=i.end,this.toX=null,this.parent.emit("bounce-x-end",this.parent)):this.parent.x=this.ease(i.time,i.start,i.delta,this.options.time)}if(this.toY){const i=this.toY;i.time+=t,this.parent.emit("moved",{viewport:this.parent,type:"bounce-y"}),i.time>=this.options.time?(this.parent.y=i.end,this.toY=null,this.parent.emit("bounce-y-end",this.parent)):this.parent.y=this.ease(i.time,i.start,i.delta,this.options.time)}}}calcUnderflowX(){let t;switch(this.underflowX){case-1:t=0;break;case 1:t=this.parent.screenWidth-this.parent.screenWorldWidth;break;default:t=(this.parent.screenWidth-this.parent.screenWorldWidth)/2}return t}calcUnderflowY(){let t;switch(this.underflowY){case-1:t=0;break;case 1:t=this.parent.screenHeight-this.parent.screenWorldHeight;break;default:t=(this.parent.screenHeight-this.parent.screenWorldHeight)/2}return t}oob(){const t=this.options.bounceBox;if(t){const e=void 0===t.x?0:t.x,s=void 0===t.y?0:t.y,h=void 0===t.width?this.parent.worldWidth:t.width,n=void 0===t.height?this.parent.worldHeight:t.height;return{left:this.parent.lefth,top:this.parent.topn,topLeft:new i.Point(e*this.parent.scale.x,s*this.parent.scale.y),bottomRight:new i.Point(h*this.parent.scale.x-this.parent.screenWidth,n*this.parent.scale.y-this.parent.screenHeight)}}return{left:this.parent.left<0,right:this.parent.right>this.parent.worldWidth,top:this.parent.top<0,bottom:this.parent.bottom>this.parent.worldHeight,topLeft:new i.Point(0,0),bottomRight:new i.Point(this.parent.worldWidth*this.parent.scale.x-this.parent.screenWidth,this.parent.worldHeight*this.parent.scale.y-this.parent.screenHeight)}}bounce(){if(this.paused)return;let t,i=this.parent.plugins.get("decelerate",!0);i&&(i.x||i.y)&&(i.x&&i.percentChangeX===c([i,"access",t=>t.options,"optionalAccess",t=>t.friction])||i.y&&i.percentChangeY===c([i,"access",t=>t.options,"optionalAccess",t=>t.friction]))&&(t=this.oob(),(t.left&&this.left||t.right&&this.right)&&(i.percentChangeX=this.options.friction),(t.top&&this.top||t.bottom&&this.bottom)&&(i.percentChangeY=this.options.friction));const e=this.parent.plugins.get("drag",!0)||{},s=this.parent.plugins.get("pinch",!0)||{};if(i=i||{},!(c([e,"optionalAccess",t=>t.active])||c([s,"optionalAccess",t=>t.active])||this.toX&&this.toY||i.x&&i.y)){t=t||this.oob();const e=t.topLeft,s=t.bottomRight;if(!this.toX&&!i.x){let i=null;t.left&&this.left?i=this.parent.screenWorldWidth(!0===this.options.right?this.parent.worldWidth:this.options.right)&&(this.parent.x=-(!0===this.options.right?this.parent.worldWidth:this.options.right)*this.parent.scale.x+this.parent.screenWidth,i.x=0,e=!0);e&&this.parent.emit("moved",{viewport:this.parent,original:t,type:"clamp-x"})}if(null!==this.options.top||null!==this.options.bottom){let e=!1;if(!this.noUnderflow&&this.parent.screenWorldHeight(!0===this.options.bottom?this.parent.worldHeight:this.options.bottom)&&(this.parent.y=-(!0===this.options.bottom?this.parent.worldHeight:this.options.bottom)*this.parent.scale.y+this.parent.screenHeight,i.y=0,e=!0);e&&this.parent.emit("moved",{viewport:this.parent,original:t,type:"clamp-y"})}this.last.x=this.parent.x,this.last.y=this.parent.y,this.last.scaleX=this.parent.scale.x,this.last.scaleY=this.parent.scale.y}reset(){this.update()}}const w={minWidth:null,minHeight:null,maxWidth:null,maxHeight:null,minScale:null,maxScale:null};class y extends r{constructor(t,i={}){super(t),this.options=Object.assign({},w,i),this.clamp()}resize(){this.clamp()}clamp(){if(!this.paused)if(this.options.minWidth||this.options.minHeight||this.options.maxWidth||this.options.maxHeight){let t=this.parent.worldScreenWidth,i=this.parent.worldScreenHeight;if(null!==this.options.minWidth&&tthis.options.maxWidth){const e=this.parent.scale.x;this.parent.fitWidth(this.options.maxWidth,!1,!1,!0),this.parent.scale.y*=this.parent.scale.x/e,t=this.parent.worldScreenWidth,i=this.parent.worldScreenHeight,this.parent.emit("zoomed",{viewport:this.parent,type:"clamp-zoom"})}if(null!==this.options.minHeight&&ithis.options.maxHeight){const t=this.parent.scale.y;this.parent.fitHeight(this.options.maxHeight,!1,!1,!0),this.parent.scale.x*=this.parent.scale.y/t,this.parent.emit("zoomed",{viewport:this.parent,type:"clamp-zoom"})}}else{let t=this.parent.scale.x;null!==this.options.minScale&&tthis.options.maxScale&&(t=this.options.maxScale),t!==this.parent.scale.x&&(this.parent.scale.set(t),this.parent.emit("zoomed",{viewport:this.parent,type:"clamp-zoom"}))}}reset(){this.clamp()}}const x={friction:.98,bounce:.8,minSpeed:.01},f=16;class v extends r{constructor(t,i={}){super(t),this.options=Object.assign({},x,i),this.saved=[],this.timeSinceRelease=0,this.reset(),this.parent.on("moved",(t=>this.moved(t)))}down(){return this.saved=[],this.x=this.y=null,!1}isActive(){return!(!this.x&&!this.y)}move(){if(this.paused)return!1;const t=this.parent.input.count();return(1===t||t>1&&!this.parent.plugins.get("pinch",!0))&&(this.saved.push({x:this.parent.x,y:this.parent.y,time:performance.now()}),this.saved.length>60&&this.saved.splice(0,30)),!1}moved(t){if(this.saved.length){const i=this.saved[this.saved.length-1];"clamp-x"===t.type?i.x===t.original.x&&(i.x=this.parent.x):"clamp-y"===t.type&&i.y===t.original.y&&(i.y=this.parent.y)}}up(){if(0===this.parent.input.count()&&this.saved.length){const t=performance.now();for(const i of this.saved)if(i.time>=t-100){const e=t-i.time;this.x=(this.parent.x-i.x)/e,this.y=(this.parent.y-i.y)/e,this.percentChangeX=this.percentChangeY=this.options.friction,this.timeSinceRelease=0;break}}return!1}activate(t){void 0!==(t=t||{}).x&&(this.x=t.x,this.percentChangeX=this.options.friction),void 0!==t.y&&(this.y=t.y,this.percentChangeY=this.options.friction)}update(t){if(this.paused)return;const i=this.x||this.y,e=this.timeSinceRelease,s=this.timeSinceRelease+t;if(this.x){const i=this.percentChangeX,h=Math.log(i);this.parent.x+=this.x*f/h*(Math.pow(i,s/f)-Math.pow(i,e/f)),this.x*=Math.pow(this.percentChangeX,t/f)}if(this.y){const i=this.percentChangeY,h=Math.log(i);this.parent.y+=this.y*f/h*(Math.pow(i,s/f)-Math.pow(i,e/f)),this.y*=Math.pow(this.percentChangeY,t/f)}this.timeSinceRelease+=t,Math.abs(this.x||0){t.includes(i.code)&&(this.keyIsPressed=!0)})),window.addEventListener("keyup",(i=>{t.includes(i.code)&&(this.keyIsPressed=!1)}))}mouseButtons(t){this.mouse=t&&"all"!==t?[-1!==t.indexOf("left"),-1!==t.indexOf("middle"),-1!==t.indexOf("right")]:[!0,!0,!0]}parseUnderflow(){const t=this.options.underflow.toLowerCase();"center"===t?(this.underflowX=0,this.underflowY=0):(this.underflowX=-1!==t.indexOf("left")?-1:-1!==t.indexOf("right")?1:0,this.underflowY=-1!==t.indexOf("top")?-1:-1!==t.indexOf("bottom")?1:0)}checkButtons(t){const i="mouse"===t.data.pointerType,e=this.parent.input.count();return!(!(1===e||e>1&&!this.parent.plugins.get("pinch",!0))||i&&!this.mouse[t.data.button])}checkKeyPress(t){return!this.options.keyToPress||this.keyIsPressed||this.options.ignoreKeyToPressOnTouch&&"touch"===t.data.pointerType}down(t){return!(this.paused||!this.options.pressDrag)&&(this.checkButtons(t)&&this.checkKeyPress(t)?(this.last={x:t.data.global.x,y:t.data.global.y},this.current=t.data.pointerId,!0):(this.last=null,!1))}get active(){return this.moved}move(t){if(this.paused||!this.options.pressDrag)return!1;if(this.last&&this.current===t.data.pointerId){const e=t.data.global.x,s=t.data.global.y,h=this.parent.input.count();if(1===h||h>1&&!this.parent.plugins.get("pinch",!0)){const h=e-this.last.x,n=s-this.last.y;if(this.moved||this.xDirection&&this.parent.input.checkThreshold(h)||this.yDirection&&this.parent.input.checkThreshold(n)){const h={x:e,y:s};return this.xDirection&&(this.parent.x+=(h.x-this.last.x)*this.options.factor),this.yDirection&&(this.parent.y+=(h.y-this.last.y)*this.options.factor),this.last=h,this.moved||this.parent.emit("drag-start",{event:t,screen:new i.Point(this.last.x,this.last.y),world:this.parent.toWorld(new i.Point(this.last.x,this.last.y)),viewport:this.parent}),this.moved=!0,this.parent.emit("moved",{viewport:this.parent,type:"drag"}),!0}}else this.moved=!1}return!1}up(t){if(this.paused)return!1;const e=this.parent.input.touches;if(1===e.length){const t=e[0];return t.last&&(this.last={x:t.last.x,y:t.last.y},this.current=t.id),this.moved=!1,!0}if(this.last&&this.moved){const e=new i.Point(this.last.x,this.last.y);return this.parent.emit("drag-end",{event:t,screen:e,world:this.parent.toWorld(e),viewport:this.parent}),this.last=null,this.moved=!1,!0}return!1}wheel(t){if(this.paused)return!1;if(this.options.wheel){if(!this.parent.plugins.get("wheel",!0)){const i=t.deltaMode?this.options.lineHeight:1;return this.xDirection&&(this.parent.x+=t.deltaX*i*this.options.wheelScroll*this.reverse),this.yDirection&&(this.parent.y+=t.deltaY*i*this.options.wheelScroll*this.reverse),this.options.clampWheel&&this.clamp(),this.parent.emit("wheel-scroll",this.parent),this.parent.emit("moved",{viewport:this.parent,type:"wheel"}),this.parent.options.passiveWheel||t.preventDefault(),!0}}return!1}resume(){this.last=null,this.paused=!1}clamp(){const t=this.parent.plugins.get("decelerate",!0)||{};if("y"!==this.options.clampWheel)if(this.parent.screenWorldWidththis.parent.worldWidth&&(this.parent.x=-this.parent.worldWidth*this.parent.scale.x+this.parent.screenWidth,t.x=0);if("x"!==this.options.clampWheel)if(this.parent.screenWorldHeightthis.parent.worldHeight&&(this.parent.y=-this.parent.worldHeight*this.parent.scale.y+this.parent.screenHeight,t.y=0)}}const H={speed:0,acceleration:null,radius:null};class S extends r{constructor(t,i,e={}){super(t),this.target=i,this.options=Object.assign({},H,e),this.velocity={x:0,y:0}}update(t){if(this.paused)return;const i=this.parent.center;let e=this.target.x,s=this.target.y;if(this.options.radius){if(!(Math.sqrt(Math.pow(this.target.y-i.y,2)+Math.pow(this.target.x-i.x,2))>this.options.radius))return;{const t=Math.atan2(this.target.y-i.y,this.target.x-i.x);e=this.target.x-Math.cos(t)*this.options.radius,s=this.target.y-Math.sin(t)*this.options.radius}}const h=e-i.x,n=s-i.y;if(h||n)if(this.options.speed)if(this.options.acceleration){const o=Math.atan2(s-i.y,e-i.x),r=Math.sqrt(Math.pow(h,2)+Math.pow(n,2));if(r){const a=(Math.pow(this.velocity.x,2)+Math.pow(this.velocity.y,2))/(2*this.options.acceleration);this.velocity=r>a?{x:Math.min(this.velocity.x+this.options.acceleration*t,this.options.speed),y:Math.min(this.velocity.y+this.options.acceleration*t,this.options.speed)}:{x:Math.max(this.velocity.x-this.options.acceleration*this.options.speed,0),y:Math.max(this.velocity.y-this.options.acceleration*this.options.speed,0)};const p=Math.cos(o)*this.velocity.x,l=Math.sin(o)*this.velocity.y,c=Math.abs(p)>Math.abs(h)?e:i.x+p,d=Math.abs(l)>Math.abs(n)?s:i.y+l;this.parent.moveCenter(c,d),this.parent.emit("moved",{viewport:this.parent,type:"follow"})}}else{const t=Math.atan2(s-i.y,e-i.x),o=Math.cos(t)*this.options.speed,r=Math.sin(t)*this.options.speed,a=Math.abs(o)>Math.abs(h)?e:i.x+o,p=Math.abs(r)>Math.abs(n)?s:i.y+r;this.parent.moveCenter(a,p),this.parent.emit("moved",{viewport:this.parent,type:"follow"})}else this.parent.moveCenter(e,s),this.parent.emit("moved",{viewport:this.parent,type:"follow"})}}const M={radius:null,distance:null,top:null,bottom:null,left:null,right:null,speed:8,reverse:!1,noDecelerate:!1,linear:!1,allowButtons:!1};class C extends r{constructor(t,i={}){super(t),this.options=Object.assign({},M,i),this.reverse=this.options.reverse?1:-1,this.radiusSquared="number"==typeof this.options.radius?Math.pow(this.options.radius,2):null,this.resize()}resize(){const t=this.options.distance;null!==t?(this.left=t,this.top=t,this.right=this.parent.worldScreenWidth-t,this.bottom=this.parent.worldScreenHeight-t):this.options.radius||(this.left=this.options.left,this.top=this.options.top,this.right=null===this.options.right?null:this.parent.worldScreenWidth-this.options.right,this.bottom=null===this.options.bottom?null:this.parent.worldScreenHeight-this.options.bottom)}down(){return this.paused||this.options.allowButtons||(this.horizontal=this.vertical=null),!1}move(t){if(this.paused)return!1;if("mouse"!==t.data.pointerType&&1!==t.data.identifier||!this.options.allowButtons&&0!==t.data.buttons)return!1;const i=t.data.global.x,e=t.data.global.y;if(this.radiusSquared){const t=this.parent.toScreen(this.parent.center);if(Math.pow(t.x-i,2)+Math.pow(t.y-e,2)>=this.radiusSquared){const s=Math.atan2(t.y-e,t.x-i);this.options.linear?(this.horizontal=Math.round(Math.cos(s))*this.options.speed*this.reverse*.06,this.vertical=Math.round(Math.sin(s))*this.options.speed*this.reverse*.06):(this.horizontal=Math.cos(s)*this.options.speed*this.reverse*.06,this.vertical=Math.sin(s)*this.options.speed*this.reverse*.06)}else this.horizontal&&this.decelerateHorizontal(),this.vertical&&this.decelerateVertical(),this.horizontal=this.vertical=0}else null!==this.left&&ithis.right?this.horizontal=-1*this.reverse*this.options.speed*.06:(this.decelerateHorizontal(),this.horizontal=0),null!==this.top&&ethis.bottom?this.vertical=-1*this.reverse*this.options.speed*.06:(this.decelerateVertical(),this.vertical=0);return!1}decelerateHorizontal(){const t=this.parent.plugins.get("decelerate",!0);this.horizontal&&t&&!this.options.noDecelerate&&t.activate({x:this.horizontal*this.options.speed*this.reverse/(1e3/60)})}decelerateVertical(){const t=this.parent.plugins.get("decelerate",!0);this.vertical&&t&&!this.options.noDecelerate&&t.activate({y:this.vertical*this.options.speed*this.reverse/(1e3/60)})}up(){return this.paused||(this.horizontal&&this.decelerateHorizontal(),this.vertical&&this.decelerateVertical(),this.horizontal=this.vertical=null),!1}update(){if(!this.paused&&(this.horizontal||this.vertical)){const t=this.parent.center;this.horizontal&&(t.x+=this.horizontal*this.options.speed),this.vertical&&(t.y+=this.vertical*this.options.speed),this.parent.moveCenter(t),this.parent.emit("moved",{viewport:this.parent,type:"mouse-edges"})}}}const z={noDrag:!1,percent:1,center:null,factor:1,axis:"all"};class _ extends r{__init(){this.active=!1}__init2(){this.pinching=!1}__init3(){this.moved=!1}constructor(t,i={}){super(t),_.prototype.__init.call(this),_.prototype.__init2.call(this),_.prototype.__init3.call(this),this.options=Object.assign({},z,i)}down(){return this.parent.input.count()>=2&&(this.active=!0,!0)}isAxisX(){return["all","x"].includes(this.options.axis)}isAxisY(){return["all","y"].includes(this.options.axis)}move(t){if(this.paused||!this.active)return!1;const i=t.data.global.x,e=t.data.global.y,s=this.parent.input.touches;if(s.length>=2){const h=s[0],n=s[1],o=h.last&&n.last?Math.sqrt(Math.pow(n.last.x-h.last.x,2)+Math.pow(n.last.y-h.last.y,2)):null;if(h.id===t.data.pointerId?h.last={x:i,y:e,data:t.data}:n.id===t.data.pointerId&&(n.last={x:i,y:e,data:t.data}),o){let t;const i={x:h.last.x+(n.last.x-h.last.x)/2,y:h.last.y+(n.last.y-h.last.y)/2};this.options.center||(t=this.parent.toLocal(i));let e=Math.sqrt(Math.pow(n.last.x-h.last.x,2)+Math.pow(n.last.y-h.last.y,2));e=0===e?e=1e-10:e;const s=(1-o/e)*this.options.percent*(this.isAxisX()?this.parent.scale.x:this.parent.scale.y);this.isAxisX()&&(this.parent.scale.x+=s),this.isAxisY()&&(this.parent.scale.y+=s),this.parent.emit("zoomed",{viewport:this.parent,type:"pinch",center:i});const r=this.parent.plugins.get("clamp-zoom",!0);if(r&&r.clamp(),this.options.center)this.parent.moveCenter(this.options.center);else{const e=this.parent.toGlobal(t);this.parent.x+=(i.x-e.x)*this.options.factor,this.parent.y+=(i.y-e.y)*this.options.factor,this.parent.emit("moved",{viewport:this.parent,type:"pinch"})}!this.options.noDrag&&this.lastCenter&&(this.parent.x+=(i.x-this.lastCenter.x)*this.options.factor,this.parent.y+=(i.y-this.lastCenter.y)*this.options.factor,this.parent.emit("moved",{viewport:this.parent,type:"pinch"})),this.lastCenter=i,this.moved=!0}else this.pinching||(this.parent.emit("pinch-start",this.parent),this.pinching=!0);return!0}return!1}up(){return!!(this.pinching&&this.parent.input.touches.length<=1)&&(this.active=!1,this.lastCenter=null,this.pinching=!1,this.moved=!1,this.parent.emit("pinch-end",this.parent),!0)}}const X={topLeft:!1,friction:.8,time:1e3,ease:"easeInOutSine",interrupt:!0,removeOnComplete:!1,removeOnInterrupt:!1,forceStart:!1};class k extends r{constructor(t,i,e,s={}){super(t),this.options=Object.assign({},X,s),this.ease=a(s.ease,"easeInOutSine"),this.x=i,this.y=e,this.options.forceStart&&this.snapStart()}snapStart(){this.percent=0,this.snapping={time:0};const t=this.options.topLeft?this.parent.corner:this.parent.center;this.deltaX=this.x-t.x,this.deltaY=this.y-t.y,this.startX=t.x,this.startY=t.y,this.parent.emit("snap-start",this.parent)}wheel(){return this.options.removeOnInterrupt&&this.parent.plugins.remove("snap"),!1}down(){return this.options.removeOnInterrupt?this.parent.plugins.remove("snap"):this.options.interrupt&&(this.snapping=null),!1}up(){if(0===this.parent.input.count()){const t=this.parent.plugins.get("decelerate",!0);t&&(t.x||t.y)&&(t.percentChangeX=t.percentChangeY=this.options.friction)}return!1}update(t){if(!(this.paused||this.options.interrupt&&0!==this.parent.input.count()))if(this.snapping){const i=this.snapping;let e,s,h;i.time+=t;const n=this.startX,o=this.startY,r=this.deltaX,a=this.deltaY;if(i.time>this.options.time)e=!0,s=n+r,h=o+a;else{const t=this.ease(i.time,0,1,this.options.time);s=n+r*t,h=o+a*t}this.options.topLeft?this.parent.moveCorner(s,h):this.parent.moveCenter(s,h),this.parent.emit("moved",{viewport:this.parent,type:"snap"}),e&&(this.options.removeOnComplete&&this.parent.plugins.remove("snap"),this.parent.emit("snap-end",this.parent),this.snapping=null)}else{const t=this.options.topLeft?this.parent.corner:this.parent.center;t.x===this.x&&t.y===this.y||this.snapStart()}}}const P={width:0,height:0,time:1e3,ease:"easeInOutSine",center:null,interrupt:!0,removeOnComplete:!1,removeOnInterrupt:!1,forceStart:!1,noMove:!1};class O extends r{constructor(t,i={}){super(t),this.options=Object.assign({},P,i),this.ease=a(this.options.ease),this.xIndependent=!1,this.yIndependent=!1,this.xScale=0,this.yScale=0,this.options.width>0&&(this.xScale=t.screenWidth/this.options.width,this.xIndependent=!0),this.options.height>0&&(this.yScale=t.screenHeight/this.options.height,this.yIndependent=!0),this.xScale=this.xIndependent?this.xScale:this.yScale,this.yScale=this.yIndependent?this.yScale:this.xScale,0===this.options.time?(t.container.scale.x=this.xScale,t.container.scale.y=this.yScale,this.options.removeOnComplete&&this.parent.plugins.remove("snap-zoom")):i.forceStart&&this.createSnapping()}createSnapping(){const t=this.parent.worldScreenWidth,i=this.parent.worldScreenHeight,e=this.parent.screenWidth/this.xScale,s=this.parent.screenHeight/this.yScale;this.snapping={time:0,startX:t,startY:i,deltaX:e-t,deltaY:s-i},this.parent.emit("snap-zoom-start",this.parent)}resize(){this.snapping=null,this.options.width>0&&(this.xScale=this.parent.screenWidth/this.options.width),this.options.height>0&&(this.yScale=this.parent.screenHeight/this.options.height),this.xScale=this.xIndependent?this.xScale:this.yScale,this.yScale=this.yIndependent?this.yScale:this.xScale}wheel(){return this.options.removeOnInterrupt&&this.parent.plugins.remove("snap-zoom"),!1}down(){return this.options.removeOnInterrupt?this.parent.plugins.remove("snap-zoom"):this.options.interrupt&&(this.snapping=null),!1}update(t){if(this.paused)return;if(this.options.interrupt&&0!==this.parent.input.count())return;let i;if(this.options.center||this.options.noMove||(i=this.parent.center),this.snapping){if(this.snapping){const e=this.snapping;if(e.time+=t,e.time>=this.options.time)this.parent.scale.set(this.xScale,this.yScale),this.options.removeOnComplete&&this.parent.plugins.remove("snap-zoom"),this.parent.emit("snap-zoom-end",this.parent),this.snapping=null;else{const t=this.snapping,i=this.ease(t.time,t.startX,t.deltaX,this.options.time),e=this.ease(t.time,t.startY,t.deltaY,this.options.time);this.parent.scale.x=this.parent.screenWidth/i,this.parent.scale.y=this.parent.screenHeight/e}const s=this.parent.plugins.get("clamp-zoom",!0);s&&s.clamp(),this.options.noMove||(this.options.center?this.parent.moveCenter(this.options.center):this.parent.moveCenter(i))}}else this.parent.scale.x===this.xScale&&this.parent.scale.y===this.yScale||this.createSnapping()}resume(){this.snapping=null,super.resume()}}const Y={percent:.1,smooth:!1,interrupt:!0,reverse:!1,center:null,lineHeight:20,axis:"all"};class A extends r{constructor(t,i={}){super(t),this.options=Object.assign({},Y,i)}down(){return this.options.interrupt&&(this.smoothing=null),!1}isAxisX(){return["all","x"].includes(this.options.axis)}isAxisY(){return["all","y"].includes(this.options.axis)}update(){if(this.smoothing){const t=this.smoothingCenter,i=this.smoothing;let e;this.options.center||(e=this.parent.toLocal(t)),this.isAxisX()&&(this.parent.scale.x+=i.x),this.isAxisY()&&(this.parent.scale.y+=i.y),this.parent.emit("zoomed",{viewport:this.parent,type:"wheel"});const s=this.parent.plugins.get("clamp-zoom",!0);if(s&&s.clamp(),this.options.center)this.parent.moveCenter(this.options.center);else{const i=this.parent.toGlobal(e);this.parent.x+=t.x-i.x,this.parent.y+=t.y-i.y}this.parent.emit("moved",{viewport:this.parent,type:"wheel"}),this.smoothingCount++,this.smoothingCount>=this.options.smooth&&(this.smoothing=null)}}wheel(t){if(this.paused)return;const i=this.parent.input.getPointerPosition(t),e=(this.options.reverse?-1:1)*-t.deltaY*(t.deltaMode?this.options.lineHeight:1)/500,s=Math.pow(2,(1+this.options.percent)*e);if(this.options.smooth){const t={x:this.smoothing?this.smoothing.x*(this.options.smooth-this.smoothingCount):0,y:this.smoothing?this.smoothing.y*(this.options.smooth-this.smoothingCount):0};this.smoothing={x:((this.parent.scale.x+t.x)*s-this.parent.scale.x)/this.options.smooth,y:((this.parent.scale.y+t.y)*s-this.parent.scale.y)/this.options.smooth},this.smoothingCount=0,this.smoothingCenter=i}else{let t;this.options.center||(t=this.parent.toLocal(i)),this.isAxisX()&&(this.parent.scale.x*=s),this.isAxisY()&&(this.parent.scale.y*=s),this.parent.emit("zoomed",{viewport:this.parent,type:"wheel"});const e=this.parent.plugins.get("clamp-zoom",!0);if(e&&e.clamp(),this.options.center)this.parent.moveCenter(this.options.center);else{const e=this.parent.toGlobal(t);this.parent.x+=i.x-e.x,this.parent.y+=i.y-e.y}}return this.parent.emit("moved",{viewport:this.parent,type:"wheel"}),this.parent.emit("wheel",{wheel:{dx:t.deltaX,dy:t.deltaY,dz:t.deltaZ},event:t,viewport:this.parent}),!this.parent.options.passiveWheel||void 0}}class I{constructor(t){this.viewport=t,this.touches=[],this.addListeners()}addListeners(){this.viewport.interactive=!0,this.viewport.forceHitArea||(this.viewport.hitArea=new i.Rectangle(0,0,this.viewport.worldWidth,this.viewport.worldHeight)),this.viewport.on("pointerdown",this.down,this),this.viewport.on("pointermove",this.move,this),this.viewport.on("pointerup",this.up,this),this.viewport.on("pointerupoutside",this.up,this),this.viewport.on("pointercancel",this.up,this),this.viewport.on("pointerout",this.up,this),this.wheelFunction=t=>this.handleWheel(t),this.viewport.options.divWheel.addEventListener("wheel",this.wheelFunction,{passive:this.viewport.options.passiveWheel}),this.isMouseDown=!1}destroy(){this.viewport.options.divWheel.removeEventListener("wheel",this.wheelFunction)}down(t){if(this.viewport.pause||!this.viewport.worldVisible)return;if("mouse"===t.data.pointerType?this.isMouseDown=!0:this.get(t.data.pointerId)||this.touches.push({id:t.data.pointerId,last:null}),1===this.count()){this.last=t.data.global.clone();const i=this.viewport.plugins.get("decelerate",!0),e=this.viewport.plugins.get("bounce",!0);i&&i.isActive()||e&&e.isActive()?this.clickedAvailable=!1:this.clickedAvailable=!0}else this.clickedAvailable=!1;this.viewport.plugins.down(t)&&this.viewport.options.stopPropagation&&t.stopPropagation()}clear(){this.isMouseDown=!1,this.touches=[],this.last=null}checkThreshold(t){return Math.abs(t)>=this.viewport.threshold}move(t){if(this.viewport.pause||!this.viewport.worldVisible)return;const i=this.viewport.plugins.move(t);if(this.clickedAvailable&&this.last){const i=t.data.global.x-this.last.x,e=t.data.global.y-this.last.y;(this.checkThreshold(i)||this.checkThreshold(e))&&(this.clickedAvailable=!1)}i&&this.viewport.options.stopPropagation&&t.stopPropagation()}up(t){if(this.viewport.pause||!this.viewport.worldVisible)return;"mouse"===t.data.pointerType&&(this.isMouseDown=!1),"mouse"!==t.data.pointerType&&this.remove(t.data.pointerId);const i=this.viewport.plugins.up(t);this.clickedAvailable&&0===this.count()&&this.last&&(this.viewport.emit("clicked",{event:t,screen:this.last,world:this.viewport.toWorld(this.last),viewport:this}),this.clickedAvailable=!1),i&&this.viewport.options.stopPropagation&&t.stopPropagation()}getPointerPosition(t){const e=new i.Point;return this.viewport.options.interaction?this.viewport.options.interaction.mapPositionToPoint(e,t.clientX,t.clientY):(e.x=t.clientX,e.y=t.clientY),e}handleWheel(t){if(this.viewport.pause||!this.viewport.worldVisible)return;if(this.viewport.options.interaction&&this.viewport.options.interaction.interactionDOMElement!==t.target)return;const i=this.viewport.toLocal(this.getPointerPosition(t));if(this.viewport.left<=i.x&&i.x<=this.viewport.right&&this.viewport.top<=i.y&&i.y<=this.viewport.bottom){this.viewport.plugins.wheel(t)&&!this.viewport.options.passiveWheel&&t.preventDefault()}}pause(){this.touches=[],this.isMouseDown=!1}get(t){for(const i of this.touches)if(i.id===t)return i;return null}remove(t){for(let i=0;ie.call(i,...t))),i=void 0)}return e}const T=["drag","pinch","wheel","follow","mouse-edges","decelerate","aniamte","bounce","snap-zoom","clamp-zoom","snap","clamp"];class V{constructor(t){this.viewport=t,this.list=[],this.plugins={}}add(t,i,e=T.length){this.plugins[t]=i;const s=T.indexOf(t);-1!==s&&T.splice(s,1),T.splice(e,0,t),this.sort()}get(t,i){return i&&D([this,"access",t=>t.plugins,"access",i=>i[t],"optionalAccess",t=>t.paused])?null:this.plugins[t]}update(t){for(const i of this.list)i.update(t)}resize(){for(const t of this.list)t.resize()}reset(){for(const t of this.list)t.reset()}removeAll(){this.plugins={},this.sort()}remove(t){this.plugins[t]&&(delete this.plugins[t],this.viewport.emit(t+"-remove"),this.sort())}pause(t){D([this,"access",t=>t.plugins,"access",i=>i[t],"optionalAccess",t=>t.pause,"call",t=>t()])}resume(t){D([this,"access",t=>t.plugins,"access",i=>i[t],"optionalAccess",t=>t.resume,"call",t=>t()])}sort(){this.list=[];for(const t of T)this.plugins[t]&&this.list.push(this.plugins[t])}down(t){let i=!1;for(const e of this.list)e.down(t)&&(i=!0);return i}move(t){let i=!1;for(const e of this.viewport.plugins.list)e.move(t)&&(i=!0);return i}up(t){let i=!1;for(const e of this.list)e.up(t)&&(i=!0);return i}wheel(t){let i=!1;for(const e of this.list)e.wheel(t)&&(i=!0);return i}}const L={screenWidth:window.innerWidth,screenHeight:window.innerHeight,worldWidth:null,worldHeight:null,threshold:5,passiveWheel:!0,stopPropagation:!1,forceHitArea:null,noTicker:!1,interaction:null,disableOnContextMenu:!1,ticker:h.Ticker.shared};class j extends s.Container{constructor(t={}){super(),this.options=Object.assign({},{divWheel:document.body},L,t),this.screenWidth=this.options.screenWidth,this.screenHeight=this.options.screenHeight,this._worldWidth=this.options.worldWidth,this._worldHeight=this.options.worldHeight,this.forceHitArea=this.options.forceHitArea,this.threshold=this.options.threshold,this.options.divWheel=this.options.divWheel||document.body,this.options.disableOnContextMenu&&(this.options.divWheel.oncontextmenu=t=>t.preventDefault()),this.options.noTicker||(this.tickerFunction=()=>this.update(this.options.ticker.elapsedMS),this.options.ticker.add(this.tickerFunction)),this.input=new I(this),this.plugins=new V(this)}destroy(t){!this.options.noTicker&&this.tickerFunction&&this.options.ticker.remove(this.tickerFunction),this.input.destroy(),super.destroy(t)}update(t){this.pause||(this.plugins.update(t),this.lastViewport&&(this.lastViewport.x!==this.x||this.lastViewport.y!==this.y?this.moving=!0:this.moving&&(this.emit("moved-end",this),this.moving=!1),this.lastViewport.scaleX!==this.scale.x||this.lastViewport.scaleY!==this.scale.y?this.zooming=!0:this.zooming&&(this.emit("zoomed-end",this),this.zooming=!1)),this.forceHitArea||(this._hitAreaDefault=new i.Rectangle(this.left,this.top,this.worldScreenWidth,this.worldScreenHeight),this.hitArea=this._hitAreaDefault),this._dirty=this._dirty||!this.lastViewport||this.lastViewport.x!==this.x||this.lastViewport.y!==this.y||this.lastViewport.scaleX!==this.scale.x||this.lastViewport.scaleY!==this.scale.y,this.lastViewport={x:this.x,y:this.y,scaleX:this.scale.x,scaleY:this.scale.y},this.emit("frame-end",this))}resize(t=window.innerWidth,i=window.innerHeight,e,s){this.screenWidth=t,this.screenHeight=i,void 0!==e&&(this._worldWidth=e),void 0!==s&&(this._worldHeight=s),this.plugins.resize(),this.dirty=!0}get worldWidth(){return this._worldWidth?this._worldWidth:this.width/this.scale.x}set worldWidth(t){this._worldWidth=t,this.plugins.resize()}get worldHeight(){return this._worldHeight?this._worldHeight:this.height/this.scale.y}set worldHeight(t){this._worldHeight=t,this.plugins.resize()}getVisibleBounds(){return new i.Rectangle(this.left,this.top,this.worldScreenWidth,this.worldScreenHeight)}toWorld(t,e){return 2===arguments.length?this.toLocal(new i.Point(t,e)):this.toLocal(t)}toScreen(t,e){return 2===arguments.length?this.toGlobal(new i.Point(t,e)):this.toGlobal(t)}get worldScreenWidth(){return this.screenWidth/this.scale.x}get worldScreenHeight(){return this.screenHeight/this.scale.y}get screenWorldWidth(){return this.worldWidth*this.scale.x}get screenWorldHeight(){return this.worldHeight*this.scale.y}get center(){return new i.Point(this.worldScreenWidth/2-this.x/this.scale.x,this.worldScreenHeight/2-this.y/this.scale.y)}set center(t){this.moveCenter(t)}moveCenter(...t){let i,e;"number"==typeof t[0]?(i=t[0],e=t[1]):(i=t[0].x,e=t[0].y);const s=(this.worldScreenWidth/2-i)*this.scale.x,h=(this.worldScreenHeight/2-e)*this.scale.y;return this.x===s&&this.y===h||(this.position.set(s,h),this.plugins.reset(),this.dirty=!0),this}get corner(){return new i.Point(-this.x/this.scale.x,-this.y/this.scale.y)}set corner(t){this.moveCorner(t)}moveCorner(...t){let i,e;return 1===t.length?(i=-t[0].x*this.scale.x,e=-t[0].y*this.scale.y):(i=-t[0]*this.scale.x,e=-t[1]*this.scale.y),i===this.x&&e===this.y||(this.position.set(i,e),this.plugins.reset(),this.dirty=!0),this}get screenWidthInWorldPixels(){return this.screenWidth/this.scale.x}get screenHeightInWorldPixels(){return this.screenHeight/this.scale.y}findFitWidth(t){return this.screenWidth/t}findFitHeight(t){return this.screenHeight/t}findFit(t,i){const e=this.screenWidth/t,s=this.screenHeight/i;return Math.min(e,s)}findCover(t,i){const e=this.screenWidth/t,s=this.screenHeight/i;return Math.max(e,s)}fitWidth(t=this.worldWidth,i,e=!0,s){let h;i&&(h=this.center),this.scale.x=this.screenWidth/t,e&&(this.scale.y=this.scale.x);const n=this.plugins.get("clamp-zoom",!0);return!s&&n&&n.clamp(),i&&h&&this.moveCenter(h),this}fitHeight(t=this.worldHeight,i,e=!0,s){let h;i&&(h=this.center),this.scale.y=this.screenHeight/t,e&&(this.scale.x=this.scale.y);const n=this.plugins.get("clamp-zoom",!0);return!s&&n&&n.clamp(),i&&h&&this.moveCenter(h),this}fitWorld(t){let i;t&&(i=this.center),this.scale.x=this.screenWidth/this.worldWidth,this.scale.y=this.screenHeight/this.worldHeight,this.scale.xthis.worldWidth,top:this.top<0,bottom:this.bottom>this.worldHeight,cornerPoint:new i.Point(this.worldWidth*this.scale.x-this.screenWidth,this.worldHeight*this.scale.y-this.screenHeight)}}get right(){return-this.x/this.scale.x+this.worldScreenWidth}set right(t){this.x=-t*this.scale.x+this.screenWidth,this.plugins.reset()}get left(){return-this.x/this.scale.x}set left(t){this.x=-t*this.scale.x,this.plugins.reset()}get top(){return-this.y/this.scale.y}set top(t){this.y=-t*this.scale.y,this.plugins.reset()}get bottom(){return-this.y/this.scale.y+this.worldScreenHeight}set bottom(t){this.y=-t*this.scale.y+this.screenHeight,this.plugins.reset()}get dirty(){return!!this._dirty}set dirty(t){this._dirty=t}get forceHitArea(){return this._forceHitArea}set forceHitArea(t){t?(this._forceHitArea=t,this.hitArea=t):(this._forceHitArea=null,this.hitArea=new i.Rectangle(0,0,this.worldWidth,this.worldHeight))}drag(t){return this.plugins.add("drag",new b(this,t)),this}clamp(t){return this.plugins.add("clamp",new m(this,t)),this}decelerate(t){return this.plugins.add("decelerate",new v(this,t)),this}bounce(t){return this.plugins.add("bounce",new u(this,t)),this}pinch(t){return this.plugins.add("pinch",new _(this,t)),this}snap(t,i,e){return this.plugins.add("snap",new k(this,t,i,e)),this}follow(t,i){return this.plugins.add("follow",new S(this,t,i)),this}wheel(t){return this.plugins.add("wheel",new A(this,t)),this}animate(t){return this.plugins.add("animate",new l(this,t)),this}clampZoom(t){return this.plugins.add("clamp-zoom",new y(this,t)),this}mouseEdges(t){return this.plugins.add("mouse-edges",new C(this,t)),this}get pause(){return!!this._pause}set pause(t){this._pause=t,this.lastViewport=null,this.moving=!1,this.zooming=!1,t&&this.input.pause()}ensureVisible(t,i,e,s,h){h&&(e>this.worldScreenWidth||s>this.worldScreenHeight)&&(this.fit(!0,e,s),this.emit("zoomed",{viewport:this,type:"ensureVisible"}));let n=!1;tthis.right&&(this.right=t+e,n=!0),ithis.bottom&&(this.bottom=i+s,n=!0),n&&this.emit("moved",{viewport:this,type:"ensureVisible"})}}t.Animate=l,t.Bounce=u,t.Clamp=m,t.ClampZoom=y,t.Decelerate=v,t.Drag=b,t.Follow=S,t.InputManager=I,t.MouseEdges=C,t.Pinch=_,t.Plugin=r,t.PluginManager=V,t.Snap=k,t.SnapZoom=O,t.Viewport=j,t.Wheel=A,Object.defineProperty(t,"__esModule",{value:!0})})),"undefined"!=typeof pixi_viewport&&Object.assign(this.PIXI,pixi_viewport); +this.PIXI=this.PIXI||{},function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports,require("@pixi/math"),require("penner"),require("@pixi/display"),require("@pixi/ticker")):"function"==typeof define&&define.amd?define(["exports","@pixi/math","penner","@pixi/display","@pixi/ticker"],i):i((t="undefined"!=typeof globalThis?globalThis:t||self).pixi_viewport={},t.PIXI,t.Penner,t.PIXI,t.PIXI)}(this,(function(t,i,e,s,h){"use strict";function n(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var o=n(e);class r{constructor(t){this.parent=t,this.paused=!1}destroy(){}down(t){return!1}move(t){return!1}up(t){return!1}wheel(t){return!1}update(t){}resize(){}reset(){}pause(){this.paused=!0}resume(){this.paused=!1}}function a(t,i){return t?"function"==typeof t?t:"string"==typeof t?o.default[t]:void 0:o.default[i]}const p={removeOnInterrupt:!1,ease:"linear",time:1e3};class l extends r{__init(){this.startWidth=null}__init2(){this.startHeight=null}__init3(){this.deltaWidth=null}__init4(){this.deltaHeight=null}__init5(){this.width=null}__init6(){this.height=null}__init7(){this.time=0}constructor(t,i={}){super(t),l.prototype.__init.call(this),l.prototype.__init2.call(this),l.prototype.__init3.call(this),l.prototype.__init4.call(this),l.prototype.__init5.call(this),l.prototype.__init6.call(this),l.prototype.__init7.call(this),this.options=Object.assign({},p,i),this.options.ease=a(this.options.ease),this.setupPosition(),this.setupZoom(),this.time=0}setupPosition(){void 0!==this.options.position?(this.startX=this.parent.center.x,this.startY=this.parent.center.y,this.deltaX=this.options.position.x-this.parent.center.x,this.deltaY=this.options.position.y-this.parent.center.y,this.keepCenter=!1):this.keepCenter=!0}setupZoom(){this.width=null,this.height=null,void 0!==this.options.scale?this.width=this.parent.screenWidth/this.options.scale:void 0!==this.options.scaleX||void 0!==this.options.scaleY?(void 0!==this.options.scaleX&&(this.width=this.parent.screenWidth/this.options.scaleX),void 0!==this.options.scaleY&&(this.height=this.parent.screenHeight/this.options.scaleY)):(void 0!==this.options.width&&(this.width=this.options.width),void 0!==this.options.height&&(this.height=this.options.height)),null!==this.width&&(this.startWidth=this.parent.screenWidthInWorldPixels,this.deltaWidth=this.width-this.startWidth),null!==this.height&&(this.startHeight=this.parent.screenHeightInWorldPixels,this.deltaHeight=this.height-this.startHeight)}down(){return this.options.removeOnInterrupt&&this.parent.plugins.remove("animate"),!1}complete(){this.parent.plugins.remove("animate"),null!==this.width&&this.parent.fitWidth(this.width,this.keepCenter,null===this.height),null!==this.height&&this.parent.fitHeight(this.height,this.keepCenter,null===this.width),this.keepCenter||this.parent.moveCenter(this.options.position),this.parent.emit("animate-end",this.parent),this.options.callbackOnComplete&&this.options.callbackOnComplete(this.parent)}update(t){if(!this.paused)if(this.time+=t,this.time>=this.options.time)this.complete();else{const t=new i.Point(this.parent.scale.x,this.parent.scale.y),e=this.options.ease(this.time,0,1,this.options.time);if(null!==this.width){const t=this.startWidth,i=this.deltaWidth;this.parent.fitWidth(t+i*e,this.keepCenter,null===this.height)}if(null!==this.height){const t=this.startHeight,i=this.deltaHeight;this.parent.fitHeight(t+i*e,this.keepCenter,null===this.width)}if(null===this.width?this.parent.scale.x=this.parent.scale.y:null===this.height&&(this.parent.scale.y=this.parent.scale.x),!this.keepCenter){const t=this.startX,s=this.startY,h=this.deltaX,n=this.deltaY,o=new i.Point(this.parent.x,this.parent.y);this.parent.moveCenter(t+h*e,s+n*e),this.parent.emit("moved",{viewport:this.parent,original:o,type:"animate"})}(this.width||this.height)&&this.parent.emit("zoomed",{viewport:this.parent,original:t,type:"animate"})}}}function c(t){let i=void 0,e=t[0],s=1;for(;se.call(i,...t))),i=void 0)}return e}const d={sides:"all",friction:.5,time:150,ease:"easeInOutSine",underflow:"center",bounceBox:null};class u extends r{constructor(t,i={}){super(t),this.options=Object.assign({},d,i),this.ease=a(this.options.ease,"easeInOutSine"),this.options.sides?"all"===this.options.sides?this.top=this.bottom=this.left=this.right=!0:"horizontal"===this.options.sides?(this.right=this.left=!0,this.top=this.bottom=!1):"vertical"===this.options.sides?(this.left=this.right=!1,this.top=this.bottom=!0):(this.top=-1!==this.options.sides.indexOf("top"),this.bottom=-1!==this.options.sides.indexOf("bottom"),this.left=-1!==this.options.sides.indexOf("left"),this.right=-1!==this.options.sides.indexOf("right")):this.left=this.top=this.right=this.bottom=!1;const e=this.options.underflow.toLowerCase();"center"===e?(this.underflowX=0,this.underflowY=0):(this.underflowX=-1!==e.indexOf("left")?-1:-1!==e.indexOf("right")?1:0,this.underflowY=-1!==e.indexOf("top")?-1:-1!==e.indexOf("bottom")?1:0),this.reset()}isActive(){return null!==this.toX||null!==this.toY}down(){return this.toX=this.toY=null,!1}up(){return this.bounce(),!1}update(t){if(!this.paused){if(this.bounce(),this.toX){const i=this.toX;i.time+=t,this.parent.emit("moved",{viewport:this.parent,type:"bounce-x"}),i.time>=this.options.time?(this.parent.x=i.end,this.toX=null,this.parent.emit("bounce-x-end",this.parent)):this.parent.x=this.ease(i.time,i.start,i.delta,this.options.time)}if(this.toY){const i=this.toY;i.time+=t,this.parent.emit("moved",{viewport:this.parent,type:"bounce-y"}),i.time>=this.options.time?(this.parent.y=i.end,this.toY=null,this.parent.emit("bounce-y-end",this.parent)):this.parent.y=this.ease(i.time,i.start,i.delta,this.options.time)}}}calcUnderflowX(){let t;switch(this.underflowX){case-1:t=0;break;case 1:t=this.parent.screenWidth-this.parent.screenWorldWidth;break;default:t=(this.parent.screenWidth-this.parent.screenWorldWidth)/2}return t}calcUnderflowY(){let t;switch(this.underflowY){case-1:t=0;break;case 1:t=this.parent.screenHeight-this.parent.screenWorldHeight;break;default:t=(this.parent.screenHeight-this.parent.screenWorldHeight)/2}return t}oob(){const t=this.options.bounceBox;if(t){const e=void 0===t.x?0:t.x,s=void 0===t.y?0:t.y,h=void 0===t.width?this.parent.worldWidth:t.width,n=void 0===t.height?this.parent.worldHeight:t.height;return{left:this.parent.lefth,top:this.parent.topn,topLeft:new i.Point(e*this.parent.scale.x,s*this.parent.scale.y),bottomRight:new i.Point(h*this.parent.scale.x-this.parent.screenWidth,n*this.parent.scale.y-this.parent.screenHeight)}}return{left:this.parent.left<0,right:this.parent.right>this.parent.worldWidth,top:this.parent.top<0,bottom:this.parent.bottom>this.parent.worldHeight,topLeft:new i.Point(0,0),bottomRight:new i.Point(this.parent.worldWidth*this.parent.scale.x-this.parent.screenWidth,this.parent.worldHeight*this.parent.scale.y-this.parent.screenHeight)}}bounce(){if(this.paused)return;let t,i=this.parent.plugins.get("decelerate",!0);i&&(i.x||i.y)&&(i.x&&i.percentChangeX===c([i,"access",t=>t.options,"optionalAccess",t=>t.friction])||i.y&&i.percentChangeY===c([i,"access",t=>t.options,"optionalAccess",t=>t.friction]))&&(t=this.oob(),(t.left&&this.left||t.right&&this.right)&&(i.percentChangeX=this.options.friction),(t.top&&this.top||t.bottom&&this.bottom)&&(i.percentChangeY=this.options.friction));const e=this.parent.plugins.get("drag",!0)||{},s=this.parent.plugins.get("pinch",!0)||{};if(i=i||{},!(c([e,"optionalAccess",t=>t.active])||c([s,"optionalAccess",t=>t.active])||this.toX&&this.toY||i.x&&i.y)){t=t||this.oob();const e=t.topLeft,s=t.bottomRight;if(!this.toX&&!i.x){let i=null;t.left&&this.left?i=this.parent.screenWorldWidth(!0===this.options.right?this.parent.worldWidth:this.options.right)&&(this.parent.x=-(!0===this.options.right?this.parent.worldWidth:this.options.right)*this.parent.scale.x+this.parent.screenWidth,i.x=0,e=!0);e&&this.parent.emit("moved",{viewport:this.parent,original:t,type:"clamp-x"})}if(null!==this.options.top||null!==this.options.bottom){let e=!1;if(!this.noUnderflow&&this.parent.screenWorldHeight(!0===this.options.bottom?this.parent.worldHeight:this.options.bottom)&&(this.parent.y=-(!0===this.options.bottom?this.parent.worldHeight:this.options.bottom)*this.parent.scale.y+this.parent.screenHeight,i.y=0,e=!0);e&&this.parent.emit("moved",{viewport:this.parent,original:t,type:"clamp-y"})}this.last.x=this.parent.x,this.last.y=this.parent.y,this.last.scaleX=this.parent.scale.x,this.last.scaleY=this.parent.scale.y}reset(){this.update()}}const w={minWidth:null,minHeight:null,maxWidth:null,maxHeight:null,minScale:null,maxScale:null};class y extends r{constructor(t,i={}){super(t),this.options=Object.assign({},w,i),this.clamp()}resize(){this.clamp()}clamp(){if(!this.paused)if(this.options.minWidth||this.options.minHeight||this.options.maxWidth||this.options.maxHeight){let t=this.parent.worldScreenWidth,i=this.parent.worldScreenHeight;if(null!==this.options.minWidth&&tthis.options.maxWidth){const e=this.parent.scale.x;this.parent.fitWidth(this.options.maxWidth,!1,!1,!0),this.parent.scale.y*=this.parent.scale.x/e,t=this.parent.worldScreenWidth,i=this.parent.worldScreenHeight,this.parent.emit("zoomed",{viewport:this.parent,type:"clamp-zoom"})}if(null!==this.options.minHeight&&ithis.options.maxHeight){const t=this.parent.scale.y;this.parent.fitHeight(this.options.maxHeight,!1,!1,!0),this.parent.scale.x*=this.parent.scale.y/t,this.parent.emit("zoomed",{viewport:this.parent,type:"clamp-zoom"})}}else if(this.options.minScale||this.options.maxScale){const t={x:null,y:null},i={x:null,y:null};if("number"==typeof this.options.minScale)t.x=this.options.minScale,t.y=this.options.minScale;else if(null!==this.options.minScale){const i=this.options.minScale;t.x=void 0===i.x?null:i.x,t.y=void 0===i.y?null:i.y}if("number"==typeof this.options.maxScale)i.x=this.options.maxScale,i.y=this.options.maxScale;else if(null!==this.options.maxScale){const t=this.options.maxScale;i.x=void 0===t.x?null:t.x,i.y=void 0===t.y?null:t.y}let e=this.parent.scale.x,s=this.parent.scale.y;null!==t.x&&ei.x&&(e=i.x),null!==t.y&&si.y&&(s=i.y),e===this.parent.scale.x&&s===this.parent.scale.y||(this.parent.scale.set(e,s),this.parent.emit("zoomed",{viewport:this.parent,type:"clamp-zoom"}))}}reset(){this.clamp()}}const x={friction:.98,bounce:.8,minSpeed:.01},f=16;class v extends r{constructor(t,i={}){super(t),this.options=Object.assign({},x,i),this.saved=[],this.timeSinceRelease=0,this.reset(),this.parent.on("moved",(t=>this.moved(t)))}down(){return this.saved=[],this.x=this.y=null,!1}isActive(){return!(!this.x&&!this.y)}move(){if(this.paused)return!1;const t=this.parent.input.count();return(1===t||t>1&&!this.parent.plugins.get("pinch",!0))&&(this.saved.push({x:this.parent.x,y:this.parent.y,time:performance.now()}),this.saved.length>60&&this.saved.splice(0,30)),!1}moved(t){if(this.saved.length){const i=this.saved[this.saved.length-1];"clamp-x"===t.type?i.x===t.original.x&&(i.x=this.parent.x):"clamp-y"===t.type&&i.y===t.original.y&&(i.y=this.parent.y)}}up(){if(0===this.parent.input.count()&&this.saved.length){const t=performance.now();for(const i of this.saved)if(i.time>=t-100){const e=t-i.time;this.x=(this.parent.x-i.x)/e,this.y=(this.parent.y-i.y)/e,this.percentChangeX=this.percentChangeY=this.options.friction,this.timeSinceRelease=0;break}}return!1}activate(t){void 0!==(t=t||{}).x&&(this.x=t.x,this.percentChangeX=this.options.friction),void 0!==t.y&&(this.y=t.y,this.percentChangeY=this.options.friction)}update(t){if(this.paused)return;const i=this.x||this.y,e=this.timeSinceRelease,s=this.timeSinceRelease+t;if(this.x){const i=this.percentChangeX,h=Math.log(i);this.parent.x+=this.x*f/h*(Math.pow(i,s/f)-Math.pow(i,e/f)),this.x*=Math.pow(this.percentChangeX,t/f)}if(this.y){const i=this.percentChangeY,h=Math.log(i);this.parent.y+=this.y*f/h*(Math.pow(i,s/f)-Math.pow(i,e/f)),this.y*=Math.pow(this.percentChangeY,t/f)}this.timeSinceRelease+=t,Math.abs(this.x||0){t.includes(i.code)&&(this.keyIsPressed=!0)})),window.addEventListener("keyup",(i=>{t.includes(i.code)&&(this.keyIsPressed=!1)}))}mouseButtons(t){this.mouse=t&&"all"!==t?[-1!==t.indexOf("left"),-1!==t.indexOf("middle"),-1!==t.indexOf("right")]:[!0,!0,!0]}parseUnderflow(){const t=this.options.underflow.toLowerCase();"center"===t?(this.underflowX=0,this.underflowY=0):(this.underflowX=-1!==t.indexOf("left")?-1:-1!==t.indexOf("right")?1:0,this.underflowY=-1!==t.indexOf("top")?-1:-1!==t.indexOf("bottom")?1:0)}checkButtons(t){const i="mouse"===t.data.pointerType,e=this.parent.input.count();return!(!(1===e||e>1&&!this.parent.plugins.get("pinch",!0))||i&&!this.mouse[t.data.button])}checkKeyPress(t){return!this.options.keyToPress||this.keyIsPressed||this.options.ignoreKeyToPressOnTouch&&"touch"===t.data.pointerType}down(t){return!(this.paused||!this.options.pressDrag)&&(this.checkButtons(t)&&this.checkKeyPress(t)?(this.last={x:t.data.global.x,y:t.data.global.y},this.current=t.data.pointerId,!0):(this.last=null,!1))}get active(){return this.moved}move(t){if(this.paused||!this.options.pressDrag)return!1;if(this.last&&this.current===t.data.pointerId){const e=t.data.global.x,s=t.data.global.y,h=this.parent.input.count();if(1===h||h>1&&!this.parent.plugins.get("pinch",!0)){const h=e-this.last.x,n=s-this.last.y;if(this.moved||this.xDirection&&this.parent.input.checkThreshold(h)||this.yDirection&&this.parent.input.checkThreshold(n)){const h={x:e,y:s};return this.xDirection&&(this.parent.x+=(h.x-this.last.x)*this.options.factor),this.yDirection&&(this.parent.y+=(h.y-this.last.y)*this.options.factor),this.last=h,this.moved||this.parent.emit("drag-start",{event:t,screen:new i.Point(this.last.x,this.last.y),world:this.parent.toWorld(new i.Point(this.last.x,this.last.y)),viewport:this.parent}),this.moved=!0,this.parent.emit("moved",{viewport:this.parent,type:"drag"}),!0}}else this.moved=!1}return!1}up(t){if(this.paused)return!1;const e=this.parent.input.touches;if(1===e.length){const t=e[0];return t.last&&(this.last={x:t.last.x,y:t.last.y},this.current=t.id),this.moved=!1,!0}if(this.last&&this.moved){const e=new i.Point(this.last.x,this.last.y);return this.parent.emit("drag-end",{event:t,screen:e,world:this.parent.toWorld(e),viewport:this.parent}),this.last=null,this.moved=!1,!0}return!1}wheel(t){if(this.paused)return!1;if(this.options.wheel){if(!this.parent.plugins.get("wheel",!0)){const i=t.deltaMode?this.options.lineHeight:1;return this.xDirection&&(this.parent.x+=t.deltaX*i*this.options.wheelScroll*this.reverse),this.yDirection&&(this.parent.y+=t.deltaY*i*this.options.wheelScroll*this.reverse),this.options.clampWheel&&this.clamp(),this.parent.emit("wheel-scroll",this.parent),this.parent.emit("moved",{viewport:this.parent,type:"wheel"}),this.parent.options.passiveWheel||t.preventDefault(),!0}}return!1}resume(){this.last=null,this.paused=!1}clamp(){const t=this.parent.plugins.get("decelerate",!0)||{};if("y"!==this.options.clampWheel)if(this.parent.screenWorldWidththis.parent.worldWidth&&(this.parent.x=-this.parent.worldWidth*this.parent.scale.x+this.parent.screenWidth,t.x=0);if("x"!==this.options.clampWheel)if(this.parent.screenWorldHeightthis.parent.worldHeight&&(this.parent.y=-this.parent.worldHeight*this.parent.scale.y+this.parent.screenHeight,t.y=0)}}const H={speed:0,acceleration:null,radius:null};class S extends r{constructor(t,i,e={}){super(t),this.target=i,this.options=Object.assign({},H,e),this.velocity={x:0,y:0}}update(t){if(this.paused)return;const i=this.parent.center;let e=this.target.x,s=this.target.y;if(this.options.radius){if(!(Math.sqrt(Math.pow(this.target.y-i.y,2)+Math.pow(this.target.x-i.x,2))>this.options.radius))return;{const t=Math.atan2(this.target.y-i.y,this.target.x-i.x);e=this.target.x-Math.cos(t)*this.options.radius,s=this.target.y-Math.sin(t)*this.options.radius}}const h=e-i.x,n=s-i.y;if(h||n)if(this.options.speed)if(this.options.acceleration){const o=Math.atan2(s-i.y,e-i.x),r=Math.sqrt(Math.pow(h,2)+Math.pow(n,2));if(r){const a=(Math.pow(this.velocity.x,2)+Math.pow(this.velocity.y,2))/(2*this.options.acceleration);this.velocity=r>a?{x:Math.min(this.velocity.x+this.options.acceleration*t,this.options.speed),y:Math.min(this.velocity.y+this.options.acceleration*t,this.options.speed)}:{x:Math.max(this.velocity.x-this.options.acceleration*this.options.speed,0),y:Math.max(this.velocity.y-this.options.acceleration*this.options.speed,0)};const p=Math.cos(o)*this.velocity.x,l=Math.sin(o)*this.velocity.y,c=Math.abs(p)>Math.abs(h)?e:i.x+p,d=Math.abs(l)>Math.abs(n)?s:i.y+l;this.parent.moveCenter(c,d),this.parent.emit("moved",{viewport:this.parent,type:"follow"})}}else{const t=Math.atan2(s-i.y,e-i.x),o=Math.cos(t)*this.options.speed,r=Math.sin(t)*this.options.speed,a=Math.abs(o)>Math.abs(h)?e:i.x+o,p=Math.abs(r)>Math.abs(n)?s:i.y+r;this.parent.moveCenter(a,p),this.parent.emit("moved",{viewport:this.parent,type:"follow"})}else this.parent.moveCenter(e,s),this.parent.emit("moved",{viewport:this.parent,type:"follow"})}}const M={radius:null,distance:null,top:null,bottom:null,left:null,right:null,speed:8,reverse:!1,noDecelerate:!1,linear:!1,allowButtons:!1};class C extends r{constructor(t,i={}){super(t),this.options=Object.assign({},M,i),this.reverse=this.options.reverse?1:-1,this.radiusSquared="number"==typeof this.options.radius?Math.pow(this.options.radius,2):null,this.resize()}resize(){const t=this.options.distance;null!==t?(this.left=t,this.top=t,this.right=this.parent.worldScreenWidth-t,this.bottom=this.parent.worldScreenHeight-t):this.options.radius||(this.left=this.options.left,this.top=this.options.top,this.right=null===this.options.right?null:this.parent.worldScreenWidth-this.options.right,this.bottom=null===this.options.bottom?null:this.parent.worldScreenHeight-this.options.bottom)}down(){return this.paused||this.options.allowButtons||(this.horizontal=this.vertical=null),!1}move(t){if(this.paused)return!1;if("mouse"!==t.data.pointerType&&1!==t.data.identifier||!this.options.allowButtons&&0!==t.data.buttons)return!1;const i=t.data.global.x,e=t.data.global.y;if(this.radiusSquared){const t=this.parent.toScreen(this.parent.center);if(Math.pow(t.x-i,2)+Math.pow(t.y-e,2)>=this.radiusSquared){const s=Math.atan2(t.y-e,t.x-i);this.options.linear?(this.horizontal=Math.round(Math.cos(s))*this.options.speed*this.reverse*.06,this.vertical=Math.round(Math.sin(s))*this.options.speed*this.reverse*.06):(this.horizontal=Math.cos(s)*this.options.speed*this.reverse*.06,this.vertical=Math.sin(s)*this.options.speed*this.reverse*.06)}else this.horizontal&&this.decelerateHorizontal(),this.vertical&&this.decelerateVertical(),this.horizontal=this.vertical=0}else null!==this.left&&ithis.right?this.horizontal=-1*this.reverse*this.options.speed*.06:(this.decelerateHorizontal(),this.horizontal=0),null!==this.top&&ethis.bottom?this.vertical=-1*this.reverse*this.options.speed*.06:(this.decelerateVertical(),this.vertical=0);return!1}decelerateHorizontal(){const t=this.parent.plugins.get("decelerate",!0);this.horizontal&&t&&!this.options.noDecelerate&&t.activate({x:this.horizontal*this.options.speed*this.reverse/(1e3/60)})}decelerateVertical(){const t=this.parent.plugins.get("decelerate",!0);this.vertical&&t&&!this.options.noDecelerate&&t.activate({y:this.vertical*this.options.speed*this.reverse/(1e3/60)})}up(){return this.paused||(this.horizontal&&this.decelerateHorizontal(),this.vertical&&this.decelerateVertical(),this.horizontal=this.vertical=null),!1}update(){if(!this.paused&&(this.horizontal||this.vertical)){const t=this.parent.center;this.horizontal&&(t.x+=this.horizontal*this.options.speed),this.vertical&&(t.y+=this.vertical*this.options.speed),this.parent.moveCenter(t),this.parent.emit("moved",{viewport:this.parent,type:"mouse-edges"})}}}const z={noDrag:!1,percent:1,center:null,factor:1,axis:"all"};class _ extends r{__init(){this.active=!1}__init2(){this.pinching=!1}__init3(){this.moved=!1}constructor(t,i={}){super(t),_.prototype.__init.call(this),_.prototype.__init2.call(this),_.prototype.__init3.call(this),this.options=Object.assign({},z,i)}down(){return this.parent.input.count()>=2&&(this.active=!0,!0)}isAxisX(){return["all","x"].includes(this.options.axis)}isAxisY(){return["all","y"].includes(this.options.axis)}move(t){if(this.paused||!this.active)return!1;const i=t.data.global.x,e=t.data.global.y,s=this.parent.input.touches;if(s.length>=2){const h=s[0],n=s[1],o=h.last&&n.last?Math.sqrt(Math.pow(n.last.x-h.last.x,2)+Math.pow(n.last.y-h.last.y,2)):null;if(h.id===t.data.pointerId?h.last={x:i,y:e,data:t.data}:n.id===t.data.pointerId&&(n.last={x:i,y:e,data:t.data}),o){let t;const i={x:h.last.x+(n.last.x-h.last.x)/2,y:h.last.y+(n.last.y-h.last.y)/2};this.options.center||(t=this.parent.toLocal(i));let e=Math.sqrt(Math.pow(n.last.x-h.last.x,2)+Math.pow(n.last.y-h.last.y,2));e=0===e?e=1e-10:e;const s=(1-o/e)*this.options.percent*(this.isAxisX()?this.parent.scale.x:this.parent.scale.y);this.isAxisX()&&(this.parent.scale.x+=s),this.isAxisY()&&(this.parent.scale.y+=s),this.parent.emit("zoomed",{viewport:this.parent,type:"pinch",center:i});const r=this.parent.plugins.get("clamp-zoom",!0);if(r&&r.clamp(),this.options.center)this.parent.moveCenter(this.options.center);else{const e=this.parent.toGlobal(t);this.parent.x+=(i.x-e.x)*this.options.factor,this.parent.y+=(i.y-e.y)*this.options.factor,this.parent.emit("moved",{viewport:this.parent,type:"pinch"})}!this.options.noDrag&&this.lastCenter&&(this.parent.x+=(i.x-this.lastCenter.x)*this.options.factor,this.parent.y+=(i.y-this.lastCenter.y)*this.options.factor,this.parent.emit("moved",{viewport:this.parent,type:"pinch"})),this.lastCenter=i,this.moved=!0}else this.pinching||(this.parent.emit("pinch-start",this.parent),this.pinching=!0);return!0}return!1}up(){return!!(this.pinching&&this.parent.input.touches.length<=1)&&(this.active=!1,this.lastCenter=null,this.pinching=!1,this.moved=!1,this.parent.emit("pinch-end",this.parent),!0)}}const X={topLeft:!1,friction:.8,time:1e3,ease:"easeInOutSine",interrupt:!0,removeOnComplete:!1,removeOnInterrupt:!1,forceStart:!1};class k extends r{constructor(t,i,e,s={}){super(t),this.options=Object.assign({},X,s),this.ease=a(s.ease,"easeInOutSine"),this.x=i,this.y=e,this.options.forceStart&&this.snapStart()}snapStart(){this.percent=0,this.snapping={time:0};const t=this.options.topLeft?this.parent.corner:this.parent.center;this.deltaX=this.x-t.x,this.deltaY=this.y-t.y,this.startX=t.x,this.startY=t.y,this.parent.emit("snap-start",this.parent)}wheel(){return this.options.removeOnInterrupt&&this.parent.plugins.remove("snap"),!1}down(){return this.options.removeOnInterrupt?this.parent.plugins.remove("snap"):this.options.interrupt&&(this.snapping=null),!1}up(){if(0===this.parent.input.count()){const t=this.parent.plugins.get("decelerate",!0);t&&(t.x||t.y)&&(t.percentChangeX=t.percentChangeY=this.options.friction)}return!1}update(t){if(!(this.paused||this.options.interrupt&&0!==this.parent.input.count()))if(this.snapping){const i=this.snapping;let e,s,h;i.time+=t;const n=this.startX,o=this.startY,r=this.deltaX,a=this.deltaY;if(i.time>this.options.time)e=!0,s=n+r,h=o+a;else{const t=this.ease(i.time,0,1,this.options.time);s=n+r*t,h=o+a*t}this.options.topLeft?this.parent.moveCorner(s,h):this.parent.moveCenter(s,h),this.parent.emit("moved",{viewport:this.parent,type:"snap"}),e&&(this.options.removeOnComplete&&this.parent.plugins.remove("snap"),this.parent.emit("snap-end",this.parent),this.snapping=null)}else{const t=this.options.topLeft?this.parent.corner:this.parent.center;t.x===this.x&&t.y===this.y||this.snapStart()}}}const P={width:0,height:0,time:1e3,ease:"easeInOutSine",center:null,interrupt:!0,removeOnComplete:!1,removeOnInterrupt:!1,forceStart:!1,noMove:!1};class O extends r{constructor(t,i={}){super(t),this.options=Object.assign({},P,i),this.ease=a(this.options.ease),this.xIndependent=!1,this.yIndependent=!1,this.xScale=0,this.yScale=0,this.options.width>0&&(this.xScale=t.screenWidth/this.options.width,this.xIndependent=!0),this.options.height>0&&(this.yScale=t.screenHeight/this.options.height,this.yIndependent=!0),this.xScale=this.xIndependent?this.xScale:this.yScale,this.yScale=this.yIndependent?this.yScale:this.xScale,0===this.options.time?(t.container.scale.x=this.xScale,t.container.scale.y=this.yScale,this.options.removeOnComplete&&this.parent.plugins.remove("snap-zoom")):i.forceStart&&this.createSnapping()}createSnapping(){const t=this.parent.worldScreenWidth,i=this.parent.worldScreenHeight,e=this.parent.screenWidth/this.xScale,s=this.parent.screenHeight/this.yScale;this.snapping={time:0,startX:t,startY:i,deltaX:e-t,deltaY:s-i},this.parent.emit("snap-zoom-start",this.parent)}resize(){this.snapping=null,this.options.width>0&&(this.xScale=this.parent.screenWidth/this.options.width),this.options.height>0&&(this.yScale=this.parent.screenHeight/this.options.height),this.xScale=this.xIndependent?this.xScale:this.yScale,this.yScale=this.yIndependent?this.yScale:this.xScale}wheel(){return this.options.removeOnInterrupt&&this.parent.plugins.remove("snap-zoom"),!1}down(){return this.options.removeOnInterrupt?this.parent.plugins.remove("snap-zoom"):this.options.interrupt&&(this.snapping=null),!1}update(t){if(this.paused)return;if(this.options.interrupt&&0!==this.parent.input.count())return;let i;if(this.options.center||this.options.noMove||(i=this.parent.center),this.snapping){if(this.snapping){const e=this.snapping;if(e.time+=t,e.time>=this.options.time)this.parent.scale.set(this.xScale,this.yScale),this.options.removeOnComplete&&this.parent.plugins.remove("snap-zoom"),this.parent.emit("snap-zoom-end",this.parent),this.snapping=null;else{const t=this.snapping,i=this.ease(t.time,t.startX,t.deltaX,this.options.time),e=this.ease(t.time,t.startY,t.deltaY,this.options.time);this.parent.scale.x=this.parent.screenWidth/i,this.parent.scale.y=this.parent.screenHeight/e}const s=this.parent.plugins.get("clamp-zoom",!0);s&&s.clamp(),this.options.noMove||(this.options.center?this.parent.moveCenter(this.options.center):this.parent.moveCenter(i))}}else this.parent.scale.x===this.xScale&&this.parent.scale.y===this.yScale||this.createSnapping()}resume(){this.snapping=null,super.resume()}}const Y={percent:.1,smooth:!1,interrupt:!0,reverse:!1,center:null,lineHeight:20,axis:"all"};class A extends r{constructor(t,i={}){super(t),this.options=Object.assign({},Y,i)}down(){return this.options.interrupt&&(this.smoothing=null),!1}isAxisX(){return["all","x"].includes(this.options.axis)}isAxisY(){return["all","y"].includes(this.options.axis)}update(){if(this.smoothing){const t=this.smoothingCenter,i=this.smoothing;let e;this.options.center||(e=this.parent.toLocal(t)),this.isAxisX()&&(this.parent.scale.x+=i.x),this.isAxisY()&&(this.parent.scale.y+=i.y),this.parent.emit("zoomed",{viewport:this.parent,type:"wheel"});const s=this.parent.plugins.get("clamp-zoom",!0);if(s&&s.clamp(),this.options.center)this.parent.moveCenter(this.options.center);else{const i=this.parent.toGlobal(e);this.parent.x+=t.x-i.x,this.parent.y+=t.y-i.y}this.parent.emit("moved",{viewport:this.parent,type:"wheel"}),this.smoothingCount++,this.smoothingCount>=this.options.smooth&&(this.smoothing=null)}}wheel(t){if(this.paused)return;const i=this.parent.input.getPointerPosition(t),e=(this.options.reverse?-1:1)*-t.deltaY*(t.deltaMode?this.options.lineHeight:1)/500,s=Math.pow(2,(1+this.options.percent)*e);if(this.options.smooth){const t={x:this.smoothing?this.smoothing.x*(this.options.smooth-this.smoothingCount):0,y:this.smoothing?this.smoothing.y*(this.options.smooth-this.smoothingCount):0};this.smoothing={x:((this.parent.scale.x+t.x)*s-this.parent.scale.x)/this.options.smooth,y:((this.parent.scale.y+t.y)*s-this.parent.scale.y)/this.options.smooth},this.smoothingCount=0,this.smoothingCenter=i}else{let t;this.options.center||(t=this.parent.toLocal(i)),this.isAxisX()&&(this.parent.scale.x*=s),this.isAxisY()&&(this.parent.scale.y*=s),this.parent.emit("zoomed",{viewport:this.parent,type:"wheel"});const e=this.parent.plugins.get("clamp-zoom",!0);if(e&&e.clamp(),this.options.center)this.parent.moveCenter(this.options.center);else{const e=this.parent.toGlobal(t);this.parent.x+=i.x-e.x,this.parent.y+=i.y-e.y}}return this.parent.emit("moved",{viewport:this.parent,type:"wheel"}),this.parent.emit("wheel",{wheel:{dx:t.deltaX,dy:t.deltaY,dz:t.deltaZ},event:t,viewport:this.parent}),!this.parent.options.passiveWheel||void 0}}class I{constructor(t){this.viewport=t,this.touches=[],this.addListeners()}addListeners(){this.viewport.interactive=!0,this.viewport.forceHitArea||(this.viewport.hitArea=new i.Rectangle(0,0,this.viewport.worldWidth,this.viewport.worldHeight)),this.viewport.on("pointerdown",this.down,this),this.viewport.on("pointermove",this.move,this),this.viewport.on("pointerup",this.up,this),this.viewport.on("pointerupoutside",this.up,this),this.viewport.on("pointercancel",this.up,this),this.viewport.on("pointerout",this.up,this),this.wheelFunction=t=>this.handleWheel(t),this.viewport.options.divWheel.addEventListener("wheel",this.wheelFunction,{passive:this.viewport.options.passiveWheel}),this.isMouseDown=!1}destroy(){this.viewport.options.divWheel.removeEventListener("wheel",this.wheelFunction)}down(t){if(this.viewport.pause||!this.viewport.worldVisible)return;if("mouse"===t.data.pointerType?this.isMouseDown=!0:this.get(t.data.pointerId)||this.touches.push({id:t.data.pointerId,last:null}),1===this.count()){this.last=t.data.global.clone();const i=this.viewport.plugins.get("decelerate",!0),e=this.viewport.plugins.get("bounce",!0);i&&i.isActive()||e&&e.isActive()?this.clickedAvailable=!1:this.clickedAvailable=!0}else this.clickedAvailable=!1;this.viewport.plugins.down(t)&&this.viewport.options.stopPropagation&&t.stopPropagation()}clear(){this.isMouseDown=!1,this.touches=[],this.last=null}checkThreshold(t){return Math.abs(t)>=this.viewport.threshold}move(t){if(this.viewport.pause||!this.viewport.worldVisible)return;const i=this.viewport.plugins.move(t);if(this.clickedAvailable&&this.last){const i=t.data.global.x-this.last.x,e=t.data.global.y-this.last.y;(this.checkThreshold(i)||this.checkThreshold(e))&&(this.clickedAvailable=!1)}i&&this.viewport.options.stopPropagation&&t.stopPropagation()}up(t){if(this.viewport.pause||!this.viewport.worldVisible)return;"mouse"===t.data.pointerType&&(this.isMouseDown=!1),"mouse"!==t.data.pointerType&&this.remove(t.data.pointerId);const i=this.viewport.plugins.up(t);this.clickedAvailable&&0===this.count()&&this.last&&(this.viewport.emit("clicked",{event:t,screen:this.last,world:this.viewport.toWorld(this.last),viewport:this}),this.clickedAvailable=!1),i&&this.viewport.options.stopPropagation&&t.stopPropagation()}getPointerPosition(t){const e=new i.Point;return this.viewport.options.interaction?this.viewport.options.interaction.mapPositionToPoint(e,t.clientX,t.clientY):(e.x=t.clientX,e.y=t.clientY),e}handleWheel(t){if(this.viewport.pause||!this.viewport.worldVisible)return;if(this.viewport.options.interaction&&this.viewport.options.interaction.interactionDOMElement!==t.target)return;const i=this.viewport.toLocal(this.getPointerPosition(t));if(this.viewport.left<=i.x&&i.x<=this.viewport.right&&this.viewport.top<=i.y&&i.y<=this.viewport.bottom){this.viewport.plugins.wheel(t)&&!this.viewport.options.passiveWheel&&t.preventDefault()}}pause(){this.touches=[],this.isMouseDown=!1}get(t){for(const i of this.touches)if(i.id===t)return i;return null}remove(t){for(let i=0;ie.call(i,...t))),i=void 0)}return e}const T=["drag","pinch","wheel","follow","mouse-edges","decelerate","aniamte","bounce","snap-zoom","clamp-zoom","snap","clamp"];class V{constructor(t){this.viewport=t,this.list=[],this.plugins={}}add(t,i,e=T.length){this.plugins[t]=i;const s=T.indexOf(t);-1!==s&&T.splice(s,1),T.splice(e,0,t),this.sort()}get(t,i){return i&&D([this,"access",t=>t.plugins,"access",i=>i[t],"optionalAccess",t=>t.paused])?null:this.plugins[t]}update(t){for(const i of this.list)i.update(t)}resize(){for(const t of this.list)t.resize()}reset(){for(const t of this.list)t.reset()}removeAll(){this.plugins={},this.sort()}remove(t){this.plugins[t]&&(delete this.plugins[t],this.viewport.emit(t+"-remove"),this.sort())}pause(t){D([this,"access",t=>t.plugins,"access",i=>i[t],"optionalAccess",t=>t.pause,"call",t=>t()])}resume(t){D([this,"access",t=>t.plugins,"access",i=>i[t],"optionalAccess",t=>t.resume,"call",t=>t()])}sort(){this.list=[];for(const t of T)this.plugins[t]&&this.list.push(this.plugins[t])}down(t){let i=!1;for(const e of this.list)e.down(t)&&(i=!0);return i}move(t){let i=!1;for(const e of this.viewport.plugins.list)e.move(t)&&(i=!0);return i}up(t){let i=!1;for(const e of this.list)e.up(t)&&(i=!0);return i}wheel(t){let i=!1;for(const e of this.list)e.wheel(t)&&(i=!0);return i}}const L={screenWidth:window.innerWidth,screenHeight:window.innerHeight,worldWidth:null,worldHeight:null,threshold:5,passiveWheel:!0,stopPropagation:!1,forceHitArea:null,noTicker:!1,interaction:null,disableOnContextMenu:!1,ticker:h.Ticker.shared};class j extends s.Container{constructor(t={}){super(),this.options=Object.assign({},{divWheel:document.body},L,t),this.screenWidth=this.options.screenWidth,this.screenHeight=this.options.screenHeight,this._worldWidth=this.options.worldWidth,this._worldHeight=this.options.worldHeight,this.forceHitArea=this.options.forceHitArea,this.threshold=this.options.threshold,this.options.divWheel=this.options.divWheel||document.body,this.options.disableOnContextMenu&&(this.options.divWheel.oncontextmenu=t=>t.preventDefault()),this.options.noTicker||(this.tickerFunction=()=>this.update(this.options.ticker.elapsedMS),this.options.ticker.add(this.tickerFunction)),this.input=new I(this),this.plugins=new V(this)}destroy(t){!this.options.noTicker&&this.tickerFunction&&this.options.ticker.remove(this.tickerFunction),this.input.destroy(),super.destroy(t)}update(t){this.pause||(this.plugins.update(t),this.lastViewport&&(this.lastViewport.x!==this.x||this.lastViewport.y!==this.y?this.moving=!0:this.moving&&(this.emit("moved-end",this),this.moving=!1),this.lastViewport.scaleX!==this.scale.x||this.lastViewport.scaleY!==this.scale.y?this.zooming=!0:this.zooming&&(this.emit("zoomed-end",this),this.zooming=!1)),this.forceHitArea||(this._hitAreaDefault=new i.Rectangle(this.left,this.top,this.worldScreenWidth,this.worldScreenHeight),this.hitArea=this._hitAreaDefault),this._dirty=this._dirty||!this.lastViewport||this.lastViewport.x!==this.x||this.lastViewport.y!==this.y||this.lastViewport.scaleX!==this.scale.x||this.lastViewport.scaleY!==this.scale.y,this.lastViewport={x:this.x,y:this.y,scaleX:this.scale.x,scaleY:this.scale.y},this.emit("frame-end",this))}resize(t=window.innerWidth,i=window.innerHeight,e,s){this.screenWidth=t,this.screenHeight=i,void 0!==e&&(this._worldWidth=e),void 0!==s&&(this._worldHeight=s),this.plugins.resize(),this.dirty=!0}get worldWidth(){return this._worldWidth?this._worldWidth:this.width/this.scale.x}set worldWidth(t){this._worldWidth=t,this.plugins.resize()}get worldHeight(){return this._worldHeight?this._worldHeight:this.height/this.scale.y}set worldHeight(t){this._worldHeight=t,this.plugins.resize()}getVisibleBounds(){return new i.Rectangle(this.left,this.top,this.worldScreenWidth,this.worldScreenHeight)}toWorld(t,e){return 2===arguments.length?this.toLocal(new i.Point(t,e)):this.toLocal(t)}toScreen(t,e){return 2===arguments.length?this.toGlobal(new i.Point(t,e)):this.toGlobal(t)}get worldScreenWidth(){return this.screenWidth/this.scale.x}get worldScreenHeight(){return this.screenHeight/this.scale.y}get screenWorldWidth(){return this.worldWidth*this.scale.x}get screenWorldHeight(){return this.worldHeight*this.scale.y}get center(){return new i.Point(this.worldScreenWidth/2-this.x/this.scale.x,this.worldScreenHeight/2-this.y/this.scale.y)}set center(t){this.moveCenter(t)}moveCenter(...t){let i,e;"number"==typeof t[0]?(i=t[0],e=t[1]):(i=t[0].x,e=t[0].y);const s=(this.worldScreenWidth/2-i)*this.scale.x,h=(this.worldScreenHeight/2-e)*this.scale.y;return this.x===s&&this.y===h||(this.position.set(s,h),this.plugins.reset(),this.dirty=!0),this}get corner(){return new i.Point(-this.x/this.scale.x,-this.y/this.scale.y)}set corner(t){this.moveCorner(t)}moveCorner(...t){let i,e;return 1===t.length?(i=-t[0].x*this.scale.x,e=-t[0].y*this.scale.y):(i=-t[0]*this.scale.x,e=-t[1]*this.scale.y),i===this.x&&e===this.y||(this.position.set(i,e),this.plugins.reset(),this.dirty=!0),this}get screenWidthInWorldPixels(){return this.screenWidth/this.scale.x}get screenHeightInWorldPixels(){return this.screenHeight/this.scale.y}findFitWidth(t){return this.screenWidth/t}findFitHeight(t){return this.screenHeight/t}findFit(t,i){const e=this.screenWidth/t,s=this.screenHeight/i;return Math.min(e,s)}findCover(t,i){const e=this.screenWidth/t,s=this.screenHeight/i;return Math.max(e,s)}fitWidth(t=this.worldWidth,i,e=!0,s){let h;i&&(h=this.center),this.scale.x=this.screenWidth/t,e&&(this.scale.y=this.scale.x);const n=this.plugins.get("clamp-zoom",!0);return!s&&n&&n.clamp(),i&&h&&this.moveCenter(h),this}fitHeight(t=this.worldHeight,i,e=!0,s){let h;i&&(h=this.center),this.scale.y=this.screenHeight/t,e&&(this.scale.x=this.scale.y);const n=this.plugins.get("clamp-zoom",!0);return!s&&n&&n.clamp(),i&&h&&this.moveCenter(h),this}fitWorld(t){let i;t&&(i=this.center),this.scale.x=this.screenWidth/this.worldWidth,this.scale.y=this.screenHeight/this.worldHeight,this.scale.xthis.worldWidth,top:this.top<0,bottom:this.bottom>this.worldHeight,cornerPoint:new i.Point(this.worldWidth*this.scale.x-this.screenWidth,this.worldHeight*this.scale.y-this.screenHeight)}}get right(){return-this.x/this.scale.x+this.worldScreenWidth}set right(t){this.x=-t*this.scale.x+this.screenWidth,this.plugins.reset()}get left(){return-this.x/this.scale.x}set left(t){this.x=-t*this.scale.x,this.plugins.reset()}get top(){return-this.y/this.scale.y}set top(t){this.y=-t*this.scale.y,this.plugins.reset()}get bottom(){return-this.y/this.scale.y+this.worldScreenHeight}set bottom(t){this.y=-t*this.scale.y+this.screenHeight,this.plugins.reset()}get dirty(){return!!this._dirty}set dirty(t){this._dirty=t}get forceHitArea(){return this._forceHitArea}set forceHitArea(t){t?(this._forceHitArea=t,this.hitArea=t):(this._forceHitArea=null,this.hitArea=new i.Rectangle(0,0,this.worldWidth,this.worldHeight))}drag(t){return this.plugins.add("drag",new b(this,t)),this}clamp(t){return this.plugins.add("clamp",new m(this,t)),this}decelerate(t){return this.plugins.add("decelerate",new v(this,t)),this}bounce(t){return this.plugins.add("bounce",new u(this,t)),this}pinch(t){return this.plugins.add("pinch",new _(this,t)),this}snap(t,i,e){return this.plugins.add("snap",new k(this,t,i,e)),this}follow(t,i){return this.plugins.add("follow",new S(this,t,i)),this}wheel(t){return this.plugins.add("wheel",new A(this,t)),this}animate(t){return this.plugins.add("animate",new l(this,t)),this}clampZoom(t){return this.plugins.add("clamp-zoom",new y(this,t)),this}mouseEdges(t){return this.plugins.add("mouse-edges",new C(this,t)),this}get pause(){return!!this._pause}set pause(t){this._pause=t,this.lastViewport=null,this.moving=!1,this.zooming=!1,t&&this.input.pause()}ensureVisible(t,i,e,s,h){h&&(e>this.worldScreenWidth||s>this.worldScreenHeight)&&(this.fit(!0,e,s),this.emit("zoomed",{viewport:this,type:"ensureVisible"}));let n=!1;tthis.right&&(this.right=t+e,n=!0),ithis.bottom&&(this.bottom=i+s,n=!0),n&&this.emit("moved",{viewport:this,type:"ensureVisible"})}}t.Animate=l,t.Bounce=u,t.Clamp=m,t.ClampZoom=y,t.Decelerate=v,t.Drag=b,t.Follow=S,t.InputManager=I,t.MouseEdges=C,t.Pinch=_,t.Plugin=r,t.PluginManager=V,t.Snap=k,t.SnapZoom=O,t.Viewport=j,t.Wheel=A,Object.defineProperty(t,"__esModule",{value:!0})})),"undefined"!=typeof pixi_viewport&&Object.assign(this.PIXI,pixi_viewport); //# sourceMappingURL=viewport.min.min.js.map diff --git a/dist/viewport.min.min.js.map b/dist/viewport.min.min.js.map index 19834f08..3ba19341 100644 --- a/dist/viewport.min.min.js.map +++ b/dist/viewport.min.min.js.map @@ -1 +1 @@ -{"version":3,"file":"viewport.min.min.js","sources":["../src/plugins/Animate.ts","../src/plugins/Bounce.ts","../src/plugins/Clamp.ts","../src/plugins/ClampZoom.ts","../src/plugins/Decelerate.ts","../src/plugins/Drag.ts","../src/plugins/Follow.ts","../src/plugins/MouseEdges.ts","../src/plugins/Pinch.ts","../src/plugins/Snap.ts","../src/plugins/SnapZoom.ts","../src/plugins/Wheel.ts","../src/PluginManager.ts","../src/Viewport.ts"],"sourcesContent":["import { Point } from '@pixi/math';\nimport { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Animate}. */\nexport interface IAnimateOptions {\n /** Time to animate */\n time?: number;\n\n /** Position to move the viewport to */\n position?: Point;\n\n /**\n * Desired viewport width in world pixels\n *\n * (use instead of scale; aspect ratio is maintained if height is not provided)\n */\n width?: number;\n\n /**\n * Desired viewport height in world pixels\n *\n * (use instead of scale; aspect ratio is maintained if width is not provided)\n */\n height?: number;\n\n /** Scale to change zoom (scale.x = scale.y) */\n scale?: number;\n\n /** Independently change zoom in x-direction */\n scaleX?: number;\n\n /** Independently change zoom in y-direction */\n scaleY?: number;\n\n /** Easing function to use */\n ease?: any;\n\n /** Callback to invoke when the animation completes */\n callbackOnComplete?: (viewport: Viewport) => void;\n\n /** Removes this plugin if interrupted by any user input */\n removeOnInterrupt?: boolean;\n}\n\nconst DEFAULT_ANIMATE_OPTIONS = {\n removeOnInterrupt: false,\n ease: 'linear',\n time: 1000,\n};\n\n/**\n * Animation plugin.\n *\n * @see Viewport#animate\n * @fires animate-end\n */\nexport class Animate extends Plugin\n{\n public readonly options: IAnimateOptions & { ease: any; time: number };\n\n /** The starting x-coordinate of the viewport. */\n protected startX?: number;\n\n /** The starting y-coordinate of the viewport. */\n protected startY?: number;\n\n /** The change in the x-coordinate of the viewport through the animation.*/\n protected deltaX?: number;\n\n /** The change in the y-coordinate of the viewport through the animation. */\n protected deltaY?: number;\n\n /** Marks whether the center of the viewport is preserved in the animation. */\n protected keepCenter!: boolean;\n\n /** The starting viewport width. */\n protected startWidth: number | null = null;\n\n /** The starting viewport height. */\n protected startHeight: number | null = null;\n\n /** The change in the viewport's width through the animation. */\n protected deltaWidth: number | null = null;\n\n /** The change in the viewport's height through the animation. */\n protected deltaHeight: number | null = null;\n\n /** The viewport's width post-animation. */\n protected width: number | null = null;\n\n /** The viewport's height post-animation. */\n protected height: number | null = null;\n\n /** The time since the animation started. */\n protected time = 0;\n\n /**\n * This is called by {@link Viewport.animate}.\n *\n * @param parent\n * @param options\n */\n constructor(parent: Viewport, options: IAnimateOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_ANIMATE_OPTIONS, options);\n this.options.ease = ease(this.options.ease);\n\n this.setupPosition();\n this.setupZoom();\n\n this.time = 0;\n }\n\n /**\n * Setup `startX`, `startY`, `deltaX`, `deltaY`, `keepCenter`.\n *\n * This is called during construction.\n */\n protected setupPosition(): void\n {\n if (typeof this.options.position !== 'undefined')\n {\n this.startX = this.parent.center.x;\n this.startY = this.parent.center.y;\n this.deltaX = this.options.position.x - this.parent.center.x;\n this.deltaY = this.options.position.y - this.parent.center.y;\n this.keepCenter = false;\n }\n else\n {\n this.keepCenter = true;\n }\n }\n\n /**\n * Setup `startWidth, `startHeight`, `deltaWidth, `deltaHeight, `width`, `height`.\n *\n * This is called during construction.\n */\n protected setupZoom(): void\n {\n this.width = null;\n this.height = null;\n\n if (typeof this.options.scale !== 'undefined')\n {\n this.width = this.parent.screenWidth / this.options.scale;\n }\n else if (typeof this.options.scaleX !== 'undefined' || typeof this.options.scaleY !== 'undefined')\n {\n if (typeof this.options.scaleX !== 'undefined')\n {\n // screenSizeInWorldPixels = screenWidth / scale\n this.width = this.parent.screenWidth / this.options.scaleX;\n }\n if (typeof this.options.scaleY !== 'undefined')\n {\n this.height = this.parent.screenHeight / this.options.scaleY;\n }\n }\n else\n {\n if (typeof this.options.width !== 'undefined')\n {\n this.width = this.options.width;\n }\n if (typeof this.options.height !== 'undefined')\n {\n this.height = this.options.height;\n }\n }\n\n if (this.width !== null)\n {\n this.startWidth = this.parent.screenWidthInWorldPixels;\n this.deltaWidth = this.width - this.startWidth;\n }\n if (this.height !== null)\n {\n this.startHeight = this.parent.screenHeightInWorldPixels;\n this.deltaHeight = this.height - this.startHeight;\n }\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('animate');\n }\n\n return false;\n }\n\n public complete(): void\n {\n this.parent.plugins.remove('animate');\n if (this.width !== null)\n {\n this.parent.fitWidth(this.width, this.keepCenter, this.height === null);\n }\n if (this.height !== null)\n {\n this.parent.fitHeight(this.height, this.keepCenter, this.width === null);\n }\n if (!this.keepCenter)\n {\n this.parent.moveCenter(this.options.position as Point);\n }\n\n this.parent.emit('animate-end', this.parent);\n\n if (this.options.callbackOnComplete)\n {\n this.options.callbackOnComplete(this.parent);\n }\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n this.time += elapsed;\n\n if (this.time >= this.options.time)\n {\n this.complete();\n }\n else\n {\n const originalZoom = new Point(this.parent.scale.x, this.parent.scale.y);\n const percent = this.options.ease(this.time, 0, 1, this.options.time);\n\n if (this.width !== null)\n {\n const startWidth = this.startWidth as number;\n const deltaWidth = this.deltaWidth as number;\n\n this.parent.fitWidth(\n startWidth + (deltaWidth * percent),\n this.keepCenter,\n this.height === null);\n }\n if (this.height !== null)\n {\n const startHeight = this.startHeight as number;\n const deltaHeight = this.deltaHeight as number;\n\n this.parent.fitHeight(\n startHeight + (deltaHeight * percent),\n this.keepCenter,\n this.width === null);\n }\n if (this.width === null)\n {\n this.parent.scale.x = this.parent.scale.y;\n }\n else if (this.height === null)\n {\n this.parent.scale.y = this.parent.scale.x;\n }\n if (!this.keepCenter)\n {\n const startX = this.startX as number;\n const startY = this.startY as number;\n const deltaX = this.deltaX as number;\n const deltaY = this.deltaY as number;\n const original = new Point(this.parent.x, this.parent.y);\n\n this.parent.moveCenter(startX + (deltaX * percent), startY + (deltaY * percent));\n this.parent.emit('moved', { viewport: this.parent, original, type: 'animate' });\n }\n if (this.width || this.height)\n {\n this.parent.emit('zoomed', { viewport: this.parent, original: originalZoom, type: 'animate' });\n }\n }\n }\n}\n","import { Point, Rectangle } from '@pixi/math';\nimport { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Drag } from './Drag';\nimport type { IDecelerateOptions } from './Decelerate';\nimport type { Pinch } from './Pinch';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Bounce}. */\nexport interface IBounceOptions {\n /** \"all\", \"horizontal\", \"vertical\", or combination of \"top\", \"bottom\", \"right\", \"left\" (e.g., 'top-bottom-right') */\n sides?:\n 'all'\n | 'horizontal'\n | 'vertical'\n | string;\n\n /** Friction to apply to decelerate if active */\n friction?: number;\n\n /** Time in ms to finish bounce */\n time?: number;\n\n /** Use this bounceBox instead of (0, 0, viewport.worldWidth, viewport.worldHeight) */\n bounceBox?: Rectangle | null;\n\n /** Ease function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /** (top/bottom/center and left/right/center, or center) where to place world if too small for screen */\n underflow?: 'center' | string;\n}\n\n/** Bounce state along an axis */\nexport interface IBounceState {\n /** Elapsed time since bounce started */\n time: number;\n\n /** Starting coordinate */\n start: number;\n\n /** Change in coordinate through bounce */\n delta: number;\n\n /** Ending coordinate */\n end: number;\n}\n\nconst DEFAULT_BOUNCE_OPTIONS: Required = {\n sides: 'all',\n friction: 0.5,\n time: 150,\n ease: 'easeInOutSine',\n underflow: 'center',\n bounceBox: null\n};\n\n/**\n * @fires bounce-start-x\n * @fires bounce.end-x\n * @fires bounce-start-y\n * @fires bounce-end-y\n * @public\n */\nexport class Bounce extends Plugin\n{\n /** The options passed to initialize this plugin, cannot be modified again. */\n public readonly options: Readonly>;\n\n /** Holds whether to bounce from left side. */\n public readonly left: boolean ;\n\n /** Holds whether to bounce from top side. */\n public readonly top: boolean;\n\n /** Holds whether to bounce from right side. */\n public readonly right: boolean;\n\n /** Holds whether to bounce from bottom side. */\n public readonly bottom: boolean;\n\n /** Direction of underflow along x-axis. */\n public readonly underflowX: -1 | 0 | 1;\n\n /** Direction of underflow along y-axis. */\n public readonly underflowY: -1 | 0 | 1;\n\n /** Easing */\n protected ease: any;\n\n /** Bounce state along x-axis */\n protected toX!: IBounceState | null;\n\n /** Bounce state along y-axis */\n protected toY!: IBounceState | null;\n\n /**\n * This is called by {@link Viewport.bounce}.\n */\n constructor(parent: Viewport, options: IBounceOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_BOUNCE_OPTIONS, options);\n this.ease = ease(this.options.ease, 'easeInOutSine');\n\n if (this.options.sides)\n {\n if (this.options.sides === 'all')\n {\n this.top = this.bottom = this.left = this.right = true;\n }\n else if (this.options.sides === 'horizontal')\n {\n this.right = this.left = true;\n this.top = this.bottom = false;\n }\n else if (this.options.sides === 'vertical')\n {\n this.left = this.right = false;\n this.top = this.bottom = true;\n }\n else\n {\n this.top = this.options.sides.indexOf('top') !== -1;\n this.bottom = this.options.sides.indexOf('bottom') !== -1;\n this.left = this.options.sides.indexOf('left') !== -1;\n this.right = this.options.sides.indexOf('right') !== -1;\n }\n } else {\n this.left = this.top = this.right = this.bottom = false;\n }\n\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'center')\n {\n this.underflowX = 0;\n this.underflowY = 0;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n }\n\n this.reset();\n }\n\n public isActive(): boolean\n {\n return this.toX !== null || this.toY !== null;\n }\n\n public down(): boolean\n {\n this.toX = this.toY = null;\n\n return false;\n }\n\n public up(): boolean\n {\n this.bounce();\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n this.bounce();\n\n if (this.toX)\n {\n const toX = this.toX;\n\n toX.time += elapsed;\n this.parent.emit('moved', { viewport: this.parent, type: 'bounce-x' });\n\n if (toX.time >= this.options.time)\n {\n this.parent.x = toX.end;\n this.toX = null;\n this.parent.emit('bounce-x-end', this.parent);\n }\n else\n {\n this.parent.x = this.ease(toX.time, toX.start, toX.delta, this.options.time);\n }\n }\n\n if (this.toY)\n {\n const toY = this.toY;\n\n toY.time += elapsed;\n this.parent.emit('moved', { viewport: this.parent, type: 'bounce-y' });\n\n if (toY.time >= this.options.time)\n {\n this.parent.y = toY.end;\n this.toY = null;\n this.parent.emit('bounce-y-end', this.parent);\n }\n else\n {\n this.parent.y = this.ease(toY.time, toY.start, toY.delta, this.options.time);\n }\n }\n }\n\n /** @internal */\n protected calcUnderflowX(): number\n {\n let x: number;\n\n switch (this.underflowX)\n {\n case -1:\n x = 0;\n break;\n case 1:\n x = (this.parent.screenWidth - this.parent.screenWorldWidth);\n break;\n default:\n x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n }\n\n return x;\n }\n\n /** @internal */\n protected calcUnderflowY(): number\n {\n let y: number;\n\n switch (this.underflowY)\n {\n case -1:\n y = 0;\n break;\n case 1:\n y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n break;\n default:\n y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n }\n\n return y;\n }\n\n private oob(): Record<'left' | 'right' | 'top' | 'bottom', boolean> & Record<'topLeft' | 'bottomRight', Point>\n {\n const box = this.options.bounceBox;\n\n if (box)\n {\n const x1 = typeof box.x === 'undefined' ? 0 : box.x;\n const y1 = typeof box.y === 'undefined' ? 0 : box.y;\n const width = typeof box.width === 'undefined' ? this.parent.worldWidth : box.width;\n const height = typeof box.height === 'undefined' ? this.parent.worldHeight : box.height;\n\n return {\n left: this.parent.left < x1,\n right: this.parent.right > width,\n top: this.parent.top < y1,\n bottom: this.parent.bottom > height,\n topLeft: new Point(\n x1 * this.parent.scale.x,\n y1 * this.parent.scale.y\n ),\n bottomRight: new Point(\n width * this.parent.scale.x - this.parent.screenWidth,\n height * this.parent.scale.y - this.parent.screenHeight\n )\n };\n }\n\n return {\n left: this.parent.left < 0,\n right: this.parent.right > this.parent.worldWidth,\n top: this.parent.top < 0,\n bottom: this.parent.bottom > this.parent.worldHeight,\n topLeft: new Point(0, 0),\n bottomRight: new Point(\n this.parent.worldWidth * this.parent.scale.x - this.parent.screenWidth,\n this.parent.worldHeight * this.parent.scale.y - this.parent.screenHeight\n )\n };\n }\n\n public bounce(): void\n {\n if (this.paused)\n {\n return;\n }\n\n let oob;\n let decelerate: undefined | null | {\n percentChangeX?: number;\n percentChangeY?: number;\n x?: number;\n y?: number;\n options?: IDecelerateOptions\n } = this.parent.plugins.get('decelerate', true) as any;\n\n if (decelerate && (decelerate.x || decelerate.y))\n {\n if ((decelerate.x && decelerate.percentChangeX === decelerate.options?.friction) || (decelerate.y && decelerate.percentChangeY === decelerate.options?.friction))\n {\n oob = this.oob();\n if ((oob.left && this.left) || (oob.right && this.right))\n {\n decelerate.percentChangeX = this.options.friction;\n }\n if ((oob.top && this.top) || (oob.bottom && this.bottom))\n {\n decelerate.percentChangeY = this.options.friction;\n }\n }\n }\n const drag: Partial = this.parent.plugins.get('drag', true) || {};\n const pinch: Partial = this.parent.plugins.get('pinch', true) || {};\n\n decelerate = decelerate || {};\n\n if (!drag?.active && !pinch?.active && ((!this.toX || !this.toY) && (!decelerate.x || !decelerate.y)))\n {\n oob = oob || this.oob();\n const topLeft = oob.topLeft;\n const bottomRight = oob.bottomRight;\n\n if (!this.toX && !decelerate.x)\n {\n let x = null;\n\n if (oob.left && this.left)\n {\n x = (this.parent.screenWorldWidth < this.parent.screenWidth) ? this.calcUnderflowX() : -topLeft.x;\n }\n else if (oob.right && this.right)\n {\n x = (this.parent.screenWorldWidth < this.parent.screenWidth) ? this.calcUnderflowX() : -bottomRight.x;\n }\n if (x !== null && this.parent.x !== x)\n {\n this.toX = { time: 0, start: this.parent.x, delta: x - this.parent.x, end: x };\n this.parent.emit('bounce-x-start', this.parent);\n }\n }\n if (!this.toY && !decelerate.y)\n {\n let y = null;\n\n if (oob.top && this.top)\n {\n y = (this.parent.screenWorldHeight < this.parent.screenHeight) ? this.calcUnderflowY() : -topLeft.y;\n }\n else if (oob.bottom && this.bottom)\n {\n y = (this.parent.screenWorldHeight < this.parent.screenHeight) ? this.calcUnderflowY() : -bottomRight.y;\n }\n if (y !== null && this.parent.y !== y)\n {\n this.toY = { time: 0, start: this.parent.y, delta: y - this.parent.y, end: y };\n this.parent.emit('bounce-y-start', this.parent);\n }\n }\n }\n }\n\n public reset(): void\n {\n this.toX = this.toY = null;\n this.bounce();\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\n\n/**\n * There are three ways to clamp:\n * 1. direction: 'all' = the world is clamped to its world boundaries, ie, you cannot drag any part of the world offscreen\n * direction: 'x' | 'y' = only the x or y direction is clamped to its world boundary\n * 2. left, right, top, bottom = true | number = the world is clamped to the world's pixel location for each side;\n * if any of these are set to true, then the location is set to the boundary [0, viewport.worldWidth/viewport.worldHeight]\n * eg: to allow the world to be completely dragged offscreen, set [-viewport.worldWidth, -viewport.worldHeight, viewport.worldWidth * 2, viewport.worldHeight * 2]\n *\n * Underflow determines what happens when the world is smaller than the viewport\n * 1. none = the world is clamped but there is no special behavior\n * 2. center = the world is centered on the viewport\n * 3. combination of top/bottom/center and left/right/center (case insensitive) = the world is stuck to the appropriate boundaries\n *\n */\nexport interface IClampOptions\n{\n /**\n * Clamp left; true = 0\n *\n * @default false\n */\n left?: number | boolean | null;\n\n /**\n * Clamp top; true = 0\n *\n * @default false\n */\n top?: number | boolean | null;\n\n /**\n * Clamp right; true = viewport.worldWidth\n *\n * @default false\n */\n right?: number | boolean | null;\n\n /**\n * Clamp bottom; true = viewport.worldHeight\n *\n * @default false\n */\n bottom?: number | boolean | null;\n\n /**\n * (all, x, or y) using clamps of [0, viewport.worldWidth/viewport.worldHeight]; replaces left/right/top/bottom if set\n *\n * @default null\n */\n direction?: 'all' | 'x' | 'y' | null;\n\n /**\n * Where to place world if too small for screen (e.g., top-right, center, none, bottomleft)\n *\n * @default 'center'\n */\n underflow?: 'center' | string;\n}\n\nconst DEFAULT_CLAMP_OPTIONS: Required = {\n left: false,\n right: false,\n top: false,\n bottom: false,\n direction: null,\n underflow: 'center'\n};\n\n/**\n * Plugin to clamp the viewport to a specific world bounding box.\n *\n * @public\n */\nexport class Clamp extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Required;\n\n /** Last state of viewport */\n protected last: {\n x: number | null;\n y: number | null;\n scaleX: number | null;\n scaleY: number | null;\n };\n\n protected noUnderflow!: boolean;\n protected underflowX!: -1 | 0 | 1;\n protected underflowY!: -1 | 0 | 1;\n\n /**\n * This is called by {@link Viewport.clamp}.\n */\n constructor(parent: Viewport, options : IClampOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_CLAMP_OPTIONS, options);\n\n if (this.options.direction)\n {\n this.options.left = this.options.direction === 'x' || this.options.direction === 'all' ? true : null;\n this.options.right = this.options.direction === 'x' || this.options.direction === 'all' ? true : null;\n this.options.top = this.options.direction === 'y' || this.options.direction === 'all' ? true : null;\n this.options.bottom = this.options.direction === 'y' || this.options.direction === 'all' ? true : null;\n }\n\n this.parseUnderflow();\n this.last = { x: null, y: null, scaleX: null, scaleY: null };\n this.update();\n }\n\n private parseUnderflow()\n {\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'none')\n {\n this.noUnderflow = true;\n }\n else if (clamp === 'center')\n {\n this.underflowX = this.underflowY = 0;\n this.noUnderflow = false;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n this.noUnderflow = false;\n }\n }\n\n public move(): boolean\n {\n this.update();\n\n return false;\n }\n\n public update(): void\n {\n if (this.paused)\n {\n return;\n }\n\n // only clamp on change\n if (this.parent.x === this.last.x\n && this.parent.y === this.last.y\n && this.parent.scale.x === this.last.scaleX\n && this.parent.scale.y === this.last.scaleY)\n {\n return;\n }\n const original = { x: this.parent.x, y: this.parent.y };\n // TODO: Fix\n const decelerate: any = (this.parent.plugins as any).decelerate || {};\n\n if (this.options.left !== null || this.options.right !== null)\n {\n let moved = false;\n\n if (!this.noUnderflow && this.parent.screenWorldWidth < this.parent.screenWidth)\n {\n switch (this.underflowX)\n {\n case -1:\n if (this.parent.x !== 0)\n {\n this.parent.x = 0;\n moved = true;\n }\n break;\n case 1:\n if (this.parent.x !== this.parent.screenWidth - this.parent.screenWorldWidth)\n {\n this.parent.x = this.parent.screenWidth - this.parent.screenWorldWidth;\n moved = true;\n }\n break;\n default:\n if (this.parent.x !== (this.parent.screenWidth - this.parent.screenWorldWidth) / 2)\n {\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n moved = true;\n }\n }\n }\n else\n {\n if (this.options.left !== null)\n {\n if (this.parent.left < (this.options.left === true ? 0 : this.options.left))\n {\n this.parent.x = -(this.options.left === true ? 0 : this.options.left) * this.parent.scale.x;\n decelerate.x = 0;\n moved = true;\n }\n }\n if (this.options.right !== null)\n {\n if (this.parent.right > (this.options.right === true ? this.parent.worldWidth : this.options.right))\n {\n this.parent.x = -(this.options.right === true ? this.parent.worldWidth : this.options.right) * this.parent.scale.x + this.parent.screenWidth;\n decelerate.x = 0;\n moved = true;\n }\n }\n }\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, original, type: 'clamp-x' });\n }\n }\n if (this.options.top !== null || this.options.bottom !== null)\n {\n let moved = false;\n\n if (!this.noUnderflow && this.parent.screenWorldHeight < this.parent.screenHeight)\n {\n switch (this.underflowY)\n {\n case -1:\n if (this.parent.y !== 0)\n {\n this.parent.y = 0;\n moved = true;\n }\n break;\n case 1:\n if (this.parent.y !== this.parent.screenHeight - this.parent.screenWorldHeight)\n {\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n moved = true;\n }\n break;\n default:\n if (this.parent.y !== (this.parent.screenHeight - this.parent.screenWorldHeight) / 2)\n {\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n moved = true;\n }\n }\n }\n else\n {\n if (this.options.top !== null)\n {\n if (this.parent.top < (this.options.top === true ? 0 : this.options.top))\n {\n this.parent.y = -(this.options.top === true ? 0 : this.options.top)\n * this.parent.scale.y;\n decelerate.y = 0;\n moved = true;\n }\n }\n if (this.options.bottom !== null)\n {\n if (this.parent.bottom > (this.options.bottom === true ? this.parent.worldHeight : this.options.bottom))\n {\n this.parent.y = -(this.options.bottom === true ? this.parent.worldHeight : this.options.bottom)\n * this.parent.scale.y + this.parent.screenHeight;\n decelerate.y = 0;\n moved = true;\n }\n }\n }\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, original, type: 'clamp-y' });\n }\n }\n this.last.x = this.parent.x;\n this.last.y = this.parent.y;\n this.last.scaleX = this.parent.scale.x;\n this.last.scaleY = this.parent.scale.y;\n }\n\n public reset(): void\n {\n this.update();\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\n\n/**\n * Options for {@link ClampZoom}.\n *\n * Use either minimum width/height or minimum scale\n */\nexport interface IClampZoomOptions\n{\n /** Minimum width */\n minWidth?: number | null;\n\n /** Minimum height */\n minHeight?: number | null;\n\n /** Maximum width */\n maxWidth?: number | null;\n\n /** Maximum height */\n maxHeight?: number | null;\n\n /** Minimum scale */\n minScale?: number | null;\n\n /** Maximum scale */\n maxScale?: number | null;\n}\n\nconst DEFAULT_CLAMP_ZOOM_OPTIONS: Required = {\n minWidth: null,\n minHeight: null,\n maxWidth: null,\n maxHeight: null,\n minScale: null,\n maxScale: null\n};\n\n/**\n * Plugin to clamp the viewport's zoom to a specific range.\n *\n * @public\n */\nexport class ClampZoom extends Plugin\n{\n public readonly options: Required;\n\n /**\n * This is called by {@link Viewport.clampZoom}.\n */\n constructor(parent: Viewport, options = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_CLAMP_ZOOM_OPTIONS, options);\n\n this.clamp();\n }\n\n public resize(): void\n {\n this.clamp();\n }\n\n /** Clamp the viewport's zoom immediately. */\n public clamp(): void\n {\n if (this.paused)\n {\n return;\n }\n\n if (this.options.minWidth || this.options.minHeight || this.options.maxWidth || this.options.maxHeight)\n {\n let width = this.parent.worldScreenWidth;\n let height = this.parent.worldScreenHeight;\n\n if (this.options.minWidth !== null && width < this.options.minWidth)\n {\n const original = this.parent.scale.x;\n\n this.parent.fitWidth(this.options.minWidth, false, false, true);\n this.parent.scale.y *= this.parent.scale.x / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.maxWidth !== null && width > this.options.maxWidth)\n {\n const original = this.parent.scale.x;\n\n this.parent.fitWidth(this.options.maxWidth, false, false, true);\n this.parent.scale.y *= this.parent.scale.x / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.minHeight !== null && height < this.options.minHeight)\n {\n const original = this.parent.scale.y;\n\n this.parent.fitHeight(this.options.minHeight, false, false, true);\n this.parent.scale.x *= this.parent.scale.y / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.maxHeight !== null && height > this.options.maxHeight)\n {\n const original = this.parent.scale.y;\n\n this.parent.fitHeight(this.options.maxHeight, false, false, true);\n this.parent.scale.x *= this.parent.scale.y / original;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n }\n else\n {\n let scale = this.parent.scale.x;\n\n if (this.options.minScale !== null && scale < this.options.minScale)\n {\n scale = this.options.minScale;\n }\n if (this.options.maxScale !== null && scale > this.options.maxScale)\n {\n scale = this.options.maxScale;\n }\n if (scale !== this.parent.scale.x)\n {\n this.parent.scale.set(scale);\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n }\n }\n\n public reset(): void\n {\n this.clamp();\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Point } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\nexport interface IDecelerateOptions {\n /**\n * Percent to decelerate after movement. This should be between 0 and 1, exclusive.\n *\n * @default 0.95\n */\n friction?: number;\n\n /**\n * Percent to decelerate when past boundaries (only applicable when viewport.bounce() is active)\n *\n * @default 0.8\n */\n bounce?: number;\n\n /**\n * Minimum velocity before stopping/reversing acceleration\n *\n * @default 0.01\n */\n minSpeed?: number;\n}\n\n/** Viewport position snapshot that's saved by {@link DeceleratePlugin} to estimate panning velocity. */\nexport interface IDecelerateSnapshot {\n /** x-coordinate of the viewport. */\n x: number;\n\n /** y-coordinate of the viewport. */\n y: number;\n\n /** Time at which this snapshot was taken. */\n time: number;\n}\n\nconst DEFAULT_DECELERATE_OPTIONS: Required = {\n friction: 0.98,\n bounce: 0.8,\n minSpeed: 0.01\n};\n\n/**\n * Time period of decay (1 frame)\n *\n * @internal\n * @ignore\n */\nconst TP = 16;\n\n/**\n * Plugin to decelerate viewport velocity smoothly after panning ends.\n *\n * @public\n */\nexport class Decelerate extends Plugin\n{\n /** Options used to initialize this plugin. */\n public readonly options: Required;\n\n /**\n * x-component of the velocity of viewport provided by this plugin, at the current time.\n *\n * This is measured in px/frame, where a frame is normalized to 16 milliseconds.\n */\n public x!: number | null;\n\n /**\n * y-component of the velocity of the viewport provided by this plugin, at the current time.\n *\n * This is measured in px/frame, where a frame is normalized to 16 milliseconds.\n */\n public y!: number | null;\n\n /**\n * The decay factor for the x-component of the viewport.\n *\n * The viewport's velocity decreased by this amount each 16 milliseconds.\n */\n public percentChangeX!: number;\n\n /**\n * The decay factor for the y-component of the viewport.\n *\n * The viewport's velocity decreased by this amount each 16 milliseconds.\n */\n public percentChangeY!: number;\n\n /** Saved list of recent viewport position snapshots, to estimate velocity. */\n protected saved: Array;\n\n /** The time since the user released panning of the viewport. */\n protected timeSinceRelease: number;\n\n /**\n * This is called by {@link Viewport.decelerate}.\n */\n constructor(parent: Viewport, options: IDecelerateOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_DECELERATE_OPTIONS, options);\n this.saved = [];\n this.timeSinceRelease = 0;\n\n this.reset();\n this.parent.on('moved', (data) => this.moved(data));\n }\n\n public down(): boolean\n {\n this.saved = [];\n this.x = this.y = null;\n\n return false;\n }\n\n public isActive(): boolean\n {\n return !!(this.x || this.y);\n }\n\n public move(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n const count = this.parent.input.count();\n\n if (count === 1 || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n this.saved.push({ x: this.parent.x, y: this.parent.y, time: performance.now() });\n\n if (this.saved.length > 60)\n {\n this.saved.splice(0, 30);\n }\n }\n\n // Silently recording viewport positions\n return false;\n }\n\n /** Listener to viewport's \"moved\" event. */\n protected moved(data: { type: 'clamp-x' | 'clamp-y'; original: Point }): void\n {\n if (this.saved.length)\n {\n const last = this.saved[this.saved.length - 1];\n\n if (data.type === 'clamp-x')\n {\n if (last.x === data.original.x)\n {\n last.x = this.parent.x;\n }\n }\n else if (data.type === 'clamp-y')\n {\n if (last.y === data.original.y)\n {\n last.y = this.parent.y;\n }\n }\n }\n }\n\n public up(): boolean\n {\n if (this.parent.input.count() === 0 && this.saved.length)\n {\n const now = performance.now();\n\n for (const save of this.saved)\n {\n if (save.time >= now - 100)\n {\n const time = now - save.time;\n\n this.x = (this.parent.x - save.x) / time;\n this.y = (this.parent.y - save.y) / time;\n this.percentChangeX = this.percentChangeY = this.options.friction;\n this.timeSinceRelease = 0;\n break;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Manually activate deceleration, starting from the (x, y) velocity components passed in the options.\n *\n * @param {object} options\n * @param {number} [options.x] - Specify x-component of initial velocity.\n * @param {number} [options.y] - Specify y-component of initial velocity.\n */\n public activate(options: { x?: number; y?: number; }): void\n {\n options = options || {};\n\n if (typeof options.x !== 'undefined')\n {\n this.x = options.x;\n this.percentChangeX = this.options.friction;\n }\n if (typeof options.y !== 'undefined')\n {\n this.y = options.y;\n this.percentChangeY = this.options.friction;\n }\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n /*\n * See https://github.com/davidfig/pixi-viewport/issues/271 for math.\n *\n * The viewport velocity (this.x, this.y) decays exponentially by the the decay factor\n * (this.percentChangeX, this.percentChangeY) each frame. This velocity function is integrated\n * to calculate the displacement.\n */\n\n const moved = this.x || this.y;\n\n const ti = this.timeSinceRelease;\n const tf = this.timeSinceRelease + elapsed;\n\n if (this.x)\n {\n const k = this.percentChangeX;\n const lnk = Math.log(k);\n\n // Apply velocity delta on the viewport x-coordinate.\n this.parent.x += ((this.x * TP) / lnk) * (Math.pow(k, tf / TP) - Math.pow(k, ti / TP));\n\n // Apply decay on x-component of velocity\n this.x *= Math.pow(this.percentChangeX, elapsed / TP);\n }\n if (this.y)\n {\n const k = this.percentChangeY;\n const lnk = Math.log(k);\n\n // Apply velocity delta on the viewport y-coordinate.\n this.parent.y += ((this.y * TP) / lnk) * (Math.pow(k, tf / TP) - Math.pow(k, ti / TP));\n\n // Apply decay on y-component of velocity\n this.y *= Math.pow(this.percentChangeY, elapsed / TP);\n }\n\n this.timeSinceRelease += elapsed;\n\n // End decelerate velocity once it goes under a certain amount of precision.\n if (Math.abs(this.x || 0) < this.options.minSpeed)\n {\n this.x = 0;\n }\n if (Math.abs(this.y || 0) < this.options.minSpeed)\n {\n this.y = 0;\n }\n\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, type: 'decelerate' });\n }\n }\n\n public reset(): void\n {\n this.x = this.y = null;\n }\n}\n","import { Point } from '@pixi/math';\nimport { Plugin } from './Plugin';\n\nimport type { Decelerate } from './Decelerate';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { IPointData } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Drag}. */\nexport interface IDragOptions {\n /**\n * direction to drag\n *\n * @default \"all\"\n */\n direction?: string;\n\n /**\n * whether click to drag is active\n *\n * @default true\n */\n pressDrag?: boolean;\n\n /**\n * Use wheel to scroll in direction (unless wheel plugin is active)\n *\n * @default true\n */\n wheel?: boolean;\n\n /**\n * number of pixels to scroll with each wheel spin\n *\n * @default 1\n */\n wheelScroll?: number;\n\n /**\n * reverse the direction of the wheel scroll\n *\n * @default false\n */\n reverse?: boolean;\n\n /**\n * clamp wheel(to avoid weird bounce with mouse wheel). Can be 'x' or 'y' or `true`.\n *\n * @default false\n */\n clampWheel?: boolean | string;\n\n /**\n * where to place world if too small for screen\n *\n * @default \"center\"\n */\n underflow?: string;\n\n /**\n * factor to multiply drag to increase the speed of movement\n *\n * @default 1\n */\n factor?: number;\n\n /**\n * Changes which mouse buttons trigger drag.\n *\n * Use: 'all', 'left', right' 'middle', or some combination, like, 'middle-right'; you may want to set\n * `viewport.options.disableOnContextMenu` if you want to use right-click dragging.\n *\n * @default \"all\"\n */\n mouseButtons?: 'all' | string;\n\n /**\n * Array containing {@link key|https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code} codes of\n * keys that can be pressed for the drag to be triggered, e.g.: ['ShiftLeft', 'ShiftRight'}.\n *\n * @default null\n */\n keyToPress?: string[] | null;\n\n /**\n * Ignore keyToPress for touch events.\n *\n * @default false\n */\n ignoreKeyToPressOnTouch?: boolean;\n\n /**\n * Scaling factor for non-DOM_DELTA_PIXEL scrolling events.\n *\n * @default 20\n */\n lineHeight?: number;\n}\n\nconst DEFAULT_DRAG_OPTIONS: Required = {\n direction: 'all',\n pressDrag: true,\n wheel: true,\n wheelScroll: 1,\n reverse: false,\n clampWheel: false,\n underflow: 'center',\n factor: 1,\n mouseButtons: 'all',\n keyToPress: null,\n ignoreKeyToPressOnTouch: false,\n lineHeight: 20,\n};\n\n/**\n * Plugin to enable panning/dragging of the viewport to move around.\n *\n * @public\n */\nexport class Drag extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Readonly>;\n\n /** Flags when viewport is moving. */\n protected moved: boolean;\n\n /** Factor to apply from {@link IDecelerateOptions}'s reverse. */\n protected reverse: 1 | -1;\n\n /** Holds whether dragging is enabled along the x-axis. */\n protected xDirection: boolean\n\n /** Holds whether dragging is enabled along the y-axis. */\n protected yDirection: boolean;\n\n /** Flags whether the keys required to drag are pressed currently. */\n protected keyIsPressed: boolean;\n\n /** Holds whether the left, center, and right buttons are required to pan. */\n protected mouse!: [boolean, boolean, boolean]\n\n /** Underflow factor along x-axis */\n protected underflowX!: -1 | 0 | 1;\n\n /** Underflow factor along y-axis */\n protected underflowY!: -1 | 0 | 1;\n\n /** Last pointer position while panning. */\n protected last?: IPointData | null\n\n /** The ID of the pointer currently panning the viewport. */\n protected current?: number;\n\n /**\n * This is called by {@link Viewport.drag}.\n */\n constructor(parent: Viewport, options = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_DRAG_OPTIONS, options);\n this.moved = false;\n this.reverse = this.options.reverse ? 1 : -1;\n this.xDirection = !this.options.direction || this.options.direction === 'all' || this.options.direction === 'x';\n this.yDirection = !this.options.direction || this.options.direction === 'all' || this.options.direction === 'y';\n this.keyIsPressed = false;\n\n this.parseUnderflow();\n this.mouseButtons(this.options.mouseButtons);\n\n if (this.options.keyToPress)\n {\n this.handleKeyPresses(this.options.keyToPress);\n }\n }\n\n /**\n * Handles keypress events and set the keyIsPressed boolean accordingly\n *\n * @param {array} codes - key codes that can be used to trigger drag event\n */\n protected handleKeyPresses(codes: string[]): void\n {\n window.addEventListener('keydown', (e) =>\n {\n if (codes.includes(e.code))\n { this.keyIsPressed = true; }\n });\n\n window.addEventListener('keyup', (e) =>\n {\n if (codes.includes(e.code))\n { this.keyIsPressed = false; }\n });\n }\n\n /**\n * initialize mousebuttons array\n * @param {string} buttons\n */\n protected mouseButtons(buttons: string): void\n {\n if (!buttons || buttons === 'all')\n {\n this.mouse = [true, true, true];\n }\n else\n {\n this.mouse = [\n buttons.indexOf('left') !== -1,\n buttons.indexOf('middle') !== -1,\n buttons.indexOf('right') !== -1\n ];\n }\n }\n\n protected parseUnderflow(): void\n {\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'center')\n {\n this.underflowX = 0;\n this.underflowY = 0;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n }\n }\n\n /**\n * @param {PIXI.InteractionEvent} event\n * @returns {boolean}\n */\n protected checkButtons(event: InteractionEvent): boolean\n {\n const isMouse = event.data.pointerType === 'mouse';\n const count = this.parent.input.count();\n\n if ((count === 1) || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n if (!isMouse || this.mouse[event.data.button])\n {\n return true;\n }\n }\n\n return false;\n }\n\n /**\n * @param {PIXI.InteractionEvent} event\n * @returns {boolean}\n */\n protected checkKeyPress(event: InteractionEvent): boolean\n {\n return (!this.options.keyToPress\n || this.keyIsPressed\n || (this.options.ignoreKeyToPressOnTouch && event.data.pointerType === 'touch'));\n }\n\n public down(event: InteractionEvent): boolean\n {\n if (this.paused || !this.options.pressDrag)\n {\n return false;\n }\n if (this.checkButtons(event) && this.checkKeyPress(event))\n {\n this.last = { x: event.data.global.x, y: event.data.global.y };\n this.current = event.data.pointerId;\n\n return true;\n }\n this.last = null;\n\n return false;\n }\n\n get active(): boolean\n {\n return this.moved;\n }\n\n public move(event: InteractionEvent): boolean\n {\n if (this.paused || !this.options.pressDrag)\n {\n return false;\n }\n if (this.last && this.current === event.data.pointerId)\n {\n const x = event.data.global.x;\n const y = event.data.global.y;\n const count = this.parent.input.count();\n\n if (count === 1 || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n const distX = x - this.last.x;\n const distY = y - this.last.y;\n\n if (this.moved\n || ((this.xDirection && this.parent.input.checkThreshold(distX))\n || (this.yDirection && this.parent.input.checkThreshold(distY))))\n {\n const newPoint = { x, y };\n\n if (this.xDirection)\n {\n this.parent.x += (newPoint.x - this.last.x) * this.options.factor;\n }\n if (this.yDirection)\n {\n this.parent.y += (newPoint.y - this.last.y) * this.options.factor;\n }\n this.last = newPoint;\n if (!this.moved)\n {\n this.parent.emit('drag-start', {\n event,\n screen: new Point(this.last.x, this.last.y),\n world: this.parent.toWorld(new Point(this.last.x, this.last.y)),\n viewport: this.parent\n });\n }\n this.moved = true;\n this.parent.emit('moved', { viewport: this.parent, type: 'drag' });\n\n return true;\n }\n }\n else\n {\n this.moved = false;\n }\n }\n\n return false;\n }\n\n public up(event: InteractionEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n const touches = this.parent.input.touches;\n\n if (touches.length === 1)\n {\n const pointer = touches[0];\n\n if (pointer.last)\n {\n this.last = { x: pointer.last.x, y: pointer.last.y };\n this.current = pointer.id;\n }\n this.moved = false;\n\n return true;\n }\n else if (this.last)\n {\n if (this.moved)\n {\n const screen = new Point(this.last.x, this.last.y);\n\n this.parent.emit('drag-end', {\n event, screen,\n world: this.parent.toWorld(screen),\n viewport: this.parent,\n });\n this.last = null;\n this.moved = false;\n\n return true;\n }\n }\n\n return false;\n }\n\n public wheel(event: WheelEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n if (this.options.wheel)\n {\n const wheel = this.parent.plugins.get('wheel', true);\n\n if (!wheel)\n {\n const step = event.deltaMode ? this.options.lineHeight : 1;\n\n if (this.xDirection)\n {\n this.parent.x += event.deltaX * step * this.options.wheelScroll * this.reverse;\n }\n if (this.yDirection)\n {\n this.parent.y += event.deltaY * step * this.options.wheelScroll * this.reverse;\n }\n if (this.options.clampWheel)\n {\n this.clamp();\n }\n this.parent.emit('wheel-scroll', this.parent);\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n if (!this.parent.options.passiveWheel)\n {\n event.preventDefault();\n }\n\n return true;\n }\n }\n\n return false;\n }\n\n public resume()\n {\n this.last = null;\n this.paused = false;\n }\n\n public clamp()\n {\n const decelerate: Partial = this.parent.plugins.get('decelerate', true) || {};\n\n if (this.options.clampWheel !== 'y')\n {\n if (this.parent.screenWorldWidth < this.parent.screenWidth)\n {\n switch (this.underflowX)\n {\n case -1:\n this.parent.x = 0;\n break;\n case 1:\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth);\n break;\n default:\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n }\n }\n else\n if (this.parent.left < 0)\n {\n this.parent.x = 0;\n decelerate.x = 0;\n }\n else if (this.parent.right > this.parent.worldWidth)\n {\n this.parent.x = -this.parent.worldWidth * this.parent.scale.x + this.parent.screenWidth;\n decelerate.x = 0;\n }\n }\n if (this.options.clampWheel !== 'x')\n {\n if (this.parent.screenWorldHeight < this.parent.screenHeight)\n {\n switch (this.underflowY)\n {\n case -1:\n this.parent.y = 0;\n break;\n case 1:\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n break;\n default:\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n }\n }\n else\n {\n if (this.parent.top < 0)\n {\n this.parent.y = 0;\n decelerate.y = 0;\n }\n if (this.parent.bottom > this.parent.worldHeight)\n {\n this.parent.y = -this.parent.worldHeight * this.parent.scale.y + this.parent.screenHeight;\n decelerate.y = 0;\n }\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { DisplayObject } from '@pixi/display';\nimport type { IPointData } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Follow}. */\nexport interface IFollowOptions\n{\n /**\n * Speed to follow in px/frame (0 = teleport to location)\n *\n * @default 9\n */\n speed?: number;\n\n /**\n * Set acceleration to accelerate and decelerate at this rate; speed cannot be 0 to use acceleration\n *\n * @default null\n */\n acceleration?: number | null;\n\n /**\n * Radius (in world coordinates) of center circle where movement is allowed without moving the viewport\n *\n * @default null\n */\n radius?: number | null;\n}\n\nconst DEFAULT_FOLLOW_OPTIONS: Required = {\n speed: 0,\n acceleration: null,\n radius: null\n};\n\n/**\n * Plugin to follow a display-object.\n *\n * @see Viewport.follow\n * @public\n */\nexport class Follow extends Plugin\n{\n /** The options used to initialize this plugin. */\n public readonly options: Required;\n\n /** The target this plugin will make the viewport follow. */\n public target: DisplayObject;\n\n /** The velocity provided the viewport by following, at the current time. */\n protected velocity: IPointData;\n\n /**\n * This is called by {@link Viewport.follow}.\n *\n * @param parent\n * @param target - target to follow\n * @param options\n */\n constructor(parent: Viewport, target: DisplayObject, options: IFollowOptions = {})\n {\n super(parent);\n\n this.target = target;\n this.options = Object.assign({}, DEFAULT_FOLLOW_OPTIONS, options);\n this.velocity = { x: 0, y: 0 };\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n const center = this.parent.center;\n let toX = this.target.x;\n let toY = this.target.y;\n\n if (this.options.radius)\n {\n const distance = Math.sqrt(Math.pow(this.target.y - center.y, 2) + Math.pow(this.target.x - center.x, 2));\n\n if (distance > this.options.radius)\n {\n const angle = Math.atan2(this.target.y - center.y, this.target.x - center.x);\n\n toX = this.target.x - (Math.cos(angle) * this.options.radius);\n toY = this.target.y - (Math.sin(angle) * this.options.radius);\n }\n else\n {\n return;\n }\n }\n\n const deltaX = toX - center.x;\n const deltaY = toY - center.y;\n\n if (deltaX || deltaY)\n {\n if (this.options.speed)\n {\n if (this.options.acceleration)\n {\n const angle = Math.atan2(toY - center.y, toX - center.x);\n const distance = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));\n\n if (distance)\n {\n const decelerationDistance = (Math.pow(this.velocity.x, 2) + Math.pow(this.velocity.y, 2)) / (2 * this.options.acceleration);\n\n if (distance > decelerationDistance)\n {\n this.velocity = {\n x: Math.min(this.velocity.x + this.options.acceleration * elapsed, this.options.speed),\n y: Math.min(this.velocity.y + this.options.acceleration * elapsed, this.options.speed)\n };\n }\n else\n {\n this.velocity = {\n x: Math.max(this.velocity.x - this.options.acceleration * this.options.speed, 0),\n y: Math.max(this.velocity.y - this.options.acceleration * this.options.speed, 0)\n };\n }\n const changeX = Math.cos(angle) * this.velocity.x;\n const changeY = Math.sin(angle) * this.velocity.y;\n const x = Math.abs(changeX) > Math.abs(deltaX) ? toX : center.x + changeX;\n const y = Math.abs(changeY) > Math.abs(deltaY) ? toY : center.y + changeY;\n\n this.parent.moveCenter(x, y);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n else\n {\n const angle = Math.atan2(toY - center.y, toX - center.x);\n const changeX = Math.cos(angle) * this.options.speed;\n const changeY = Math.sin(angle) * this.options.speed;\n const x = Math.abs(changeX) > Math.abs(deltaX) ? toX : center.x + changeX;\n const y = Math.abs(changeY) > Math.abs(deltaY) ? toY : center.y + changeY;\n\n this.parent.moveCenter(x, y);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n else\n {\n this.parent.moveCenter(toX, toY);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\nimport type { InteractionEvent } from '@pixi/interaction';\n\n/** Insets for mouse edges scrolling regions */\nexport interface IMouseEdgesInsets {\n /** Distance from center of screen in screen pixels */\n radius?: number | null;\n\n /** Distance from all sides in screen pixels */\n distance?: number | null;\n\n /** Alternatively, set top distance (leave unset for no top scroll) */\n top?: number | null;\n\n /** Alternatively, set bottom distance (leave unset for no top scroll) */\n bottom?: number | null;\n\n /** Alternatively, set left distance (leave unset for no top scroll) */\n left?: number | null;\n\n /** Alternatively, set right distance (leave unset for no top scroll) */\n right?: number | null;\n}\n\n/** Options for {@link MouseEdges}. */\nexport interface IMouseEdgesOptions extends IMouseEdgesInsets {\n /** Speed in pixels/frame to scroll viewport */\n speed?: number;\n\n /** Reverse direction of scroll */\n reverse?: boolean;\n\n /** Don't use decelerate plugin even if it's installed */\n noDecelerate?: boolean;\n\n /**\n * If using radius, use linear movement (+/- 1, +/- 1) instead of angled movement.\n *\n * (Math.cos(angle from center), Math.sin(angle from center))\n */\n linear?: boolean;\n\n /** Allows plugin to continue working even when there's a `mousedown` event. */\n allowButtons?: boolean;\n}\n\nconst MOUSE_EDGES_OPTIONS: Required = {\n radius: null,\n distance: null,\n top: null,\n bottom: null,\n left: null,\n right: null,\n speed: 8,\n reverse: false,\n noDecelerate: false,\n linear: false,\n allowButtons: false,\n};\n\n/**\n * Scroll viewport when mouse hovers near one of the edges.\n *\n * @event mouse-edge-start(Viewport) emitted when mouse-edge starts\n * @event mouse-edge-end(Viewport) emitted when mouse-edge ends\n */\nexport class MouseEdges extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Readonly>;\n\n /** Factor from reverse option. */\n protected readonly reverse: -1 | 1;\n\n /** Radius squared */\n protected readonly radiusSquared: number | null;\n\n /** Scroll region size on the left side. */\n protected left!: number | null;\n\n /** Scroll region size on the top size. */\n protected top!: number | null;\n\n /** Scroll region size on the right side. */\n protected right!: number | null;\n\n /** Scroll region size on the bottom side. */\n protected bottom!: number | null;\n\n protected horizontal?: number | null;\n\n protected vertical?: number | null;\n\n /**\n * This is called by {@link Viewport.mouseEdges}.\n */\n constructor(parent: Viewport, options: IMouseEdgesOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, MOUSE_EDGES_OPTIONS, options);\n this.reverse = this.options.reverse ? 1 : -1;\n this.radiusSquared = typeof this.options.radius === 'number' ? Math.pow(this.options.radius, 2) : null;\n\n this.resize();\n }\n\n public resize(): void\n {\n const distance = this.options.distance;\n\n if (distance !== null)\n {\n this.left = distance;\n this.top = distance;\n this.right = this.parent.worldScreenWidth - distance;\n this.bottom = this.parent.worldScreenHeight - distance;\n }\n else if (!this.options.radius)\n {\n this.left = this.options.left;\n this.top = this.options.top;\n this.right = this.options.right === null ? null : this.parent.worldScreenWidth - this.options.right;\n this.bottom = this.options.bottom === null ? null : this.parent.worldScreenHeight - this.options.bottom;\n }\n }\n\n public down(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if (!this.options.allowButtons)\n {\n this.horizontal = this.vertical = null;\n }\n\n return false;\n }\n\n public move(event: InteractionEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if ((event.data.pointerType !== 'mouse' && event.data.identifier !== 1)\n || (!this.options.allowButtons && event.data.buttons !== 0))\n {\n return false;\n }\n\n const x = event.data.global.x;\n const y = event.data.global.y;\n\n if (this.radiusSquared)\n {\n const center = this.parent.toScreen(this.parent.center);\n const distance = Math.pow(center.x - x, 2) + Math.pow(center.y - y, 2);\n\n if (distance >= this.radiusSquared)\n {\n const angle = Math.atan2(center.y - y, center.x - x);\n\n if (this.options.linear)\n {\n this.horizontal = Math.round(Math.cos(angle)) * this.options.speed * this.reverse * (60 / 1000);\n this.vertical = Math.round(Math.sin(angle)) * this.options.speed * this.reverse * (60 / 1000);\n }\n else\n {\n this.horizontal = Math.cos(angle) * this.options.speed * this.reverse * (60 / 1000);\n this.vertical = Math.sin(angle) * this.options.speed * this.reverse * (60 / 1000);\n }\n }\n else\n {\n if (this.horizontal)\n {\n this.decelerateHorizontal();\n }\n if (this.vertical)\n {\n this.decelerateVertical();\n }\n\n this.horizontal = this.vertical = 0;\n }\n }\n else\n {\n if (this.left !== null && x < this.left)\n {\n this.horizontal = Number(this.reverse) * this.options.speed * (60 / 1000);\n }\n else if (this.right !== null && x > this.right)\n {\n this.horizontal = -1 * this.reverse * this.options.speed * (60 / 1000);\n }\n else\n {\n this.decelerateHorizontal();\n this.horizontal = 0;\n }\n if (this.top !== null && y < this.top)\n {\n this.vertical = Number(this.reverse) * this.options.speed * (60 / 1000);\n }\n else if (this.bottom !== null && y > this.bottom)\n {\n this.vertical = -1 * this.reverse * this.options.speed * (60 / 1000);\n }\n else\n {\n this.decelerateVertical();\n this.vertical = 0;\n }\n }\n\n return false;\n }\n\n private decelerateHorizontal(): void\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (this.horizontal && decelerate && !this.options.noDecelerate)\n {\n decelerate.activate({ x: (this.horizontal * this.options.speed * this.reverse) / (1000 / 60) });\n }\n }\n\n private decelerateVertical(): void\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (this.vertical && decelerate && !this.options.noDecelerate)\n {\n decelerate.activate({ y: (this.vertical * this.options.speed * this.reverse) / (1000 / 60) });\n }\n }\n\n public up(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if (this.horizontal)\n {\n this.decelerateHorizontal();\n }\n if (this.vertical)\n {\n this.decelerateVertical();\n }\n this.horizontal = this.vertical = null;\n\n return false;\n }\n\n public update(): void\n {\n if (this.paused)\n {\n return;\n }\n\n if (this.horizontal || this.vertical)\n {\n const center = this.parent.center;\n\n if (this.horizontal)\n {\n center.x += this.horizontal * this.options.speed;\n }\n if (this.vertical)\n {\n center.y += this.vertical * this.options.speed;\n }\n\n this.parent.moveCenter(center);\n this.parent.emit('moved', { viewport: this.parent, type: 'mouse-edges' });\n }\n }\n}\n","import { Plugin } from './Plugin';\nimport { Point } from '@pixi/math';\n\nimport type { IPointData } from '@pixi/math';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { IViewportTouch } from '../InputManager';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Pinch}. */\nexport interface IPinchOptions\n{\n /** Disable two-finger dragging. */\n noDrag?: boolean;\n\n /**\n * Percent to modify pinch speed.\n *\n * @default 1\n */\n percent?: number;\n\n /**\n * Factor to multiply two-finger drag to increase speed of movement\n *\n * @default 1\n */\n factor?: number;\n\n /** Place this point at center during zoom instead of center of two fingers */\n center?: Point | null;\n\n /** Axis to zoom */\n axis?: 'all' | 'x' | 'y';\n}\n\nconst DEFAULT_PINCH_OPTIONS: Required = {\n noDrag: false,\n percent: 1,\n center: null,\n factor: 1,\n axis: 'all',\n};\n\n/**\n * Plugin for enabling two-finger pinching (or dragging).\n *\n * @public\n */\nexport class Pinch extends Plugin\n{\n /** Options used to initialize this plugin. */\n public readonly options: Required;\n\n /** Flags whether this plugin is active, i.e. a pointer is down on the viewport. */\n public active = false;\n\n /** Flags whether the viewport is being pinched. */\n public pinching = false;\n\n protected moved = false;\n protected lastCenter?: IPointData | null;\n\n /**\n * This is called by {@link Viewport.pinch}.\n */\n constructor(parent: Viewport, options: IPinchOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_PINCH_OPTIONS, options);\n }\n\n public down(): boolean\n {\n if (this.parent.input.count() >= 2)\n {\n this.active = true;\n return true;\n }\n\n return false;\n }\n\n public isAxisX(): boolean\n {\n return ['all', 'x'].includes(this.options.axis);\n }\n\n public isAxisY(): boolean\n {\n return ['all', 'y'].includes(this.options.axis);\n }\n\n public move(e: InteractionEvent): boolean\n {\n if (this.paused || !this.active)\n {\n return false;\n }\n\n const x = e.data.global.x;\n const y = e.data.global.y;\n\n const pointers = this.parent.input.touches;\n\n if (pointers.length >= 2)\n {\n const first = pointers[0] as IViewportTouch;\n const second = pointers[1] as IViewportTouch;\n const last = (first.last && second.last)\n ? Math.sqrt(Math.pow(second.last.x - first.last.x, 2) + Math.pow(second.last.y - first.last.y, 2))\n : null;\n\n if (first.id === e.data.pointerId)\n {\n first.last = { x, y, data: e.data } as IPointData;\n }\n else if (second.id === e.data.pointerId)\n {\n second.last = { x, y, data: e.data } as IPointData;\n }\n if (last)\n {\n let oldPoint: IPointData | undefined;\n\n const point = {\n x: (first.last as IPointData).x +\n ((second.last as IPointData).x - (first.last as IPointData).x) / 2,\n y: (first.last as IPointData).y +\n ((second.last as IPointData).y - (first.last as IPointData).y) / 2,\n };\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point);\n }\n let dist = Math.sqrt(Math.pow(\n (second.last as IPointData).x - (first.last as IPointData).x, 2) +\n Math.pow((second.last as IPointData).y - (first.last as IPointData).y, 2));\n\n dist = dist === 0 ? dist = 0.0000000001 : dist;\n\n const change = (1 - last / dist) * this.options.percent\n * (this.isAxisX() ? this.parent.scale.x : this.parent.scale.y);\n\n if (this.isAxisX())\n {\n this.parent.scale.x += change;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y += change;\n }\n\n this.parent.emit('zoomed', { viewport: this.parent, type: 'pinch', center: point });\n\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += (point.x - newPoint.x) * this.options.factor;\n this.parent.y += (point.y - newPoint.y) * this.options.factor;\n this.parent.emit('moved', { viewport: this.parent, type: 'pinch' });\n }\n if (!this.options.noDrag && this.lastCenter)\n {\n this.parent.x += (point.x - this.lastCenter.x) * this.options.factor;\n this.parent.y += (point.y - this.lastCenter.y) * this.options.factor;\n this.parent.emit('moved', { viewport: this.parent, type: 'pinch' });\n }\n\n this.lastCenter = point;\n this.moved = true;\n }\n else if (!this.pinching)\n {\n this.parent.emit('pinch-start', this.parent);\n this.pinching = true;\n }\n\n return true;\n }\n\n return false;\n }\n\n public up(): boolean\n {\n if (this.pinching)\n {\n if (this.parent.input.touches.length <= 1)\n {\n this.active = false;\n this.lastCenter = null;\n this.pinching = false;\n this.moved = false;\n this.parent.emit('pinch-end', this.parent);\n\n return true;\n }\n }\n\n return false;\n }\n}\n","import { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Viewport } from '../Viewport';\n\nexport interface ISnapOptions\n{\n /** snap to the top-left of viewport instead of center */\n topLeft?: boolean;\n\n /**\n * Friction/frame to apply if decelerate is active\n *\n * @default 0.8\n */\n friction?: number;\n\n /**\n * @default 1000\n */\n time?: number;\n\n /** Easing function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /**\n * Pause snapping with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Removes this plugin after snapping is complete\n *\n * @default false\n */\n removeOnComplete?: boolean;\n\n /**\n * Removes this plugin if interrupted by any user input\n *\n * @default false\n */\n removeOnInterrupt?: boolean;\n\n /**\n * Starts the snap immediately regardless of whether the viewport is at the desired location\n *\n * @default false\n */\n forceStart?: boolean;\n}\n\nconst DEFAULT_SNAP_OPTIONS: Required = {\n topLeft: false,\n friction: 0.8,\n time: 1000,\n ease: 'easeInOutSine',\n interrupt: true,\n removeOnComplete: false,\n removeOnInterrupt: false,\n forceStart: false\n};\n\n/**\n * @event snap-start(Viewport) emitted each time a snap animation starts\n * @event snap-restart(Viewport) emitted each time a snap resets because of a change in viewport size\n * @event snap-end(Viewport) emitted each time snap reaches its target\n * @event snap-remove(Viewport) emitted if snap plugin is removed\n */\nexport class Snap extends Plugin\n{\n public readonly options: Required;\n public ease?: any;\n public x: number;\n public y: number;\n\n protected percent?: number;\n protected snapping?: { time: number } | null;\n protected deltaX?: number;\n protected deltaY?: number;\n protected startX?: number;\n protected startY?: number;\n\n /**\n * This is called by {@link Viewport.snap}.\n */\n constructor(parent: Viewport, x: number, y: number, options: ISnapOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_SNAP_OPTIONS, options);\n this.ease = ease(options.ease, 'easeInOutSine');\n this.x = x;\n this.y = y;\n\n if (this.options.forceStart)\n {\n this.snapStart();\n }\n }\n\n public snapStart(): void\n {\n this.percent = 0;\n this.snapping = { time: 0 };\n const current = this.options.topLeft ? this.parent.corner : this.parent.center;\n\n this.deltaX = this.x - current.x;\n this.deltaY = this.y - current.y;\n this.startX = current.x;\n this.startY = current.y;\n this.parent.emit('snap-start', this.parent);\n }\n\n public wheel(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap');\n }\n\n return false;\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap');\n }\n else if (this.options.interrupt)\n {\n this.snapping = null;\n }\n\n return false;\n }\n\n public up(): boolean\n {\n if (this.parent.input.count() === 0)\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (decelerate && (decelerate.x || decelerate.y))\n {\n decelerate.percentChangeX = decelerate.percentChangeY = this.options.friction;\n }\n }\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n if (this.options.interrupt && this.parent.input.count() !== 0)\n {\n return;\n }\n if (!this.snapping)\n {\n const current = this.options.topLeft ? this.parent.corner : this.parent.center;\n\n if (current.x !== this.x || current.y !== this.y)\n {\n this.snapStart();\n }\n }\n else\n {\n const snapping = this.snapping;\n\n snapping.time += elapsed;\n let finished;\n let x;\n let y;\n\n const startX = this.startX as number;\n const startY = this.startY as number;\n const deltaX = this.deltaX as number;\n const deltaY = this.deltaY as number;\n\n if (snapping.time > this.options.time)\n {\n finished = true;\n x = startX + deltaX;\n y = startY + deltaY;\n }\n else\n {\n const percent = this.ease(snapping.time, 0, 1, this.options.time);\n\n x = startX + (deltaX * percent);\n y = startY + (deltaY * percent);\n }\n if (this.options.topLeft)\n {\n this.parent.moveCorner(x, y);\n }\n else\n {\n this.parent.moveCenter(x, y);\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'snap' });\n\n if (finished)\n {\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap');\n }\n this.parent.emit('snap-end', this.parent);\n this.snapping = null;\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Point } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link SnapZoom}. */\nexport interface ISnapZoomOptions\n{\n /** the desired width to snap (to maintain aspect ratio, choose only width or height) */\n width?: number;\n\n /** the desired height to snap (to maintain aspect ratio, choose only width or height) */\n height?: number;\n\n /**\n * time for snapping in ms\n *\n * @default 1000\n */\n time?: number;\n\n /** ease function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /** Place this point at center during zoom instead of center of the viewport */\n center?: Point | null;\n\n /**\n * Pause snapping with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Removes this plugin after snapping is complete\n *\n * @default false\n */\n removeOnComplete?: boolean;\n\n /**\n * Removes this plugin if interrupted by any user input\n *\n * @default false\n */\n removeOnInterrupt?: boolean;\n\n /**\n * Starts the snap immediately regardless of whether the viewport is at the desired zoom\n *\n * @default false\n */\n forceStart?: boolean;\n\n /**\n * Zoom but do not move\n *\n * @default false\n */\n noMove?: boolean;\n}\n\nconst DEFAULT_SNAP_ZOOM_OPTIONS: Required = {\n width: 0,\n height: 0,\n time: 1000,\n ease: 'easeInOutSine',\n center: null,\n interrupt: true,\n removeOnComplete: false,\n removeOnInterrupt: false,\n forceStart: false,\n noMove: false\n};\n\n/**\n * @event snap-zoom-start(Viewport) emitted each time a fit animation starts\n * @event snap-zoom-end(Viewport) emitted each time fit reaches its target\n * @event snap-zoom-end(Viewport) emitted each time fit reaches its target\n */\nexport class SnapZoom extends Plugin\n{\n public readonly options: Required;\n\n protected ease: any;\n protected xScale: number;\n protected yScale: number;\n protected xIndependent: boolean;\n protected yIndependent: boolean;\n protected snapping?: {\n time: number;\n startX: number;\n startY: number;\n deltaX: number;\n deltaY: number;\n } | null;\n\n /**\n * This is called by {@link Viewport.snapZoom}.\n */\n constructor(parent: Viewport, options: ISnapZoomOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_SNAP_ZOOM_OPTIONS, options);\n this.ease = ease(this.options.ease);\n\n // Assign defaults for typescript.\n this.xIndependent = false;\n this.yIndependent = false;\n this.xScale = 0;\n this.yScale = 0;\n\n if (this.options.width > 0)\n {\n this.xScale = parent.screenWidth / this.options.width;\n this.xIndependent = true;\n }\n if (this.options.height > 0)\n {\n this.yScale = parent.screenHeight / this.options.height;\n this.yIndependent = true;\n }\n\n this.xScale = this.xIndependent ? (this.xScale as number) : (this.yScale as number);\n this.yScale = this.yIndependent ? (this.yScale as number) : this.xScale;\n\n if (this.options.time === 0)\n {\n // TODO: Fix this\n // @ts-expect-error todo\n parent.container.scale.x = this.xScale;\n\n // TODO: Fix this\n // @ts-expect-error todo\n parent.container.scale.y = this.yScale;\n\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n }\n else if (options.forceStart)\n {\n this.createSnapping();\n }\n }\n\n private createSnapping(): void\n {\n const startWorldScreenWidth = this.parent.worldScreenWidth;\n const startWorldScreenHeight = this.parent.worldScreenHeight;\n const endWorldScreenWidth = this.parent.screenWidth / this.xScale;\n const endWorldScreenHeight = this.parent.screenHeight / this.yScale;\n\n this.snapping = {\n time: 0,\n startX: startWorldScreenWidth,\n startY: startWorldScreenHeight,\n deltaX: endWorldScreenWidth - startWorldScreenWidth,\n deltaY: endWorldScreenHeight - startWorldScreenHeight\n };\n\n this.parent.emit('snap-zoom-start', this.parent);\n }\n\n public resize(): void\n {\n this.snapping = null;\n\n if (this.options.width > 0)\n {\n this.xScale = this.parent.screenWidth / this.options.width;\n }\n if (this.options.height > 0)\n {\n this.yScale = this.parent.screenHeight / this.options.height;\n }\n this.xScale = this.xIndependent ? this.xScale : this.yScale;\n this.yScale = this.yIndependent ? this.yScale : this.xScale;\n }\n\n public wheel(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n\n return false;\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n else if (this.options.interrupt)\n {\n this.snapping = null;\n }\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n if (this.options.interrupt && this.parent.input.count() !== 0)\n {\n return;\n }\n\n let oldCenter: Point | undefined;\n\n if (!this.options.center && !this.options.noMove)\n {\n oldCenter = this.parent.center;\n }\n if (!this.snapping)\n {\n if (this.parent.scale.x !== this.xScale || this.parent.scale.y !== this.yScale)\n {\n this.createSnapping();\n }\n }\n else if (this.snapping)\n {\n const snapping = this.snapping;\n\n snapping.time += elapsed;\n\n if (snapping.time >= this.options.time)\n {\n this.parent.scale.set(this.xScale, this.yScale);\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n this.parent.emit('snap-zoom-end', this.parent);\n this.snapping = null;\n }\n else\n {\n const snapping = this.snapping;\n const worldScreenWidth = this.ease(snapping.time, snapping.startX, snapping.deltaX, this.options.time);\n const worldScreenHeight = this.ease(snapping.time, snapping.startY, snapping.deltaY, this.options.time);\n\n this.parent.scale.x = this.parent.screenWidth / worldScreenWidth;\n this.parent.scale.y = this.parent.screenHeight / worldScreenHeight;\n }\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (!this.options.noMove)\n {\n if (!this.options.center)\n {\n this.parent.moveCenter(oldCenter as Point);\n }\n else\n {\n this.parent.moveCenter(this.options.center);\n }\n }\n }\n }\n\n public resume(): void\n {\n this.snapping = null;\n super.resume();\n }\n}\n","import { Plugin } from './Plugin';\nimport { IPointData, Point } from '@pixi/math';\n\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Wheel}. */\nexport interface IWheelOptions\n{\n /**\n * Percent to scroll with each spin\n *\n * @default 0.1\n */\n percent?: number;\n\n /**\n * smooth the zooming by providing the number of frames to zoom between wheel spins\n *\n * @default false\n */\n smooth?: false | number;\n\n /**\n * Stop smoothing with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Reverse the direction of the scroll\n *\n * @default false\n */\n reverse?: boolean;\n\n /**\n * Place this point at center during zoom instead of current mouse position\n *\n * @default null\n */\n center?: Point | null;\n\n /**\n * Scaling factor for non-DOM_DELTA_PIXEL scrolling events\n *\n * @default 20\n */\n lineHeight?: number;\n\n /**\n * Axis to zoom\n *\n * @default 'all'\n */\n axis?: 'all' | 'x' | 'y';\n}\n\nconst DEFAULT_WHEEL_OPTIONS: Required = {\n percent: 0.1,\n smooth: false,\n interrupt: true,\n reverse: false,\n center: null,\n lineHeight: 20,\n axis: 'all',\n};\n\n/**\n * Plugin for handling wheel scrolling for viewport zoom.\n *\n * @event wheel({wheel: {dx, dy, dz}, event, viewport})\n */\nexport class Wheel extends Plugin\n{\n public readonly options: Required;\n\n protected smoothing?: IPointData | null;\n protected smoothingCenter?: Point | null;\n protected smoothingCount?: number;\n\n /**\n * This is called by {@link Viewport.wheel}.\n */\n constructor(parent: Viewport, options: IWheelOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_WHEEL_OPTIONS, options);\n }\n\n public down(): boolean\n {\n if (this.options.interrupt)\n {\n this.smoothing = null;\n }\n\n return false;\n }\n\n protected isAxisX(): boolean\n {\n return ['all', 'x'].includes(this.options.axis);\n }\n\n protected isAxisY(): boolean\n {\n return ['all', 'y'].includes(this.options.axis);\n }\n\n public update(): void\n {\n if (this.smoothing)\n {\n const point = this.smoothingCenter;\n const change = this.smoothing;\n let oldPoint;\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point as IPointData);\n }\n if (this.isAxisX())\n {\n this.parent.scale.x += change.x;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y += change.y;\n }\n\n this.parent.emit('zoomed', { viewport: this.parent, type: 'wheel' });\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += (point as IPointData).x - newPoint.x;\n this.parent.y += (point as IPointData).y - newPoint.y;\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n (this.smoothingCount as number)++;\n\n if (this.smoothingCount as number >= this.options.smooth)\n {\n this.smoothing = null;\n }\n }\n }\n\n public wheel(e: WheelEvent): boolean | undefined\n {\n if (this.paused)\n {\n return;\n }\n\n const point = this.parent.input.getPointerPosition(e);\n const sign = this.options.reverse ? -1 : 1;\n const step = sign * -e.deltaY * (e.deltaMode ? this.options.lineHeight : 1) / 500;\n const change = Math.pow(2, (1 + this.options.percent) * step);\n\n if (this.options.smooth)\n {\n const original = {\n x: this.smoothing ? this.smoothing.x * (this.options.smooth - (this.smoothingCount as number)) : 0,\n y: this.smoothing ? this.smoothing.y * (this.options.smooth - (this.smoothingCount as number)) : 0\n };\n\n this.smoothing = {\n x: ((this.parent.scale.x + original.x) * change - this.parent.scale.x) / this.options.smooth,\n y: ((this.parent.scale.y + original.y) * change - this.parent.scale.y) / this.options.smooth,\n };\n this.smoothingCount = 0;\n this.smoothingCenter = point;\n }\n else\n {\n let oldPoint: IPointData | undefined;\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point);\n }\n if (this.isAxisX())\n {\n this.parent.scale.x *= change;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y *= change;\n }\n this.parent.emit('zoomed', { viewport: this.parent, type: 'wheel' });\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += point.x - newPoint.x;\n this.parent.y += point.y - newPoint.y;\n }\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n this.parent.emit('wheel', { wheel: { dx: e.deltaX, dy: e.deltaY, dz: e.deltaZ }, event: e, viewport: this.parent });\n\n if (!this.parent.options.passiveWheel)\n {\n return true;\n }\n }\n}\n","import type {\n Animate,\n Bounce,\n Clamp,\n ClampZoom,\n Decelerate,\n Drag,\n Follow,\n MouseEdges,\n Pinch,\n Plugin,\n Snap,\n SnapZoom,\n Wheel,\n} from './plugins';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { Viewport } from './Viewport';\n\nconst PLUGIN_ORDER = [\n 'drag',\n 'pinch',\n 'wheel',\n 'follow',\n 'mouse-edges',\n 'decelerate',\n 'aniamte',\n 'bounce',\n 'snap-zoom',\n 'clamp-zoom',\n 'snap',\n 'clamp',\n];\n\n/**\n * Use this to access current plugins or add user-defined plugins\n *\n * @public\n */\nexport class PluginManager\n{\n /** Maps mounted plugins by their type */\n public plugins: Partial>;\n\n /**\n * List of plugins mounted\n *\n * This list is kept sorted by the internal priority of plugins (hard-coded).\n */\n public list: Array;\n\n /** The viewport using the plugins managed by `this`. */\n public readonly viewport: Viewport;\n\n /** This is called by {@link Viewport} to initialize the {@link Viewport.plugins plugins}. */\n constructor(viewport: Viewport)\n {\n this.viewport = viewport;\n this.list = [];\n this.plugins = {};\n }\n\n /**\n * Inserts a named plugin or a user plugin into the viewport\n * default plugin order: 'drag', 'pinch', 'wheel', 'follow', 'mouse-edges', 'decelerate', 'bounce',\n * 'snap-zoom', 'clamp-zoom', 'snap', 'clamp'\n *\n * @param {string} name of plugin\n * @param {Plugin} plugin - instantiated Plugin class\n * @param {number} index to insert userPlugin (otherwise inserts it at the end)\n */\n public add(name: string, plugin: Plugin, index: number = PLUGIN_ORDER.length)\n {\n this.plugins[name] = plugin;\n\n const current = PLUGIN_ORDER.indexOf(name);\n\n if (current !== -1)\n {\n PLUGIN_ORDER.splice(current, 1);\n }\n\n PLUGIN_ORDER.splice(index, 0, name);\n this.sort();\n }\n\n public get(name: 'animate', ignorePaused?: boolean): Animate | undefined | null;\n public get(name: 'bounce', ignorePaused?: boolean): Bounce | undefined | null;\n public get(name: 'clamp', ignorePaused?: boolean): Clamp | undefined | null;\n public get(name: 'clamp-zoom', ignorePaused?: boolean): ClampZoom | undefined | null;\n public get(name: 'decelerate', ignorePaused?: boolean): Decelerate | undefined | null;\n public get(name: 'drag', ignorePaused?: boolean): Drag | undefined | null;\n public get(name: 'follow', ignorePaused?: boolean): Follow | undefined | null;\n public get(name: 'mouse-edges', ignorePaused?: boolean): MouseEdges | undefined | null;\n public get(name: 'pinch', ignorePaused?: boolean): Pinch | undefined | null;\n public get(name: 'snap', ignorePaused?: boolean): Snap | undefined | null;\n public get(name: 'snap-zoom', ignorePaused?: boolean): SnapZoom | undefined | null;\n public get(name: 'wheel', ignorePaused?: boolean): Wheel | undefined | null;\n public get(name: string, ignorePaused?: boolean): T | undefined | null;\n\n /**\n * Get plugin\n *\n * @param {string} name of plugin\n * @param {boolean} [ignorePaused] return null if plugin is paused\n */\n public get(name: string, ignorePaused?: boolean): T | undefined | null\n {\n if (ignorePaused)\n {\n if (this.plugins[name]?.paused)\n {\n return null;\n }\n }\n\n return this.plugins[name] as T;\n }\n\n /**\n * Update all active plugins\n *\n * @internal\n * @ignore\n * @param {number} elapsed type in milliseconds since last update\n */\n public update(elapsed: number): void\n {\n for (const plugin of this.list)\n {\n plugin.update(elapsed);\n }\n }\n\n /**\n * Resize all active plugins\n *\n * @internal\n * @ignore\n */\n public resize(): void\n {\n for (const plugin of this.list)\n {\n plugin.resize();\n }\n }\n\n /** Clamps and resets bounce and decelerate (as needed) after manually moving viewport */\n public reset(): void\n {\n for (const plugin of this.list)\n {\n plugin.reset();\n }\n }\n\n /** removes all installed plugins */\n public removeAll(): void\n {\n this.plugins = {};\n this.sort();\n }\n\n /**\n * Removes installed plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public remove(name: string): void\n {\n if (this.plugins[name])\n {\n delete this.plugins[name];\n this.viewport.emit(`${name}-remove`);\n this.sort();\n }\n }\n\n /**\n * Pause plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public pause(name: string): void\n {\n this.plugins[name]?.pause();\n }\n\n /**\n * Resume plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public resume(name: string): void\n {\n this.plugins[name]?.resume();\n }\n\n /**\n * Sort plugins according to PLUGIN_ORDER\n *\n * @internal\n * @ignore\n */\n public sort()\n {\n this.list = [];\n\n for (const plugin of PLUGIN_ORDER)\n {\n if (this.plugins[plugin])\n {\n this.list.push(this.plugins[plugin] as Plugin);\n }\n }\n }\n\n /**\n * Handle down for all plugins\n *\n * @internal\n * @ignore\n */\n public down(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.list)\n {\n if (plugin.down(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle move for all plugins\n *\n * @internal\n * @ignore\n */\n public move(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.viewport.plugins.list)\n {\n if (plugin.move(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle up for all plugins\n *\n * @internal\n * @ignore\n */\n public up(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.list)\n {\n if (plugin.up(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle wheel event for all plugins\n *\n * @internal\n * @ignore\n */\n public wheel(e: WheelEvent): boolean\n {\n let result = false;\n\n for (const plugin of this.list)\n {\n if (plugin.wheel(e))\n {\n result = true;\n }\n }\n\n return result;\n }\n}\n","import { Container } from '@pixi/display';\nimport { Point, Rectangle } from '@pixi/math';\nimport { Ticker } from '@pixi/ticker';\n\nimport { InputManager } from './InputManager';\nimport { PluginManager } from './PluginManager';\nimport {\n Animate,\n Bounce,\n Clamp,\n ClampZoom,\n Decelerate,\n Drag,\n Follow,\n MouseEdges,\n Pinch,\n Snap,\n SnapZoom,\n Wheel\n} from './plugins';\n\nimport type { DisplayObject, IDestroyOptions } from '@pixi/display';\nimport type { IHitArea, InteractionManager } from '@pixi/interaction';\n\n/** Options for {@link Viewport}. */\nexport interface IViewportOptions {\n /** @default window.innerWidth */\n screenWidth?: number;\n\n /** @default window.innerHeight */\n screenHeight?: number;\n\n /** @default this.width */\n worldWidth?: number | null;\n\n /** @default this.height */\n worldHeight?: number | null;\n\n /**\n * Number of pixels to move to trigger an input event (e.g., drag, pinch) or disable a clicked event\n *\n * @default 5\n */\n threshold?: number;\n\n /**\n * Whether the 'wheel' event is set to passive (note: if false, e.preventDefault() will be called when wheel is used over the viewport)\n *\n * @default true\n */\n passiveWheel?: boolean;\n\n /**\n * Whether to stopPropagation of events that impact the viewport (except wheel events, see options.passiveWheel)\n */\n stopPropagation?: boolean;\n\n /**\n * Change the default hitArea from world size to a new value\n */\n forceHitArea?: Rectangle | null;\n\n /**\n * Set this if you want to manually call update() function on each frame\n *\n * @default false\n */\n noTicker?: boolean;\n\n /**\n * InteractionManager, available from instantiated `WebGLRenderer/CanvasRenderer.plugins.interaction`\n *\n * It's used to calculate pointer postion relative to canvas location on screen\n */\n interaction?: InteractionManager | null;\n\n /**\n * Remove oncontextmenu=() => {} from the divWheel element\n */\n disableOnContextMenu?: boolean;\n\n /**\n * div to attach the wheel event\n *\n * @default document.body\n */\n divWheel?: HTMLElement;\n\n /**\n * Use this PIXI.ticker for updates\n *\n * @default PIXI.Ticker.shared\n */\n ticker?: Ticker;\n}\n\nexport interface ICompleteViewportOptions extends IViewportOptions {\n screenWidth: number;\n screenHeight: number;\n threshold: number;\n passiveWheel: boolean;\n stopPropagation: boolean;\n noTicker: boolean;\n ticker: Ticker;\n}\n\nexport interface IViewportTransformState {\n x: number;\n y: number;\n scaleX: number;\n scaleY: number;\n}\n\nconst DEFAULT_VIEWPORT_OPTIONS: ICompleteViewportOptions = {\n screenWidth: window.innerWidth,\n screenHeight: window.innerHeight,\n worldWidth: null,\n worldHeight: null,\n threshold: 5,\n passiveWheel: true,\n stopPropagation: false,\n forceHitArea: null,\n noTicker: false,\n interaction: null,\n disableOnContextMenu: false,\n ticker: Ticker.shared,\n};\n\n/**\n * Main class to use when creating a Viewport\n *\n * @public\n * @fires clicked\n * @fires drag-start\n * @fires drag-end\n * @fires drag-remove\n * @fires pinch-start\n * @fires pinch-end\n * @fires pinch-remove\n * @fires snap-start\n * @fires snap-end\n * @fires snap-remove\n * @fires snap-zoom-start\n * @fires snap-zoom-end\n * @fires snap-zoom-remove\n * @fires bounce-x-start\n * @fires bounce-x-end\n * @fires bounce-y-start\n * @fires bounce-y-end\n * @fires bounce-remove\n * @fires wheel\n * @fires wheel-remove\n * @fires wheel-scroll\n * @fires wheel-scroll-remove\n * @fires mouse-edge-start\n * @fires mouse-edge-end\n * @fires mouse-edge-remove\n * @fires moved\n * @fires moved-end\n * @fires zoomed\n * @fires zoomed-end\n * @fires frame-end\n */\nexport class Viewport extends Container\n{\n /** Flags whether the viewport is being panned */\n public moving?: boolean;\n\n public screenWidth: number;\n public screenHeight: number;\n\n /** Number of pixels to move to trigger an input event (e.g., drag, pinch) or disable a clicked event */\n public threshold: number;\n\n public readonly input: InputManager;\n\n /** Use this to add user plugins or access existing plugins (e.g., to pause, resume, or remove them) */\n public readonly plugins: PluginManager;\n\n /** Flags whether the viewport zoom is being changed. */\n public zooming?: boolean;\n\n public lastViewport?: IViewportTransformState | null;\n\n /** The options passed when creating this viewport, merged with the default values */\n public readonly options: ICompleteViewportOptions & { divWheel: HTMLElement };\n\n private _dirty?: boolean;\n private _forceHitArea?: IHitArea | null;\n private _hitAreaDefault?: Rectangle;\n private _pause?: boolean;\n private readonly tickerFunction?: () => void;\n private _worldWidth?: number | null;\n private _worldHeight?: number | null;\n\n /**\n * @param options\n */\n constructor(options: IViewportOptions = {})\n {\n super();\n this.options = Object.assign(\n {},\n { divWheel: document.body },\n DEFAULT_VIEWPORT_OPTIONS,\n options\n );\n\n this.screenWidth = this.options.screenWidth;\n this.screenHeight = this.options.screenHeight;\n\n this._worldWidth = this.options.worldWidth;\n this._worldHeight = this.options.worldHeight;\n this.forceHitArea = this.options.forceHitArea;\n this.threshold = this.options.threshold;\n\n this.options.divWheel = this.options.divWheel || document.body;\n\n if (this.options.disableOnContextMenu)\n {\n this.options.divWheel.oncontextmenu = (e) => e.preventDefault();\n }\n if (!this.options.noTicker)\n {\n this.tickerFunction = () => this.update(this.options.ticker.elapsedMS);\n this.options.ticker.add(this.tickerFunction);\n }\n\n this.input = new InputManager(this);\n this.plugins = new PluginManager(this);\n }\n\n /** Overrides PIXI.Container's destroy to also remove the 'wheel' and PIXI.Ticker listeners */\n destroy(options: IDestroyOptions)\n {\n if (!this.options.noTicker && this.tickerFunction)\n {\n this.options.ticker.remove(this.tickerFunction);\n }\n\n this.input.destroy();\n super.destroy(options);\n }\n\n /**\n * Update viewport on each frame.\n *\n * By default, you do not need to call this unless you set `options.noTicker=true`.\n *\n * @param {number} elapsed time in milliseconds since last update\n */\n update(elapsed: number): void\n {\n if (!this.pause)\n {\n this.plugins.update(elapsed);\n\n if (this.lastViewport)\n {\n // Check for moved-end event\n if (this.lastViewport.x !== this.x || this.lastViewport.y !== this.y)\n {\n this.moving = true;\n }\n else if (this.moving)\n {\n this.emit('moved-end', this);\n this.moving = false;\n }\n\n // Check for zoomed-end event\n if (this.lastViewport.scaleX !== this.scale.x || this.lastViewport.scaleY !== this.scale.y)\n {\n this.zooming = true;\n }\n else if (this.zooming)\n {\n this.emit('zoomed-end', this);\n this.zooming = false;\n }\n }\n\n if (!this.forceHitArea)\n {\n this._hitAreaDefault = new Rectangle(this.left, this.top, this.worldScreenWidth, this.worldScreenHeight);\n this.hitArea = this._hitAreaDefault;\n }\n\n this._dirty = this._dirty || !this.lastViewport\n || this.lastViewport.x !== this.x || this.lastViewport.y !== this.y\n || this.lastViewport.scaleX !== this.scale.x || this.lastViewport.scaleY !== this.scale.y;\n\n this.lastViewport = {\n x: this.x,\n y: this.y,\n scaleX: this.scale.x,\n scaleY: this.scale.y\n };\n this.emit('frame-end', this);\n }\n }\n\n /** Use this to set screen and world sizes, needed for pinch/wheel/clamp/bounce. */\n resize(\n screenWidth = window.innerWidth,\n screenHeight = window.innerHeight,\n worldWidth: number,\n worldHeight: number\n ): void\n {\n this.screenWidth = screenWidth;\n this.screenHeight = screenHeight;\n\n if (typeof worldWidth !== 'undefined')\n {\n this._worldWidth = worldWidth;\n }\n if (typeof worldHeight !== 'undefined')\n {\n this._worldHeight = worldHeight;\n }\n\n this.plugins.resize();\n this.dirty = true;\n }\n\n /** World width, in pixels */\n get worldWidth(): number\n {\n if (this._worldWidth)\n {\n return this._worldWidth;\n }\n\n return this.width / this.scale.x;\n }\n set worldWidth(value)\n {\n this._worldWidth = value;\n this.plugins.resize();\n }\n\n /** World height, in pixels */\n get worldHeight(): number\n {\n if (this._worldHeight)\n {\n return this._worldHeight;\n }\n\n return this.height / this.scale.y;\n }\n set worldHeight(value)\n {\n this._worldHeight = value;\n this.plugins.resize();\n }\n\n /** Get visible world bounds of viewport */\n public getVisibleBounds(): Rectangle\n {\n return new Rectangle(this.left, this.top, this.worldScreenWidth, this.worldScreenHeight);\n }\n\n /** Change coordinates from screen to world */\n public toWorld(x: number, y: number): Point;\n /** Change coordinates from screen to world */\n public toWorld(screenPoint: Point): Point;\n\n public toWorld(x: number | Point, y?: number): Point\n {\n if (arguments.length === 2)\n {\n return this.toLocal(new Point(x as number, y));\n }\n\n return this.toLocal(x as Point);\n }\n\n /** Change coordinates from world to screen */\n public toScreen(x: number, y: number): Point;\n /** Change coordinates from world to screen */\n public toScreen(worldPoint: Point): Point;\n\n public toScreen(x: number | Point, y?: number): Point\n {\n if (arguments.length === 2)\n {\n return this.toGlobal(new Point(x as number, y));\n }\n\n return this.toGlobal(x as Point);\n }\n\n /** Screen width in world coordinates */\n get worldScreenWidth(): number\n {\n return this.screenWidth / this.scale.x;\n }\n\n /** Screen height in world coordinates */\n get worldScreenHeight(): number\n {\n return this.screenHeight / this.scale.y;\n }\n\n /** World width in screen coordinates */\n get screenWorldWidth(): number\n {\n return this.worldWidth * this.scale.x;\n }\n\n /** World height in screen coordinates */\n get screenWorldHeight()\n {\n return this.worldHeight * this.scale.y;\n }\n\n /** Center of screen in world coordinates */\n get center(): Point\n {\n return new Point(\n this.worldScreenWidth / 2 - this.x / this.scale.x,\n this.worldScreenHeight / 2 - this.y / this.scale.y,\n );\n }\n set center(value: Point)\n {\n this.moveCenter(value);\n }\n\n /** Move center of viewport to (x, y) */\n public moveCenter(x: number, y: number): this;\n\n /** Move center of viewport to {@code center}. */\n public moveCenter(center: Point): this;\n\n public moveCenter(...args: [number, number] | [Point]): this\n {\n let x: number;\n let y: number;\n\n if (typeof args[0] === 'number')\n {\n x = args[0];\n y = args[1] as number;\n }\n else\n {\n x = args[0].x;\n y = args[0].y;\n }\n\n const newX = (this.worldScreenWidth / 2 - x) * this.scale.x;\n const newY = (this.worldScreenHeight / 2 - y) * this.scale.y;\n\n if (this.x !== newX || this.y !== newY)\n {\n this.position.set(newX, newY);\n this.plugins.reset();\n this.dirty = true;\n }\n\n return this;\n }\n\n /** Top-left corner of Viewport */\n get corner(): Point\n {\n return new Point(-this.x / this.scale.x, -this.y / this.scale.y);\n }\n set corner(value: Point)\n {\n this.moveCorner(value);\n }\n\n /** Move viewport's top-left corner; also clamps and resets decelerate and bounce (as needed) */\n public moveCorner(x: number, y: number): this;\n\n /** move viewport's top-left corner; also clamps and resets decelerate and bounce (as needed) */\n public moveCorner(center: Point): this;\n\n public moveCorner(...args: [number, number] | [Point])\n {\n let x;\n let y;\n\n if (args.length === 1)\n {\n x = -args[0].x * this.scale.x;\n y = -args[0].y * this.scale.y;\n }\n else\n {\n x = -args[0] * this.scale.x;\n y = -args[1] * this.scale.y;\n }\n\n if (x !== this.x || y !== this.y)\n {\n this.position.set(x, y);\n this.plugins.reset();\n this.dirty = true;\n }\n\n return this;\n }\n\n /** Get how many world pixels fit in screen's width */\n get screenWidthInWorldPixels()\n {\n return this.screenWidth / this.scale.x;\n }\n\n /** Get how many world pixels fit on screen's height */\n get screenHeightInWorldPixels()\n {\n return this.screenHeight / this.scale.y;\n }\n\n /**\n * Find the scale value that fits a world width on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param width - Width in world pixels\n * @return - scale\n */\n findFitWidth(width: number): number\n {\n return this.screenWidth / width;\n }\n\n /**\n * Finds the scale value that fits a world height on the screens\n * does not change the viewport (use fit... to change)\n *\n * @param height - Height in world pixels\n * @return - scale\n */\n findFitHeight(height: number): number\n {\n return this.screenHeight / height;\n }\n\n /**\n * Finds the scale value that fits the smaller of a world width and world height on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param {number} width in world pixels\n * @param {number} height in world pixels\n * @returns {number} scale\n */\n findFit(width: number, height: number): number\n {\n const scaleX = this.screenWidth / width;\n const scaleY = this.screenHeight / height;\n\n return Math.min(scaleX, scaleY);\n }\n\n /**\n * Finds the scale value that fits the larger of a world width and world height on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param {number} width in world pixels\n * @param {number} height in world pixels\n * @returns {number} scale\n */\n findCover(width: number, height: number): number\n {\n const scaleX = this.screenWidth / width;\n const scaleY = this.screenHeight / height;\n\n return Math.max(scaleX, scaleY);\n }\n\n /**\n * Change zoom so the width fits in the viewport\n *\n * @param width - width in world coordinates\n * @param center - maintain the same center\n * @param scaleY - whether to set scaleY=scaleX\n * @param noClamp - whether to disable clamp-zoom\n * @returns {Viewport} this\n */\n fitWidth(width = this.worldWidth, center?: boolean, scaleY = true, noClamp?: boolean): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.x = this.screenWidth / width;\n\n if (scaleY)\n {\n this.scale.y = this.scale.x;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (!noClamp && clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so the height fits in the viewport\n *\n * @param {number} [height=this.worldHeight] in world coordinates\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @param {boolean} [scaleX=true] whether to set scaleX = scaleY\n * @param {boolean} [noClamp] whether to disable clamp-zoom\n * @returns {Viewport} this\n */\n fitHeight(height = this.worldHeight, center?: boolean, scaleX = true, noClamp?: boolean)\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.y = this.screenHeight / height;\n\n if (scaleX)\n {\n this.scale.x = this.scale.y;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (!noClamp && clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so it fits the entire world in the viewport\n *\n * @param {boolean} center maintain the same center of the screen after zoom\n * @returns {Viewport} this\n */\n fitWorld(center?: boolean): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n\n this.scale.x = this.screenWidth / this.worldWidth;\n this.scale.y = this.screenHeight / this.worldHeight;\n\n if (this.scale.x < this.scale.y)\n {\n this.scale.y = this.scale.x;\n }\n else\n {\n this.scale.x = this.scale.y;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so it fits the size or the entire world in the viewport\n *\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @param {number} [width=this.worldWidth] desired width\n * @param {number} [height=this.worldHeight] desired height\n * @returns {Viewport} this\n */\n fit(center?: boolean, width = this.worldWidth, height = this.worldHeight): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n\n this.scale.x = this.screenWidth / width;\n this.scale.y = this.screenHeight / height;\n\n if (this.scale.x < this.scale.y)\n {\n this.scale.y = this.scale.x;\n }\n else\n {\n this.scale.x = this.scale.y;\n }\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n // eslint-disable-next-line\n // @ts-ignore\n set visible(value: boolean)\n {\n if (!value)\n {\n this.input.clear();\n }\n\n super.visible = value;\n }\n\n /**\n * Zoom viewport to specific value.\n *\n * @param {number} scale value (e.g., 1 would be 100%, 0.25 would be 25%)\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n setZoom(scale: number, center?: boolean): this\n {\n let save;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.set(scale);\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Zoom viewport by a certain percent (in both x and y direction).\n *\n * @param {number} percent change (e.g., 0.25 would increase a starting scale of 1.0 to 1.25)\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n zoomPercent(percent: number, center?: boolean): this\n {\n return this.setZoom(this.scale.x + this.scale.x * percent, center);\n }\n\n /**\n * Zoom viewport by increasing/decreasing width by a certain number of pixels.\n *\n * @param {number} change in pixels\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n zoom(change: number, center?: boolean)\n {\n this.fitWidth(change + this.worldScreenWidth, center);\n\n return this;\n }\n\n /** Changes scale of viewport and maintains center of viewport */\n get scaled(): number\n {\n return this.scale.x;\n }\n set scaled(scale: number)\n {\n this.setZoom(scale, true);\n }\n\n /**\n * @param {SnapZoomOptions} options\n */\n snapZoom(options: any): this\n {\n this.plugins.add('snap-zoom', new SnapZoom(this, options));\n\n return this;\n }\n\n /** Is container out of world bounds */\n OOB(): {\n left: boolean;\n right: boolean;\n top: boolean;\n bottom: boolean;\n cornerPoint: Point;\n }\n {\n return {\n left: this.left < 0,\n right: this.right > this.worldWidth,\n top: this.top < 0,\n bottom: this.bottom > this.worldHeight,\n cornerPoint: new Point(\n this.worldWidth * this.scale.x - this.screenWidth,\n this.worldHeight * this.scale.y - this.screenHeight\n )\n };\n }\n\n /** World coordinates of the right edge of the screen */\n get right(): number\n {\n return -this.x / this.scale.x + this.worldScreenWidth;\n }\n set right(value: number)\n {\n this.x = -value * this.scale.x + this.screenWidth;\n this.plugins.reset();\n }\n\n /** World coordinates of the left edge of the screen */\n get left(): number\n {\n return -this.x / this.scale.x;\n }\n set left(value: number)\n {\n this.x = -value * this.scale.x;\n this.plugins.reset();\n }\n\n /** World coordinates of the top edge of the screen */\n get top(): number\n {\n return -this.y / this.scale.y;\n }\n set top(value: number)\n {\n this.y = -value * this.scale.y;\n this.plugins.reset();\n }\n\n /** World coordinates of the bottom edge of the screen */\n get bottom(): number\n {\n return -this.y / this.scale.y + this.worldScreenHeight;\n }\n set bottom(value: number)\n {\n this.y = -value * this.scale.y + this.screenHeight;\n this.plugins.reset();\n }\n\n /**\n * Determines whether the viewport is dirty (i.e., needs to be renderered to the screen because of a change)\n */\n get dirty(): boolean\n {\n return !!this._dirty;\n }\n set dirty(value: boolean)\n {\n this._dirty = value;\n }\n\n /**\n * Permanently changes the Viewport's hitArea\n *\n * NOTE: if not set then hitArea = PIXI.Rectangle(Viewport.left, Viewport.top, Viewport.worldScreenWidth, Viewport.worldScreenHeight)\n */\n get forceHitArea(): IHitArea | null | undefined\n {\n return this._forceHitArea;\n }\n set forceHitArea(value: IHitArea | null | undefined)\n {\n if (value)\n {\n this._forceHitArea = value;\n this.hitArea = value;\n }\n else\n {\n this._forceHitArea = null;\n this.hitArea = new Rectangle(0, 0, this.worldWidth, this.worldHeight);\n }\n }\n\n /**\n * Enable one-finger touch to drag\n *\n * NOTE: if you expect users to use right-click dragging, you should enable `viewport.options.disableOnContextMenu`\n * to avoid the context menu popping up on each right-click drag.\n *\n * @param {DragOptions} [options]\n * @returns {Viewport} this\n */\n public drag(options: any): this\n {\n this.plugins.add('drag', new Drag(this, options));\n\n return this;\n }\n\n /**\n * Clamp to world boundaries or other provided boundaries\n *\n * NOTES:\n * clamp is disabled if called with no options; use { direction: 'all' } for all edge clamping\n * screenWidth, screenHeight, worldWidth, and worldHeight needs to be set for this to work properly\n *\n * @param {ClampOptions} [options]\n * @returns {Viewport} this\n */\n public clamp(options: any): this\n {\n this.plugins.add('clamp', new Clamp(this, options));\n\n return this;\n }\n\n /**\n * Decelerate after a move\n *\n * NOTE: this fires 'moved' event during deceleration\n *\n * @param {DecelerateOptions} [options]\n * @return {Viewport} this\n */\n public decelerate(options: any): this\n {\n this.plugins.add('decelerate', new Decelerate(this, options));\n\n return this;\n }\n\n /**\n * Bounce on borders\n * NOTES:\n * screenWidth, screenHeight, worldWidth, and worldHeight needs to be set for this to work properly\n * fires 'moved', 'bounce-x-start', 'bounce-y-start', 'bounce-x-end', and 'bounce-y-end' events\n * @param {object} [options]\n * @param {string} [options.sides=all] all, horizontal, vertical, or combination of top, bottom, right, left (e.g., 'top-bottom-right')\n * @param {number} [options.friction=0.5] friction to apply to decelerate if active\n * @param {number} [options.time=150] time in ms to finish bounce\n * @param {object} [options.bounceBox] use this bounceBox instead of (0, 0, viewport.worldWidth, viewport.worldHeight)\n * @param {number} [options.bounceBox.x=0]\n * @param {number} [options.bounceBox.y=0]\n * @param {number} [options.bounceBox.width=viewport.worldWidth]\n * @param {number} [options.bounceBox.height=viewport.worldHeight]\n * @param {string|function} [options.ease=easeInOutSine] ease function or name (see http://easings.net/ for supported names)\n * @param {string} [options.underflow=center] (top/bottom/center and left/right/center, or center) where to place world if too small for screen\n * @return {Viewport} this\n */\n public bounce(options: any): this\n {\n this.plugins.add('bounce', new Bounce(this, options));\n\n return this;\n }\n\n /**\n * Enable pinch to zoom and two-finger touch to drag\n *\n * @param {PinchOptions} [options]\n * @return {Viewport} this\n */\n public pinch(options: any): this\n {\n this.plugins.add('pinch', new Pinch(this, options));\n\n return this;\n }\n\n /**\n * Snap to a point\n *\n * @param {number} x\n * @param {number} y\n * @param {SnapOptions} [options]\n * @return {Viewport} this\n */\n public snap(x: number, y: number, options: any): this\n {\n this.plugins.add('snap', new Snap(this, x, y, options));\n\n return this;\n }\n\n /**\n * Follow a target\n *\n * NOTES:\n * uses the (x, y) as the center to follow; for PIXI.Sprite to work properly, use sprite.anchor.set(0.5)\n * options.acceleration is not perfect as it doesn't know the velocity of the target\n * it adds acceleration to the start of movement and deceleration to the end of movement when the target is stopped\n * fires 'moved' event\n * @param {PIXI.DisplayObject} target to follow\n * @param {FollowOptions} [options]\n * @returns {Viewport} this\n */\n public follow(target: DisplayObject, options: any): this\n {\n this.plugins.add('follow', new Follow(this, target, options));\n\n return this;\n }\n\n /**\n * Zoom using mouse wheel\n *\n * @param {WheelOptions} [options]\n * @return {Viewport} this\n */\n public wheel(options: any): this\n {\n this.plugins.add('wheel', new Wheel(this, options));\n\n return this;\n }\n\n /**\n * Animate the position and/or scale of the viewport\n *\n * @param {AnimateOptions} options\n * @returns {Viewport} this\n */\n public animate(options: any): this\n {\n this.plugins.add('animate', new Animate(this, options));\n\n return this;\n }\n\n /**\n * Enable clamping of zoom to constraints\n *\n * The minWidth/Height settings are how small the world can get (as it would appear on the screen)\n * before clamping. The maxWidth/maxHeight is how larger the world can scale (as it would appear on\n * the screen) before clamping.\n *\n * For example, if you have a world size of 1000 x 1000 and a screen size of 100 x 100, if you set\n * minWidth/Height = 100 then the world will not be able to zoom smaller than the screen size (ie,\n * zooming out so it appears smaller than the screen). Similarly, if you set maxWidth/Height = 100\n * the world will not be able to zoom larger than the screen size (ie, zooming in so it appears\n * larger than the screen).\n *\n * @param {ClampZoomOptions} [options]\n * @return {Viewport} this\n */\n public clampZoom(options: any): this\n {\n this.plugins.add('clamp-zoom', new ClampZoom(this, options));\n\n return this;\n }\n\n /**\n * Scroll viewport when mouse hovers near one of the edges or radius-distance from center of screen.\n *\n * NOTE: fires 'moved' event\n *\n * @param {MouseEdgesOptions} [options]\n */\n public mouseEdges(options: any): this\n {\n this.plugins.add('mouse-edges', new MouseEdges(this, options));\n\n return this;\n }\n\n /** Pause viewport (including animation updates such as decelerate) */\n get pause(): boolean\n {\n return !!this._pause;\n }\n set pause(value: boolean)\n {\n this._pause = value;\n\n this.lastViewport = null;\n this.moving = false;\n this.zooming = false;\n\n if (value)\n {\n this.input.pause();\n }\n }\n\n /**\n * Move the viewport so the bounding box is visible\n *\n * @param x - left\n * @param y - top\n * @param width\n * @param height\n * @param resizeToFit - Resize the viewport so the box fits within the viewport\n */\n public ensureVisible(x: number, y: number, width: number, height: number, resizeToFit?: boolean)\n {\n if (resizeToFit && (width > this.worldScreenWidth || height > this.worldScreenHeight))\n {\n this.fit(true, width, height);\n this.emit('zoomed', { viewport: this, type: 'ensureVisible' });\n }\n let moved = false;\n\n if (x < this.left)\n {\n this.left = x;\n moved = true;\n }\n else if (x + width > this.right)\n {\n this.right = x + width;\n moved = true;\n }\n if (y < this.top)\n {\n this.top = y;\n moved = true;\n }\n else if (y + height > this.bottom)\n {\n this.bottom = y + height;\n moved = true;\n }\n if (moved)\n {\n this.emit('moved', { viewport: this, type: 'ensureVisible' });\n }\n }\n}\n\n/**\n * Fires after a mouse or touch click\n * @event Viewport#clicked\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a drag starts\n * @event Viewport#drag-start\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a drag ends\n * @event Viewport#drag-end\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a pinch starts\n * @event Viewport#pinch-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a pinch end\n * @event Viewport#pinch-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap starts\n * @event Viewport#snap-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap ends\n * @event Viewport#snap-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap-zoom starts\n * @event Viewport#snap-zoom-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap-zoom ends\n * @event Viewport#snap-zoom-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce starts in the x direction\n * @event Viewport#bounce-x-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce ends in the x direction\n * @event Viewport#bounce-x-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce starts in the y direction\n * @event Viewport#bounce-y-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce ends in the y direction\n * @event Viewport#bounce-y-end\n * @type {Viewport}\n */\n\n/**\n * Fires when for a mouse wheel event\n * @event Viewport#wheel\n * @type {object}\n * @property {object} wheel\n * @property {number} wheel.dx\n * @property {number} wheel.dy\n * @property {number} wheel.dz\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a wheel-scroll occurs\n * @event Viewport#wheel-scroll\n * @type {Viewport}\n */\n\n/**\n * Fires when a mouse-edge starts to scroll\n * @event Viewport#mouse-edge-start\n * @type {Viewport}\n */\n\n/**\n * Fires when the mouse-edge scrolling ends\n * @event Viewport#mouse-edge-end\n * @type {Viewport}\n */\n\n/**\n * Fires when viewport moves through UI interaction, deceleration, ensureVisible, or follow\n * @event Viewport#moved\n * @type {object}\n * @property {Viewport} viewport\n * @property {string} type - (drag, snap, pinch, follow, bounce-x, bounce-y,\n * clamp-x, clamp-y, decelerate, mouse-edges, wheel, ensureVisible)\n */\n\n/**\n * Fires when viewport moves through UI interaction, deceleration, ensureVisible, or follow\n * @event Viewport#zoomed\n * @type {object}\n * @property {Viewport} viewport\n * @property {string} type (drag-zoom, pinch, wheel, clamp-zoom, ensureVisible)\n */\n\n/**\n * Fires when viewport stops moving\n * @event Viewport#moved-end\n * @type {Viewport}\n */\n\n/**\n * Fires when viewport stops zooming\n * @event Viewport#zoomed-end\n * @type {Viewport}\n */\n\n/**\n* Fires at the end of an update frame\n* @event Viewport#frame-end\n* @type {Viewport}\n*/\n"],"names":[],"mappings":";u0BA+CA,wwGCEA,g3ICcA,06GCjCA,+4DCUA,s0DC2DA,gjJCpEA,+qDCiBA,03FCbA,8nECmBA,ymDCUA,8iFCNA,siLCxCA,0vCC+FA"} \ No newline at end of file +{"version":3,"file":"viewport.min.min.js","sources":["../src/plugins/Animate.ts","../src/plugins/Bounce.ts","../src/plugins/Clamp.ts","../src/plugins/ClampZoom.ts","../src/plugins/Decelerate.ts","../src/plugins/Drag.ts","../src/plugins/Follow.ts","../src/plugins/MouseEdges.ts","../src/plugins/Pinch.ts","../src/plugins/Snap.ts","../src/plugins/SnapZoom.ts","../src/plugins/Wheel.ts","../src/PluginManager.ts","../src/Viewport.ts"],"sourcesContent":["import { Point } from '@pixi/math';\nimport { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Animate}. */\nexport interface IAnimateOptions {\n /** Time to animate */\n time?: number;\n\n /** Position to move the viewport to */\n position?: Point;\n\n /**\n * Desired viewport width in world pixels\n *\n * (use instead of scale; aspect ratio is maintained if height is not provided)\n */\n width?: number;\n\n /**\n * Desired viewport height in world pixels\n *\n * (use instead of scale; aspect ratio is maintained if width is not provided)\n */\n height?: number;\n\n /** Scale to change zoom (scale.x = scale.y) */\n scale?: number;\n\n /** Independently change zoom in x-direction */\n scaleX?: number;\n\n /** Independently change zoom in y-direction */\n scaleY?: number;\n\n /** Easing function to use */\n ease?: any;\n\n /** Callback to invoke when the animation completes */\n callbackOnComplete?: (viewport: Viewport) => void;\n\n /** Removes this plugin if interrupted by any user input */\n removeOnInterrupt?: boolean;\n}\n\nconst DEFAULT_ANIMATE_OPTIONS = {\n removeOnInterrupt: false,\n ease: 'linear',\n time: 1000,\n};\n\n/**\n * Animation plugin.\n *\n * @see Viewport#animate\n * @fires animate-end\n */\nexport class Animate extends Plugin\n{\n public readonly options: IAnimateOptions & { ease: any; time: number };\n\n /** The starting x-coordinate of the viewport. */\n protected startX?: number;\n\n /** The starting y-coordinate of the viewport. */\n protected startY?: number;\n\n /** The change in the x-coordinate of the viewport through the animation.*/\n protected deltaX?: number;\n\n /** The change in the y-coordinate of the viewport through the animation. */\n protected deltaY?: number;\n\n /** Marks whether the center of the viewport is preserved in the animation. */\n protected keepCenter!: boolean;\n\n /** The starting viewport width. */\n protected startWidth: number | null = null;\n\n /** The starting viewport height. */\n protected startHeight: number | null = null;\n\n /** The change in the viewport's width through the animation. */\n protected deltaWidth: number | null = null;\n\n /** The change in the viewport's height through the animation. */\n protected deltaHeight: number | null = null;\n\n /** The viewport's width post-animation. */\n protected width: number | null = null;\n\n /** The viewport's height post-animation. */\n protected height: number | null = null;\n\n /** The time since the animation started. */\n protected time = 0;\n\n /**\n * This is called by {@link Viewport.animate}.\n *\n * @param parent\n * @param options\n */\n constructor(parent: Viewport, options: IAnimateOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_ANIMATE_OPTIONS, options);\n this.options.ease = ease(this.options.ease);\n\n this.setupPosition();\n this.setupZoom();\n\n this.time = 0;\n }\n\n /**\n * Setup `startX`, `startY`, `deltaX`, `deltaY`, `keepCenter`.\n *\n * This is called during construction.\n */\n protected setupPosition(): void\n {\n if (typeof this.options.position !== 'undefined')\n {\n this.startX = this.parent.center.x;\n this.startY = this.parent.center.y;\n this.deltaX = this.options.position.x - this.parent.center.x;\n this.deltaY = this.options.position.y - this.parent.center.y;\n this.keepCenter = false;\n }\n else\n {\n this.keepCenter = true;\n }\n }\n\n /**\n * Setup `startWidth, `startHeight`, `deltaWidth, `deltaHeight, `width`, `height`.\n *\n * This is called during construction.\n */\n protected setupZoom(): void\n {\n this.width = null;\n this.height = null;\n\n if (typeof this.options.scale !== 'undefined')\n {\n this.width = this.parent.screenWidth / this.options.scale;\n }\n else if (typeof this.options.scaleX !== 'undefined' || typeof this.options.scaleY !== 'undefined')\n {\n if (typeof this.options.scaleX !== 'undefined')\n {\n // screenSizeInWorldPixels = screenWidth / scale\n this.width = this.parent.screenWidth / this.options.scaleX;\n }\n if (typeof this.options.scaleY !== 'undefined')\n {\n this.height = this.parent.screenHeight / this.options.scaleY;\n }\n }\n else\n {\n if (typeof this.options.width !== 'undefined')\n {\n this.width = this.options.width;\n }\n if (typeof this.options.height !== 'undefined')\n {\n this.height = this.options.height;\n }\n }\n\n if (this.width !== null)\n {\n this.startWidth = this.parent.screenWidthInWorldPixels;\n this.deltaWidth = this.width - this.startWidth;\n }\n if (this.height !== null)\n {\n this.startHeight = this.parent.screenHeightInWorldPixels;\n this.deltaHeight = this.height - this.startHeight;\n }\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('animate');\n }\n\n return false;\n }\n\n public complete(): void\n {\n this.parent.plugins.remove('animate');\n if (this.width !== null)\n {\n this.parent.fitWidth(this.width, this.keepCenter, this.height === null);\n }\n if (this.height !== null)\n {\n this.parent.fitHeight(this.height, this.keepCenter, this.width === null);\n }\n if (!this.keepCenter)\n {\n this.parent.moveCenter(this.options.position as Point);\n }\n\n this.parent.emit('animate-end', this.parent);\n\n if (this.options.callbackOnComplete)\n {\n this.options.callbackOnComplete(this.parent);\n }\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n this.time += elapsed;\n\n if (this.time >= this.options.time)\n {\n this.complete();\n }\n else\n {\n const originalZoom = new Point(this.parent.scale.x, this.parent.scale.y);\n const percent = this.options.ease(this.time, 0, 1, this.options.time);\n\n if (this.width !== null)\n {\n const startWidth = this.startWidth as number;\n const deltaWidth = this.deltaWidth as number;\n\n this.parent.fitWidth(\n startWidth + (deltaWidth * percent),\n this.keepCenter,\n this.height === null);\n }\n if (this.height !== null)\n {\n const startHeight = this.startHeight as number;\n const deltaHeight = this.deltaHeight as number;\n\n this.parent.fitHeight(\n startHeight + (deltaHeight * percent),\n this.keepCenter,\n this.width === null);\n }\n if (this.width === null)\n {\n this.parent.scale.x = this.parent.scale.y;\n }\n else if (this.height === null)\n {\n this.parent.scale.y = this.parent.scale.x;\n }\n if (!this.keepCenter)\n {\n const startX = this.startX as number;\n const startY = this.startY as number;\n const deltaX = this.deltaX as number;\n const deltaY = this.deltaY as number;\n const original = new Point(this.parent.x, this.parent.y);\n\n this.parent.moveCenter(startX + (deltaX * percent), startY + (deltaY * percent));\n this.parent.emit('moved', { viewport: this.parent, original, type: 'animate' });\n }\n if (this.width || this.height)\n {\n this.parent.emit('zoomed', { viewport: this.parent, original: originalZoom, type: 'animate' });\n }\n }\n }\n}\n","import { Point, Rectangle } from '@pixi/math';\nimport { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Drag } from './Drag';\nimport type { IDecelerateOptions } from './Decelerate';\nimport type { Pinch } from './Pinch';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Bounce}. */\nexport interface IBounceOptions {\n /** \"all\", \"horizontal\", \"vertical\", or combination of \"top\", \"bottom\", \"right\", \"left\" (e.g., 'top-bottom-right') */\n sides?:\n 'all'\n | 'horizontal'\n | 'vertical'\n | string;\n\n /** Friction to apply to decelerate if active */\n friction?: number;\n\n /** Time in ms to finish bounce */\n time?: number;\n\n /** Use this bounceBox instead of (0, 0, viewport.worldWidth, viewport.worldHeight) */\n bounceBox?: Rectangle | null;\n\n /** Ease function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /** (top/bottom/center and left/right/center, or center) where to place world if too small for screen */\n underflow?: 'center' | string;\n}\n\n/** Bounce state along an axis */\nexport interface IBounceState {\n /** Elapsed time since bounce started */\n time: number;\n\n /** Starting coordinate */\n start: number;\n\n /** Change in coordinate through bounce */\n delta: number;\n\n /** Ending coordinate */\n end: number;\n}\n\nconst DEFAULT_BOUNCE_OPTIONS: Required = {\n sides: 'all',\n friction: 0.5,\n time: 150,\n ease: 'easeInOutSine',\n underflow: 'center',\n bounceBox: null\n};\n\n/**\n * @fires bounce-start-x\n * @fires bounce.end-x\n * @fires bounce-start-y\n * @fires bounce-end-y\n * @public\n */\nexport class Bounce extends Plugin\n{\n /** The options passed to initialize this plugin, cannot be modified again. */\n public readonly options: Readonly>;\n\n /** Holds whether to bounce from left side. */\n public readonly left: boolean ;\n\n /** Holds whether to bounce from top side. */\n public readonly top: boolean;\n\n /** Holds whether to bounce from right side. */\n public readonly right: boolean;\n\n /** Holds whether to bounce from bottom side. */\n public readonly bottom: boolean;\n\n /** Direction of underflow along x-axis. */\n public readonly underflowX: -1 | 0 | 1;\n\n /** Direction of underflow along y-axis. */\n public readonly underflowY: -1 | 0 | 1;\n\n /** Easing */\n protected ease: any;\n\n /** Bounce state along x-axis */\n protected toX!: IBounceState | null;\n\n /** Bounce state along y-axis */\n protected toY!: IBounceState | null;\n\n /**\n * This is called by {@link Viewport.bounce}.\n */\n constructor(parent: Viewport, options: IBounceOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_BOUNCE_OPTIONS, options);\n this.ease = ease(this.options.ease, 'easeInOutSine');\n\n if (this.options.sides)\n {\n if (this.options.sides === 'all')\n {\n this.top = this.bottom = this.left = this.right = true;\n }\n else if (this.options.sides === 'horizontal')\n {\n this.right = this.left = true;\n this.top = this.bottom = false;\n }\n else if (this.options.sides === 'vertical')\n {\n this.left = this.right = false;\n this.top = this.bottom = true;\n }\n else\n {\n this.top = this.options.sides.indexOf('top') !== -1;\n this.bottom = this.options.sides.indexOf('bottom') !== -1;\n this.left = this.options.sides.indexOf('left') !== -1;\n this.right = this.options.sides.indexOf('right') !== -1;\n }\n } else {\n this.left = this.top = this.right = this.bottom = false;\n }\n\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'center')\n {\n this.underflowX = 0;\n this.underflowY = 0;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n }\n\n this.reset();\n }\n\n public isActive(): boolean\n {\n return this.toX !== null || this.toY !== null;\n }\n\n public down(): boolean\n {\n this.toX = this.toY = null;\n\n return false;\n }\n\n public up(): boolean\n {\n this.bounce();\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n this.bounce();\n\n if (this.toX)\n {\n const toX = this.toX;\n\n toX.time += elapsed;\n this.parent.emit('moved', { viewport: this.parent, type: 'bounce-x' });\n\n if (toX.time >= this.options.time)\n {\n this.parent.x = toX.end;\n this.toX = null;\n this.parent.emit('bounce-x-end', this.parent);\n }\n else\n {\n this.parent.x = this.ease(toX.time, toX.start, toX.delta, this.options.time);\n }\n }\n\n if (this.toY)\n {\n const toY = this.toY;\n\n toY.time += elapsed;\n this.parent.emit('moved', { viewport: this.parent, type: 'bounce-y' });\n\n if (toY.time >= this.options.time)\n {\n this.parent.y = toY.end;\n this.toY = null;\n this.parent.emit('bounce-y-end', this.parent);\n }\n else\n {\n this.parent.y = this.ease(toY.time, toY.start, toY.delta, this.options.time);\n }\n }\n }\n\n /** @internal */\n protected calcUnderflowX(): number\n {\n let x: number;\n\n switch (this.underflowX)\n {\n case -1:\n x = 0;\n break;\n case 1:\n x = (this.parent.screenWidth - this.parent.screenWorldWidth);\n break;\n default:\n x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n }\n\n return x;\n }\n\n /** @internal */\n protected calcUnderflowY(): number\n {\n let y: number;\n\n switch (this.underflowY)\n {\n case -1:\n y = 0;\n break;\n case 1:\n y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n break;\n default:\n y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n }\n\n return y;\n }\n\n private oob(): Record<'left' | 'right' | 'top' | 'bottom', boolean> & Record<'topLeft' | 'bottomRight', Point>\n {\n const box = this.options.bounceBox;\n\n if (box)\n {\n const x1 = typeof box.x === 'undefined' ? 0 : box.x;\n const y1 = typeof box.y === 'undefined' ? 0 : box.y;\n const width = typeof box.width === 'undefined' ? this.parent.worldWidth : box.width;\n const height = typeof box.height === 'undefined' ? this.parent.worldHeight : box.height;\n\n return {\n left: this.parent.left < x1,\n right: this.parent.right > width,\n top: this.parent.top < y1,\n bottom: this.parent.bottom > height,\n topLeft: new Point(\n x1 * this.parent.scale.x,\n y1 * this.parent.scale.y\n ),\n bottomRight: new Point(\n width * this.parent.scale.x - this.parent.screenWidth,\n height * this.parent.scale.y - this.parent.screenHeight\n )\n };\n }\n\n return {\n left: this.parent.left < 0,\n right: this.parent.right > this.parent.worldWidth,\n top: this.parent.top < 0,\n bottom: this.parent.bottom > this.parent.worldHeight,\n topLeft: new Point(0, 0),\n bottomRight: new Point(\n this.parent.worldWidth * this.parent.scale.x - this.parent.screenWidth,\n this.parent.worldHeight * this.parent.scale.y - this.parent.screenHeight\n )\n };\n }\n\n public bounce(): void\n {\n if (this.paused)\n {\n return;\n }\n\n let oob;\n let decelerate: undefined | null | {\n percentChangeX?: number;\n percentChangeY?: number;\n x?: number;\n y?: number;\n options?: IDecelerateOptions\n } = this.parent.plugins.get('decelerate', true) as any;\n\n if (decelerate && (decelerate.x || decelerate.y))\n {\n if ((decelerate.x && decelerate.percentChangeX === decelerate.options?.friction) || (decelerate.y && decelerate.percentChangeY === decelerate.options?.friction))\n {\n oob = this.oob();\n if ((oob.left && this.left) || (oob.right && this.right))\n {\n decelerate.percentChangeX = this.options.friction;\n }\n if ((oob.top && this.top) || (oob.bottom && this.bottom))\n {\n decelerate.percentChangeY = this.options.friction;\n }\n }\n }\n const drag: Partial = this.parent.plugins.get('drag', true) || {};\n const pinch: Partial = this.parent.plugins.get('pinch', true) || {};\n\n decelerate = decelerate || {};\n\n if (!drag?.active && !pinch?.active && ((!this.toX || !this.toY) && (!decelerate.x || !decelerate.y)))\n {\n oob = oob || this.oob();\n const topLeft = oob.topLeft;\n const bottomRight = oob.bottomRight;\n\n if (!this.toX && !decelerate.x)\n {\n let x = null;\n\n if (oob.left && this.left)\n {\n x = (this.parent.screenWorldWidth < this.parent.screenWidth) ? this.calcUnderflowX() : -topLeft.x;\n }\n else if (oob.right && this.right)\n {\n x = (this.parent.screenWorldWidth < this.parent.screenWidth) ? this.calcUnderflowX() : -bottomRight.x;\n }\n if (x !== null && this.parent.x !== x)\n {\n this.toX = { time: 0, start: this.parent.x, delta: x - this.parent.x, end: x };\n this.parent.emit('bounce-x-start', this.parent);\n }\n }\n if (!this.toY && !decelerate.y)\n {\n let y = null;\n\n if (oob.top && this.top)\n {\n y = (this.parent.screenWorldHeight < this.parent.screenHeight) ? this.calcUnderflowY() : -topLeft.y;\n }\n else if (oob.bottom && this.bottom)\n {\n y = (this.parent.screenWorldHeight < this.parent.screenHeight) ? this.calcUnderflowY() : -bottomRight.y;\n }\n if (y !== null && this.parent.y !== y)\n {\n this.toY = { time: 0, start: this.parent.y, delta: y - this.parent.y, end: y };\n this.parent.emit('bounce-y-start', this.parent);\n }\n }\n }\n }\n\n public reset(): void\n {\n this.toX = this.toY = null;\n this.bounce();\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\n\n/**\n * There are three ways to clamp:\n * 1. direction: 'all' = the world is clamped to its world boundaries, ie, you cannot drag any part of the world offscreen\n * direction: 'x' | 'y' = only the x or y direction is clamped to its world boundary\n * 2. left, right, top, bottom = true | number = the world is clamped to the world's pixel location for each side;\n * if any of these are set to true, then the location is set to the boundary [0, viewport.worldWidth/viewport.worldHeight]\n * eg: to allow the world to be completely dragged offscreen, set [-viewport.worldWidth, -viewport.worldHeight, viewport.worldWidth * 2, viewport.worldHeight * 2]\n *\n * Underflow determines what happens when the world is smaller than the viewport\n * 1. none = the world is clamped but there is no special behavior\n * 2. center = the world is centered on the viewport\n * 3. combination of top/bottom/center and left/right/center (case insensitive) = the world is stuck to the appropriate boundaries\n *\n */\nexport interface IClampOptions\n{\n /**\n * Clamp left; true = 0\n *\n * @default false\n */\n left?: number | boolean | null;\n\n /**\n * Clamp top; true = 0\n *\n * @default false\n */\n top?: number | boolean | null;\n\n /**\n * Clamp right; true = viewport.worldWidth\n *\n * @default false\n */\n right?: number | boolean | null;\n\n /**\n * Clamp bottom; true = viewport.worldHeight\n *\n * @default false\n */\n bottom?: number | boolean | null;\n\n /**\n * (all, x, or y) using clamps of [0, viewport.worldWidth/viewport.worldHeight]; replaces left/right/top/bottom if set\n *\n * @default null\n */\n direction?: 'all' | 'x' | 'y' | null;\n\n /**\n * Where to place world if too small for screen (e.g., top-right, center, none, bottomleft)\n *\n * @default 'center'\n */\n underflow?: 'center' | string;\n}\n\nconst DEFAULT_CLAMP_OPTIONS: Required = {\n left: false,\n right: false,\n top: false,\n bottom: false,\n direction: null,\n underflow: 'center'\n};\n\n/**\n * Plugin to clamp the viewport to a specific world bounding box.\n *\n * @public\n */\nexport class Clamp extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Required;\n\n /** Last state of viewport */\n protected last: {\n x: number | null;\n y: number | null;\n scaleX: number | null;\n scaleY: number | null;\n };\n\n protected noUnderflow!: boolean;\n protected underflowX!: -1 | 0 | 1;\n protected underflowY!: -1 | 0 | 1;\n\n /**\n * This is called by {@link Viewport.clamp}.\n */\n constructor(parent: Viewport, options : IClampOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_CLAMP_OPTIONS, options);\n\n if (this.options.direction)\n {\n this.options.left = this.options.direction === 'x' || this.options.direction === 'all' ? true : null;\n this.options.right = this.options.direction === 'x' || this.options.direction === 'all' ? true : null;\n this.options.top = this.options.direction === 'y' || this.options.direction === 'all' ? true : null;\n this.options.bottom = this.options.direction === 'y' || this.options.direction === 'all' ? true : null;\n }\n\n this.parseUnderflow();\n this.last = { x: null, y: null, scaleX: null, scaleY: null };\n this.update();\n }\n\n private parseUnderflow()\n {\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'none')\n {\n this.noUnderflow = true;\n }\n else if (clamp === 'center')\n {\n this.underflowX = this.underflowY = 0;\n this.noUnderflow = false;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n this.noUnderflow = false;\n }\n }\n\n public move(): boolean\n {\n this.update();\n\n return false;\n }\n\n public update(): void\n {\n if (this.paused)\n {\n return;\n }\n\n // only clamp on change\n if (this.parent.x === this.last.x\n && this.parent.y === this.last.y\n && this.parent.scale.x === this.last.scaleX\n && this.parent.scale.y === this.last.scaleY)\n {\n return;\n }\n const original = { x: this.parent.x, y: this.parent.y };\n // TODO: Fix\n const decelerate: any = (this.parent.plugins as any).decelerate || {};\n\n if (this.options.left !== null || this.options.right !== null)\n {\n let moved = false;\n\n if (!this.noUnderflow && this.parent.screenWorldWidth < this.parent.screenWidth)\n {\n switch (this.underflowX)\n {\n case -1:\n if (this.parent.x !== 0)\n {\n this.parent.x = 0;\n moved = true;\n }\n break;\n case 1:\n if (this.parent.x !== this.parent.screenWidth - this.parent.screenWorldWidth)\n {\n this.parent.x = this.parent.screenWidth - this.parent.screenWorldWidth;\n moved = true;\n }\n break;\n default:\n if (this.parent.x !== (this.parent.screenWidth - this.parent.screenWorldWidth) / 2)\n {\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n moved = true;\n }\n }\n }\n else\n {\n if (this.options.left !== null)\n {\n if (this.parent.left < (this.options.left === true ? 0 : this.options.left))\n {\n this.parent.x = -(this.options.left === true ? 0 : this.options.left) * this.parent.scale.x;\n decelerate.x = 0;\n moved = true;\n }\n }\n if (this.options.right !== null)\n {\n if (this.parent.right > (this.options.right === true ? this.parent.worldWidth : this.options.right))\n {\n this.parent.x = -(this.options.right === true ? this.parent.worldWidth : this.options.right) * this.parent.scale.x + this.parent.screenWidth;\n decelerate.x = 0;\n moved = true;\n }\n }\n }\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, original, type: 'clamp-x' });\n }\n }\n if (this.options.top !== null || this.options.bottom !== null)\n {\n let moved = false;\n\n if (!this.noUnderflow && this.parent.screenWorldHeight < this.parent.screenHeight)\n {\n switch (this.underflowY)\n {\n case -1:\n if (this.parent.y !== 0)\n {\n this.parent.y = 0;\n moved = true;\n }\n break;\n case 1:\n if (this.parent.y !== this.parent.screenHeight - this.parent.screenWorldHeight)\n {\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n moved = true;\n }\n break;\n default:\n if (this.parent.y !== (this.parent.screenHeight - this.parent.screenWorldHeight) / 2)\n {\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n moved = true;\n }\n }\n }\n else\n {\n if (this.options.top !== null)\n {\n if (this.parent.top < (this.options.top === true ? 0 : this.options.top))\n {\n this.parent.y = -(this.options.top === true ? 0 : this.options.top)\n * this.parent.scale.y;\n decelerate.y = 0;\n moved = true;\n }\n }\n if (this.options.bottom !== null)\n {\n if (this.parent.bottom > (this.options.bottom === true ? this.parent.worldHeight : this.options.bottom))\n {\n this.parent.y = -(this.options.bottom === true ? this.parent.worldHeight : this.options.bottom)\n * this.parent.scale.y + this.parent.screenHeight;\n decelerate.y = 0;\n moved = true;\n }\n }\n }\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, original, type: 'clamp-y' });\n }\n }\n this.last.x = this.parent.x;\n this.last.y = this.parent.y;\n this.last.scaleX = this.parent.scale.x;\n this.last.scaleY = this.parent.scale.y;\n }\n\n public reset(): void\n {\n this.update();\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\n\n/**\n * Options for {@link ClampZoom}.\n *\n * Use either minimum width/height or minimum scale\n */\nexport interface IClampZoomOptions\n{\n /** Minimum width */\n minWidth?: number | null;\n\n /** Minimum height */\n minHeight?: number | null;\n\n /** Maximum width */\n maxWidth?: number | null;\n\n /** Maximum height */\n maxHeight?: number | null;\n\n /** Minimum scale */\n minScale?: number | null | IScale;\n\n /** Maximum scale */\n maxScale?: number | null | IScale;\n}\n\nconst DEFAULT_CLAMP_ZOOM_OPTIONS: Required = {\n minWidth: null,\n minHeight: null,\n maxWidth: null,\n maxHeight: null,\n minScale: null,\n maxScale: null\n};\n\n/**\n * Plugin to clamp the viewport's zoom to a specific range.\n *\n * @public\n */\nexport class ClampZoom extends Plugin\n{\n public readonly options: Required;\n\n /**\n * This is called by {@link Viewport.clampZoom}.\n */\n constructor(parent: Viewport, options = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_CLAMP_ZOOM_OPTIONS, options);\n\n this.clamp();\n }\n\n public resize(): void\n {\n this.clamp();\n }\n\n /** Clamp the viewport scale zoom) */\n public clamp(): void\n {\n if (this.paused)\n {\n return;\n }\n\n if (this.options.minWidth || this.options.minHeight || this.options.maxWidth || this.options.maxHeight)\n {\n let width = this.parent.worldScreenWidth;\n let height = this.parent.worldScreenHeight;\n\n if (this.options.minWidth !== null && width < this.options.minWidth)\n {\n const original = this.parent.scale.x;\n\n this.parent.fitWidth(this.options.minWidth, false, false, true);\n this.parent.scale.y *= this.parent.scale.x / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.maxWidth !== null && width > this.options.maxWidth)\n {\n const original = this.parent.scale.x;\n\n this.parent.fitWidth(this.options.maxWidth, false, false, true);\n this.parent.scale.y *= this.parent.scale.x / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.minHeight !== null && height < this.options.minHeight)\n {\n const original = this.parent.scale.y;\n\n this.parent.fitHeight(this.options.minHeight, false, false, true);\n this.parent.scale.x *= this.parent.scale.y / original;\n width = this.parent.worldScreenWidth;\n height = this.parent.worldScreenHeight;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n if (this.options.maxHeight !== null && height > this.options.maxHeight)\n {\n const original = this.parent.scale.y;\n\n this.parent.fitHeight(this.options.maxHeight, false, false, true);\n this.parent.scale.x *= this.parent.scale.y / original;\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n }\n else\n if (this.options.minScale || this.options.maxScale)\n {\n const minScale: IScale = { x: null, y: null };\n const maxScale: IScale = { x: null, y: null };\n\n if (typeof this.options.minScale === 'number')\n {\n minScale.x = this.options.minScale;\n minScale.y = this.options.minScale;\n }\n else if (this.options.minScale !== null)\n {\n const optsMinScale = this.options.minScale as IScale;\n\n minScale.x = typeof optsMinScale.x === 'undefined' ? null : optsMinScale.x;\n minScale.y = typeof optsMinScale.y === 'undefined' ? null : optsMinScale.y;\n }\n\n if (typeof this.options.maxScale === 'number')\n {\n maxScale.x = this.options.maxScale;\n maxScale.y = this.options.maxScale;\n }\n else if (this.options.maxScale !== null)\n {\n const optsMaxScale = this.options.maxScale as IScale;\n\n maxScale.x = typeof optsMaxScale.x === 'undefined' ? null : optsMaxScale.x;\n maxScale.y = typeof optsMaxScale.y === 'undefined' ? null : optsMaxScale.y;\n }\n\n let scaleX = this.parent.scale.x;\n let scaleY = this.parent.scale.y;\n\n if (minScale.x !== null && scaleX < minScale.x)\n {\n scaleX = minScale.x;\n }\n if (maxScale.x !== null && scaleX > maxScale.x)\n {\n scaleX = maxScale.x;\n }\n if (minScale.y !== null && scaleY < minScale.y)\n {\n scaleY = minScale.y;\n }\n if (maxScale.y !== null && scaleY > maxScale.y)\n {\n scaleY = maxScale.y;\n }\n if (scaleX !== this.parent.scale.x || scaleY !== this.parent.scale.y)\n {\n this.parent.scale.set(scaleX, scaleY);\n this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' });\n }\n }\n }\n\n public reset(): void\n {\n this.clamp();\n }\n}\n\n/** This allows independent x and y values for min/maxScale */\nexport interface IScale {\n x: null | number\n y: null | number\n}\n","import { Plugin } from './Plugin';\n\nimport type { Point } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\nexport interface IDecelerateOptions {\n /**\n * Percent to decelerate after movement. This should be between 0 and 1, exclusive.\n *\n * @default 0.95\n */\n friction?: number;\n\n /**\n * Percent to decelerate when past boundaries (only applicable when viewport.bounce() is active)\n *\n * @default 0.8\n */\n bounce?: number;\n\n /**\n * Minimum velocity before stopping/reversing acceleration\n *\n * @default 0.01\n */\n minSpeed?: number;\n}\n\n/** Viewport position snapshot that's saved by {@link DeceleratePlugin} to estimate panning velocity. */\nexport interface IDecelerateSnapshot {\n /** x-coordinate of the viewport. */\n x: number;\n\n /** y-coordinate of the viewport. */\n y: number;\n\n /** Time at which this snapshot was taken. */\n time: number;\n}\n\nconst DEFAULT_DECELERATE_OPTIONS: Required = {\n friction: 0.98,\n bounce: 0.8,\n minSpeed: 0.01\n};\n\n/**\n * Time period of decay (1 frame)\n *\n * @internal\n * @ignore\n */\nconst TP = 16;\n\n/**\n * Plugin to decelerate viewport velocity smoothly after panning ends.\n *\n * @public\n */\nexport class Decelerate extends Plugin\n{\n /** Options used to initialize this plugin. */\n public readonly options: Required;\n\n /**\n * x-component of the velocity of viewport provided by this plugin, at the current time.\n *\n * This is measured in px/frame, where a frame is normalized to 16 milliseconds.\n */\n public x!: number | null;\n\n /**\n * y-component of the velocity of the viewport provided by this plugin, at the current time.\n *\n * This is measured in px/frame, where a frame is normalized to 16 milliseconds.\n */\n public y!: number | null;\n\n /**\n * The decay factor for the x-component of the viewport.\n *\n * The viewport's velocity decreased by this amount each 16 milliseconds.\n */\n public percentChangeX!: number;\n\n /**\n * The decay factor for the y-component of the viewport.\n *\n * The viewport's velocity decreased by this amount each 16 milliseconds.\n */\n public percentChangeY!: number;\n\n /** Saved list of recent viewport position snapshots, to estimate velocity. */\n protected saved: Array;\n\n /** The time since the user released panning of the viewport. */\n protected timeSinceRelease: number;\n\n /**\n * This is called by {@link Viewport.decelerate}.\n */\n constructor(parent: Viewport, options: IDecelerateOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_DECELERATE_OPTIONS, options);\n this.saved = [];\n this.timeSinceRelease = 0;\n\n this.reset();\n this.parent.on('moved', (data) => this.moved(data));\n }\n\n public down(): boolean\n {\n this.saved = [];\n this.x = this.y = null;\n\n return false;\n }\n\n public isActive(): boolean\n {\n return !!(this.x || this.y);\n }\n\n public move(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n const count = this.parent.input.count();\n\n if (count === 1 || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n this.saved.push({ x: this.parent.x, y: this.parent.y, time: performance.now() });\n\n if (this.saved.length > 60)\n {\n this.saved.splice(0, 30);\n }\n }\n\n // Silently recording viewport positions\n return false;\n }\n\n /** Listener to viewport's \"moved\" event. */\n protected moved(data: { type: 'clamp-x' | 'clamp-y'; original: Point }): void\n {\n if (this.saved.length)\n {\n const last = this.saved[this.saved.length - 1];\n\n if (data.type === 'clamp-x')\n {\n if (last.x === data.original.x)\n {\n last.x = this.parent.x;\n }\n }\n else if (data.type === 'clamp-y')\n {\n if (last.y === data.original.y)\n {\n last.y = this.parent.y;\n }\n }\n }\n }\n\n public up(): boolean\n {\n if (this.parent.input.count() === 0 && this.saved.length)\n {\n const now = performance.now();\n\n for (const save of this.saved)\n {\n if (save.time >= now - 100)\n {\n const time = now - save.time;\n\n this.x = (this.parent.x - save.x) / time;\n this.y = (this.parent.y - save.y) / time;\n this.percentChangeX = this.percentChangeY = this.options.friction;\n this.timeSinceRelease = 0;\n break;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Manually activate deceleration, starting from the (x, y) velocity components passed in the options.\n *\n * @param {object} options\n * @param {number} [options.x] - Specify x-component of initial velocity.\n * @param {number} [options.y] - Specify y-component of initial velocity.\n */\n public activate(options: { x?: number; y?: number; }): void\n {\n options = options || {};\n\n if (typeof options.x !== 'undefined')\n {\n this.x = options.x;\n this.percentChangeX = this.options.friction;\n }\n if (typeof options.y !== 'undefined')\n {\n this.y = options.y;\n this.percentChangeY = this.options.friction;\n }\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n /*\n * See https://github.com/davidfig/pixi-viewport/issues/271 for math.\n *\n * The viewport velocity (this.x, this.y) decays exponentially by the the decay factor\n * (this.percentChangeX, this.percentChangeY) each frame. This velocity function is integrated\n * to calculate the displacement.\n */\n\n const moved = this.x || this.y;\n\n const ti = this.timeSinceRelease;\n const tf = this.timeSinceRelease + elapsed;\n\n if (this.x)\n {\n const k = this.percentChangeX;\n const lnk = Math.log(k);\n\n // Apply velocity delta on the viewport x-coordinate.\n this.parent.x += ((this.x * TP) / lnk) * (Math.pow(k, tf / TP) - Math.pow(k, ti / TP));\n\n // Apply decay on x-component of velocity\n this.x *= Math.pow(this.percentChangeX, elapsed / TP);\n }\n if (this.y)\n {\n const k = this.percentChangeY;\n const lnk = Math.log(k);\n\n // Apply velocity delta on the viewport y-coordinate.\n this.parent.y += ((this.y * TP) / lnk) * (Math.pow(k, tf / TP) - Math.pow(k, ti / TP));\n\n // Apply decay on y-component of velocity\n this.y *= Math.pow(this.percentChangeY, elapsed / TP);\n }\n\n this.timeSinceRelease += elapsed;\n\n // End decelerate velocity once it goes under a certain amount of precision.\n if (Math.abs(this.x || 0) < this.options.minSpeed)\n {\n this.x = 0;\n }\n if (Math.abs(this.y || 0) < this.options.minSpeed)\n {\n this.y = 0;\n }\n\n if (moved)\n {\n this.parent.emit('moved', { viewport: this.parent, type: 'decelerate' });\n }\n }\n\n public reset(): void\n {\n this.x = this.y = null;\n }\n}\n","import { Point } from '@pixi/math';\nimport { Plugin } from './Plugin';\n\nimport type { Decelerate } from './Decelerate';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { IPointData } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Drag}. */\nexport interface IDragOptions {\n /**\n * direction to drag\n *\n * @default \"all\"\n */\n direction?: string;\n\n /**\n * whether click to drag is active\n *\n * @default true\n */\n pressDrag?: boolean;\n\n /**\n * Use wheel to scroll in direction (unless wheel plugin is active)\n *\n * @default true\n */\n wheel?: boolean;\n\n /**\n * number of pixels to scroll with each wheel spin\n *\n * @default 1\n */\n wheelScroll?: number;\n\n /**\n * reverse the direction of the wheel scroll\n *\n * @default false\n */\n reverse?: boolean;\n\n /**\n * clamp wheel(to avoid weird bounce with mouse wheel). Can be 'x' or 'y' or `true`.\n *\n * @default false\n */\n clampWheel?: boolean | string;\n\n /**\n * where to place world if too small for screen\n *\n * @default \"center\"\n */\n underflow?: string;\n\n /**\n * factor to multiply drag to increase the speed of movement\n *\n * @default 1\n */\n factor?: number;\n\n /**\n * Changes which mouse buttons trigger drag.\n *\n * Use: 'all', 'left', right' 'middle', or some combination, like, 'middle-right'; you may want to set\n * `viewport.options.disableOnContextMenu` if you want to use right-click dragging.\n *\n * @default \"all\"\n */\n mouseButtons?: 'all' | string;\n\n /**\n * Array containing {@link key|https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code} codes of\n * keys that can be pressed for the drag to be triggered, e.g.: ['ShiftLeft', 'ShiftRight'}.\n *\n * @default null\n */\n keyToPress?: string[] | null;\n\n /**\n * Ignore keyToPress for touch events.\n *\n * @default false\n */\n ignoreKeyToPressOnTouch?: boolean;\n\n /**\n * Scaling factor for non-DOM_DELTA_PIXEL scrolling events.\n *\n * @default 20\n */\n lineHeight?: number;\n}\n\nconst DEFAULT_DRAG_OPTIONS: Required = {\n direction: 'all',\n pressDrag: true,\n wheel: true,\n wheelScroll: 1,\n reverse: false,\n clampWheel: false,\n underflow: 'center',\n factor: 1,\n mouseButtons: 'all',\n keyToPress: null,\n ignoreKeyToPressOnTouch: false,\n lineHeight: 20,\n};\n\n/**\n * Plugin to enable panning/dragging of the viewport to move around.\n *\n * @public\n */\nexport class Drag extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Readonly>;\n\n /** Flags when viewport is moving. */\n protected moved: boolean;\n\n /** Factor to apply from {@link IDecelerateOptions}'s reverse. */\n protected reverse: 1 | -1;\n\n /** Holds whether dragging is enabled along the x-axis. */\n protected xDirection: boolean\n\n /** Holds whether dragging is enabled along the y-axis. */\n protected yDirection: boolean;\n\n /** Flags whether the keys required to drag are pressed currently. */\n protected keyIsPressed: boolean;\n\n /** Holds whether the left, center, and right buttons are required to pan. */\n protected mouse!: [boolean, boolean, boolean]\n\n /** Underflow factor along x-axis */\n protected underflowX!: -1 | 0 | 1;\n\n /** Underflow factor along y-axis */\n protected underflowY!: -1 | 0 | 1;\n\n /** Last pointer position while panning. */\n protected last?: IPointData | null\n\n /** The ID of the pointer currently panning the viewport. */\n protected current?: number;\n\n /**\n * This is called by {@link Viewport.drag}.\n */\n constructor(parent: Viewport, options = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_DRAG_OPTIONS, options);\n this.moved = false;\n this.reverse = this.options.reverse ? 1 : -1;\n this.xDirection = !this.options.direction || this.options.direction === 'all' || this.options.direction === 'x';\n this.yDirection = !this.options.direction || this.options.direction === 'all' || this.options.direction === 'y';\n this.keyIsPressed = false;\n\n this.parseUnderflow();\n this.mouseButtons(this.options.mouseButtons);\n\n if (this.options.keyToPress)\n {\n this.handleKeyPresses(this.options.keyToPress);\n }\n }\n\n /**\n * Handles keypress events and set the keyIsPressed boolean accordingly\n *\n * @param {array} codes - key codes that can be used to trigger drag event\n */\n protected handleKeyPresses(codes: string[]): void\n {\n window.addEventListener('keydown', (e) =>\n {\n if (codes.includes(e.code))\n { this.keyIsPressed = true; }\n });\n\n window.addEventListener('keyup', (e) =>\n {\n if (codes.includes(e.code))\n { this.keyIsPressed = false; }\n });\n }\n\n /**\n * initialize mousebuttons array\n * @param {string} buttons\n */\n protected mouseButtons(buttons: string): void\n {\n if (!buttons || buttons === 'all')\n {\n this.mouse = [true, true, true];\n }\n else\n {\n this.mouse = [\n buttons.indexOf('left') !== -1,\n buttons.indexOf('middle') !== -1,\n buttons.indexOf('right') !== -1\n ];\n }\n }\n\n protected parseUnderflow(): void\n {\n const clamp = this.options.underflow.toLowerCase();\n\n if (clamp === 'center')\n {\n this.underflowX = 0;\n this.underflowY = 0;\n }\n else\n {\n this.underflowX = (clamp.indexOf('left') !== -1) ? -1 : (clamp.indexOf('right') !== -1) ? 1 : 0;\n this.underflowY = (clamp.indexOf('top') !== -1) ? -1 : (clamp.indexOf('bottom') !== -1) ? 1 : 0;\n }\n }\n\n /**\n * @param {PIXI.InteractionEvent} event\n * @returns {boolean}\n */\n protected checkButtons(event: InteractionEvent): boolean\n {\n const isMouse = event.data.pointerType === 'mouse';\n const count = this.parent.input.count();\n\n if ((count === 1) || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n if (!isMouse || this.mouse[event.data.button])\n {\n return true;\n }\n }\n\n return false;\n }\n\n /**\n * @param {PIXI.InteractionEvent} event\n * @returns {boolean}\n */\n protected checkKeyPress(event: InteractionEvent): boolean\n {\n return (!this.options.keyToPress\n || this.keyIsPressed\n || (this.options.ignoreKeyToPressOnTouch && event.data.pointerType === 'touch'));\n }\n\n public down(event: InteractionEvent): boolean\n {\n if (this.paused || !this.options.pressDrag)\n {\n return false;\n }\n if (this.checkButtons(event) && this.checkKeyPress(event))\n {\n this.last = { x: event.data.global.x, y: event.data.global.y };\n this.current = event.data.pointerId;\n\n return true;\n }\n this.last = null;\n\n return false;\n }\n\n get active(): boolean\n {\n return this.moved;\n }\n\n public move(event: InteractionEvent): boolean\n {\n if (this.paused || !this.options.pressDrag)\n {\n return false;\n }\n if (this.last && this.current === event.data.pointerId)\n {\n const x = event.data.global.x;\n const y = event.data.global.y;\n const count = this.parent.input.count();\n\n if (count === 1 || (count > 1 && !this.parent.plugins.get('pinch', true)))\n {\n const distX = x - this.last.x;\n const distY = y - this.last.y;\n\n if (this.moved\n || ((this.xDirection && this.parent.input.checkThreshold(distX))\n || (this.yDirection && this.parent.input.checkThreshold(distY))))\n {\n const newPoint = { x, y };\n\n if (this.xDirection)\n {\n this.parent.x += (newPoint.x - this.last.x) * this.options.factor;\n }\n if (this.yDirection)\n {\n this.parent.y += (newPoint.y - this.last.y) * this.options.factor;\n }\n this.last = newPoint;\n if (!this.moved)\n {\n this.parent.emit('drag-start', {\n event,\n screen: new Point(this.last.x, this.last.y),\n world: this.parent.toWorld(new Point(this.last.x, this.last.y)),\n viewport: this.parent\n });\n }\n this.moved = true;\n this.parent.emit('moved', { viewport: this.parent, type: 'drag' });\n\n return true;\n }\n }\n else\n {\n this.moved = false;\n }\n }\n\n return false;\n }\n\n public up(event: InteractionEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n const touches = this.parent.input.touches;\n\n if (touches.length === 1)\n {\n const pointer = touches[0];\n\n if (pointer.last)\n {\n this.last = { x: pointer.last.x, y: pointer.last.y };\n this.current = pointer.id;\n }\n this.moved = false;\n\n return true;\n }\n else if (this.last)\n {\n if (this.moved)\n {\n const screen = new Point(this.last.x, this.last.y);\n\n this.parent.emit('drag-end', {\n event, screen,\n world: this.parent.toWorld(screen),\n viewport: this.parent,\n });\n this.last = null;\n this.moved = false;\n\n return true;\n }\n }\n\n return false;\n }\n\n public wheel(event: WheelEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n\n if (this.options.wheel)\n {\n const wheel = this.parent.plugins.get('wheel', true);\n\n if (!wheel)\n {\n const step = event.deltaMode ? this.options.lineHeight : 1;\n\n if (this.xDirection)\n {\n this.parent.x += event.deltaX * step * this.options.wheelScroll * this.reverse;\n }\n if (this.yDirection)\n {\n this.parent.y += event.deltaY * step * this.options.wheelScroll * this.reverse;\n }\n if (this.options.clampWheel)\n {\n this.clamp();\n }\n this.parent.emit('wheel-scroll', this.parent);\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n if (!this.parent.options.passiveWheel)\n {\n event.preventDefault();\n }\n\n return true;\n }\n }\n\n return false;\n }\n\n public resume()\n {\n this.last = null;\n this.paused = false;\n }\n\n public clamp()\n {\n const decelerate: Partial = this.parent.plugins.get('decelerate', true) || {};\n\n if (this.options.clampWheel !== 'y')\n {\n if (this.parent.screenWorldWidth < this.parent.screenWidth)\n {\n switch (this.underflowX)\n {\n case -1:\n this.parent.x = 0;\n break;\n case 1:\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth);\n break;\n default:\n this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2;\n }\n }\n else\n if (this.parent.left < 0)\n {\n this.parent.x = 0;\n decelerate.x = 0;\n }\n else if (this.parent.right > this.parent.worldWidth)\n {\n this.parent.x = -this.parent.worldWidth * this.parent.scale.x + this.parent.screenWidth;\n decelerate.x = 0;\n }\n }\n if (this.options.clampWheel !== 'x')\n {\n if (this.parent.screenWorldHeight < this.parent.screenHeight)\n {\n switch (this.underflowY)\n {\n case -1:\n this.parent.y = 0;\n break;\n case 1:\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight);\n break;\n default:\n this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2;\n }\n }\n else\n {\n if (this.parent.top < 0)\n {\n this.parent.y = 0;\n decelerate.y = 0;\n }\n if (this.parent.bottom > this.parent.worldHeight)\n {\n this.parent.y = -this.parent.worldHeight * this.parent.scale.y + this.parent.screenHeight;\n decelerate.y = 0;\n }\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { DisplayObject } from '@pixi/display';\nimport type { IPointData } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Follow}. */\nexport interface IFollowOptions\n{\n /**\n * Speed to follow in px/frame (0 = teleport to location)\n *\n * @default 9\n */\n speed?: number;\n\n /**\n * Set acceleration to accelerate and decelerate at this rate; speed cannot be 0 to use acceleration\n *\n * @default null\n */\n acceleration?: number | null;\n\n /**\n * Radius (in world coordinates) of center circle where movement is allowed without moving the viewport\n *\n * @default null\n */\n radius?: number | null;\n}\n\nconst DEFAULT_FOLLOW_OPTIONS: Required = {\n speed: 0,\n acceleration: null,\n radius: null\n};\n\n/**\n * Plugin to follow a display-object.\n *\n * @see Viewport.follow\n * @public\n */\nexport class Follow extends Plugin\n{\n /** The options used to initialize this plugin. */\n public readonly options: Required;\n\n /** The target this plugin will make the viewport follow. */\n public target: DisplayObject;\n\n /** The velocity provided the viewport by following, at the current time. */\n protected velocity: IPointData;\n\n /**\n * This is called by {@link Viewport.follow}.\n *\n * @param parent\n * @param target - target to follow\n * @param options\n */\n constructor(parent: Viewport, target: DisplayObject, options: IFollowOptions = {})\n {\n super(parent);\n\n this.target = target;\n this.options = Object.assign({}, DEFAULT_FOLLOW_OPTIONS, options);\n this.velocity = { x: 0, y: 0 };\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n\n const center = this.parent.center;\n let toX = this.target.x;\n let toY = this.target.y;\n\n if (this.options.radius)\n {\n const distance = Math.sqrt(Math.pow(this.target.y - center.y, 2) + Math.pow(this.target.x - center.x, 2));\n\n if (distance > this.options.radius)\n {\n const angle = Math.atan2(this.target.y - center.y, this.target.x - center.x);\n\n toX = this.target.x - (Math.cos(angle) * this.options.radius);\n toY = this.target.y - (Math.sin(angle) * this.options.radius);\n }\n else\n {\n return;\n }\n }\n\n const deltaX = toX - center.x;\n const deltaY = toY - center.y;\n\n if (deltaX || deltaY)\n {\n if (this.options.speed)\n {\n if (this.options.acceleration)\n {\n const angle = Math.atan2(toY - center.y, toX - center.x);\n const distance = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));\n\n if (distance)\n {\n const decelerationDistance = (Math.pow(this.velocity.x, 2) + Math.pow(this.velocity.y, 2)) / (2 * this.options.acceleration);\n\n if (distance > decelerationDistance)\n {\n this.velocity = {\n x: Math.min(this.velocity.x + this.options.acceleration * elapsed, this.options.speed),\n y: Math.min(this.velocity.y + this.options.acceleration * elapsed, this.options.speed)\n };\n }\n else\n {\n this.velocity = {\n x: Math.max(this.velocity.x - this.options.acceleration * this.options.speed, 0),\n y: Math.max(this.velocity.y - this.options.acceleration * this.options.speed, 0)\n };\n }\n const changeX = Math.cos(angle) * this.velocity.x;\n const changeY = Math.sin(angle) * this.velocity.y;\n const x = Math.abs(changeX) > Math.abs(deltaX) ? toX : center.x + changeX;\n const y = Math.abs(changeY) > Math.abs(deltaY) ? toY : center.y + changeY;\n\n this.parent.moveCenter(x, y);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n else\n {\n const angle = Math.atan2(toY - center.y, toX - center.x);\n const changeX = Math.cos(angle) * this.options.speed;\n const changeY = Math.sin(angle) * this.options.speed;\n const x = Math.abs(changeX) > Math.abs(deltaX) ? toX : center.x + changeX;\n const y = Math.abs(changeY) > Math.abs(deltaY) ? toY : center.y + changeY;\n\n this.parent.moveCenter(x, y);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n else\n {\n this.parent.moveCenter(toX, toY);\n this.parent.emit('moved', { viewport: this.parent, type: 'follow' });\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\n\nimport type { Viewport } from '../Viewport';\nimport type { InteractionEvent } from '@pixi/interaction';\n\n/** Insets for mouse edges scrolling regions */\nexport interface IMouseEdgesInsets {\n /** Distance from center of screen in screen pixels */\n radius?: number | null;\n\n /** Distance from all sides in screen pixels */\n distance?: number | null;\n\n /** Alternatively, set top distance (leave unset for no top scroll) */\n top?: number | null;\n\n /** Alternatively, set bottom distance (leave unset for no top scroll) */\n bottom?: number | null;\n\n /** Alternatively, set left distance (leave unset for no top scroll) */\n left?: number | null;\n\n /** Alternatively, set right distance (leave unset for no top scroll) */\n right?: number | null;\n}\n\n/** Options for {@link MouseEdges}. */\nexport interface IMouseEdgesOptions extends IMouseEdgesInsets {\n /** Speed in pixels/frame to scroll viewport */\n speed?: number;\n\n /** Reverse direction of scroll */\n reverse?: boolean;\n\n /** Don't use decelerate plugin even if it's installed */\n noDecelerate?: boolean;\n\n /**\n * If using radius, use linear movement (+/- 1, +/- 1) instead of angled movement.\n *\n * (Math.cos(angle from center), Math.sin(angle from center))\n */\n linear?: boolean;\n\n /** Allows plugin to continue working even when there's a `mousedown` event. */\n allowButtons?: boolean;\n}\n\nconst MOUSE_EDGES_OPTIONS: Required = {\n radius: null,\n distance: null,\n top: null,\n bottom: null,\n left: null,\n right: null,\n speed: 8,\n reverse: false,\n noDecelerate: false,\n linear: false,\n allowButtons: false,\n};\n\n/**\n * Scroll viewport when mouse hovers near one of the edges.\n *\n * @event mouse-edge-start(Viewport) emitted when mouse-edge starts\n * @event mouse-edge-end(Viewport) emitted when mouse-edge ends\n */\nexport class MouseEdges extends Plugin\n{\n /** Options used to initialize this plugin, cannot be modified later. */\n public readonly options: Readonly>;\n\n /** Factor from reverse option. */\n protected readonly reverse: -1 | 1;\n\n /** Radius squared */\n protected readonly radiusSquared: number | null;\n\n /** Scroll region size on the left side. */\n protected left!: number | null;\n\n /** Scroll region size on the top size. */\n protected top!: number | null;\n\n /** Scroll region size on the right side. */\n protected right!: number | null;\n\n /** Scroll region size on the bottom side. */\n protected bottom!: number | null;\n\n protected horizontal?: number | null;\n\n protected vertical?: number | null;\n\n /**\n * This is called by {@link Viewport.mouseEdges}.\n */\n constructor(parent: Viewport, options: IMouseEdgesOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, MOUSE_EDGES_OPTIONS, options);\n this.reverse = this.options.reverse ? 1 : -1;\n this.radiusSquared = typeof this.options.radius === 'number' ? Math.pow(this.options.radius, 2) : null;\n\n this.resize();\n }\n\n public resize(): void\n {\n const distance = this.options.distance;\n\n if (distance !== null)\n {\n this.left = distance;\n this.top = distance;\n this.right = this.parent.worldScreenWidth - distance;\n this.bottom = this.parent.worldScreenHeight - distance;\n }\n else if (!this.options.radius)\n {\n this.left = this.options.left;\n this.top = this.options.top;\n this.right = this.options.right === null ? null : this.parent.worldScreenWidth - this.options.right;\n this.bottom = this.options.bottom === null ? null : this.parent.worldScreenHeight - this.options.bottom;\n }\n }\n\n public down(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if (!this.options.allowButtons)\n {\n this.horizontal = this.vertical = null;\n }\n\n return false;\n }\n\n public move(event: InteractionEvent): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if ((event.data.pointerType !== 'mouse' && event.data.identifier !== 1)\n || (!this.options.allowButtons && event.data.buttons !== 0))\n {\n return false;\n }\n\n const x = event.data.global.x;\n const y = event.data.global.y;\n\n if (this.radiusSquared)\n {\n const center = this.parent.toScreen(this.parent.center);\n const distance = Math.pow(center.x - x, 2) + Math.pow(center.y - y, 2);\n\n if (distance >= this.radiusSquared)\n {\n const angle = Math.atan2(center.y - y, center.x - x);\n\n if (this.options.linear)\n {\n this.horizontal = Math.round(Math.cos(angle)) * this.options.speed * this.reverse * (60 / 1000);\n this.vertical = Math.round(Math.sin(angle)) * this.options.speed * this.reverse * (60 / 1000);\n }\n else\n {\n this.horizontal = Math.cos(angle) * this.options.speed * this.reverse * (60 / 1000);\n this.vertical = Math.sin(angle) * this.options.speed * this.reverse * (60 / 1000);\n }\n }\n else\n {\n if (this.horizontal)\n {\n this.decelerateHorizontal();\n }\n if (this.vertical)\n {\n this.decelerateVertical();\n }\n\n this.horizontal = this.vertical = 0;\n }\n }\n else\n {\n if (this.left !== null && x < this.left)\n {\n this.horizontal = Number(this.reverse) * this.options.speed * (60 / 1000);\n }\n else if (this.right !== null && x > this.right)\n {\n this.horizontal = -1 * this.reverse * this.options.speed * (60 / 1000);\n }\n else\n {\n this.decelerateHorizontal();\n this.horizontal = 0;\n }\n if (this.top !== null && y < this.top)\n {\n this.vertical = Number(this.reverse) * this.options.speed * (60 / 1000);\n }\n else if (this.bottom !== null && y > this.bottom)\n {\n this.vertical = -1 * this.reverse * this.options.speed * (60 / 1000);\n }\n else\n {\n this.decelerateVertical();\n this.vertical = 0;\n }\n }\n\n return false;\n }\n\n private decelerateHorizontal(): void\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (this.horizontal && decelerate && !this.options.noDecelerate)\n {\n decelerate.activate({ x: (this.horizontal * this.options.speed * this.reverse) / (1000 / 60) });\n }\n }\n\n private decelerateVertical(): void\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (this.vertical && decelerate && !this.options.noDecelerate)\n {\n decelerate.activate({ y: (this.vertical * this.options.speed * this.reverse) / (1000 / 60) });\n }\n }\n\n public up(): boolean\n {\n if (this.paused)\n {\n return false;\n }\n if (this.horizontal)\n {\n this.decelerateHorizontal();\n }\n if (this.vertical)\n {\n this.decelerateVertical();\n }\n this.horizontal = this.vertical = null;\n\n return false;\n }\n\n public update(): void\n {\n if (this.paused)\n {\n return;\n }\n\n if (this.horizontal || this.vertical)\n {\n const center = this.parent.center;\n\n if (this.horizontal)\n {\n center.x += this.horizontal * this.options.speed;\n }\n if (this.vertical)\n {\n center.y += this.vertical * this.options.speed;\n }\n\n this.parent.moveCenter(center);\n this.parent.emit('moved', { viewport: this.parent, type: 'mouse-edges' });\n }\n }\n}\n","import { Plugin } from './Plugin';\nimport { Point } from '@pixi/math';\n\nimport type { IPointData } from '@pixi/math';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { IViewportTouch } from '../InputManager';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Pinch}. */\nexport interface IPinchOptions\n{\n /** Disable two-finger dragging. */\n noDrag?: boolean;\n\n /**\n * Percent to modify pinch speed.\n *\n * @default 1\n */\n percent?: number;\n\n /**\n * Factor to multiply two-finger drag to increase speed of movement\n *\n * @default 1\n */\n factor?: number;\n\n /** Place this point at center during zoom instead of center of two fingers */\n center?: Point | null;\n\n /** Axis to zoom */\n axis?: 'all' | 'x' | 'y';\n}\n\nconst DEFAULT_PINCH_OPTIONS: Required = {\n noDrag: false,\n percent: 1,\n center: null,\n factor: 1,\n axis: 'all',\n};\n\n/**\n * Plugin for enabling two-finger pinching (or dragging).\n *\n * @public\n */\nexport class Pinch extends Plugin\n{\n /** Options used to initialize this plugin. */\n public readonly options: Required;\n\n /** Flags whether this plugin is active, i.e. a pointer is down on the viewport. */\n public active = false;\n\n /** Flags whether the viewport is being pinched. */\n public pinching = false;\n\n protected moved = false;\n protected lastCenter?: IPointData | null;\n\n /**\n * This is called by {@link Viewport.pinch}.\n */\n constructor(parent: Viewport, options: IPinchOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_PINCH_OPTIONS, options);\n }\n\n public down(): boolean\n {\n if (this.parent.input.count() >= 2)\n {\n this.active = true;\n return true;\n }\n\n return false;\n }\n\n public isAxisX(): boolean\n {\n return ['all', 'x'].includes(this.options.axis);\n }\n\n public isAxisY(): boolean\n {\n return ['all', 'y'].includes(this.options.axis);\n }\n\n public move(e: InteractionEvent): boolean\n {\n if (this.paused || !this.active)\n {\n return false;\n }\n\n const x = e.data.global.x;\n const y = e.data.global.y;\n\n const pointers = this.parent.input.touches;\n\n if (pointers.length >= 2)\n {\n const first = pointers[0] as IViewportTouch;\n const second = pointers[1] as IViewportTouch;\n const last = (first.last && second.last)\n ? Math.sqrt(Math.pow(second.last.x - first.last.x, 2) + Math.pow(second.last.y - first.last.y, 2))\n : null;\n\n if (first.id === e.data.pointerId)\n {\n first.last = { x, y, data: e.data } as IPointData;\n }\n else if (second.id === e.data.pointerId)\n {\n second.last = { x, y, data: e.data } as IPointData;\n }\n if (last)\n {\n let oldPoint: IPointData | undefined;\n\n const point = {\n x: (first.last as IPointData).x +\n ((second.last as IPointData).x - (first.last as IPointData).x) / 2,\n y: (first.last as IPointData).y +\n ((second.last as IPointData).y - (first.last as IPointData).y) / 2,\n };\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point);\n }\n let dist = Math.sqrt(Math.pow(\n (second.last as IPointData).x - (first.last as IPointData).x, 2) +\n Math.pow((second.last as IPointData).y - (first.last as IPointData).y, 2));\n\n dist = dist === 0 ? dist = 0.0000000001 : dist;\n\n const change = (1 - last / dist) * this.options.percent\n * (this.isAxisX() ? this.parent.scale.x : this.parent.scale.y);\n\n if (this.isAxisX())\n {\n this.parent.scale.x += change;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y += change;\n }\n\n this.parent.emit('zoomed', { viewport: this.parent, type: 'pinch', center: point });\n\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += (point.x - newPoint.x) * this.options.factor;\n this.parent.y += (point.y - newPoint.y) * this.options.factor;\n this.parent.emit('moved', { viewport: this.parent, type: 'pinch' });\n }\n if (!this.options.noDrag && this.lastCenter)\n {\n this.parent.x += (point.x - this.lastCenter.x) * this.options.factor;\n this.parent.y += (point.y - this.lastCenter.y) * this.options.factor;\n this.parent.emit('moved', { viewport: this.parent, type: 'pinch' });\n }\n\n this.lastCenter = point;\n this.moved = true;\n }\n else if (!this.pinching)\n {\n this.parent.emit('pinch-start', this.parent);\n this.pinching = true;\n }\n\n return true;\n }\n\n return false;\n }\n\n public up(): boolean\n {\n if (this.pinching)\n {\n if (this.parent.input.touches.length <= 1)\n {\n this.active = false;\n this.lastCenter = null;\n this.pinching = false;\n this.moved = false;\n this.parent.emit('pinch-end', this.parent);\n\n return true;\n }\n }\n\n return false;\n }\n}\n","import { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Viewport } from '../Viewport';\n\nexport interface ISnapOptions\n{\n /** snap to the top-left of viewport instead of center */\n topLeft?: boolean;\n\n /**\n * Friction/frame to apply if decelerate is active\n *\n * @default 0.8\n */\n friction?: number;\n\n /**\n * @default 1000\n */\n time?: number;\n\n /** Easing function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /**\n * Pause snapping with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Removes this plugin after snapping is complete\n *\n * @default false\n */\n removeOnComplete?: boolean;\n\n /**\n * Removes this plugin if interrupted by any user input\n *\n * @default false\n */\n removeOnInterrupt?: boolean;\n\n /**\n * Starts the snap immediately regardless of whether the viewport is at the desired location\n *\n * @default false\n */\n forceStart?: boolean;\n}\n\nconst DEFAULT_SNAP_OPTIONS: Required = {\n topLeft: false,\n friction: 0.8,\n time: 1000,\n ease: 'easeInOutSine',\n interrupt: true,\n removeOnComplete: false,\n removeOnInterrupt: false,\n forceStart: false\n};\n\n/**\n * @event snap-start(Viewport) emitted each time a snap animation starts\n * @event snap-restart(Viewport) emitted each time a snap resets because of a change in viewport size\n * @event snap-end(Viewport) emitted each time snap reaches its target\n * @event snap-remove(Viewport) emitted if snap plugin is removed\n */\nexport class Snap extends Plugin\n{\n public readonly options: Required;\n public ease?: any;\n public x: number;\n public y: number;\n\n protected percent?: number;\n protected snapping?: { time: number } | null;\n protected deltaX?: number;\n protected deltaY?: number;\n protected startX?: number;\n protected startY?: number;\n\n /**\n * This is called by {@link Viewport.snap}.\n */\n constructor(parent: Viewport, x: number, y: number, options: ISnapOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_SNAP_OPTIONS, options);\n this.ease = ease(options.ease, 'easeInOutSine');\n this.x = x;\n this.y = y;\n\n if (this.options.forceStart)\n {\n this.snapStart();\n }\n }\n\n public snapStart(): void\n {\n this.percent = 0;\n this.snapping = { time: 0 };\n const current = this.options.topLeft ? this.parent.corner : this.parent.center;\n\n this.deltaX = this.x - current.x;\n this.deltaY = this.y - current.y;\n this.startX = current.x;\n this.startY = current.y;\n this.parent.emit('snap-start', this.parent);\n }\n\n public wheel(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap');\n }\n\n return false;\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap');\n }\n else if (this.options.interrupt)\n {\n this.snapping = null;\n }\n\n return false;\n }\n\n public up(): boolean\n {\n if (this.parent.input.count() === 0)\n {\n const decelerate = this.parent.plugins.get('decelerate', true);\n\n if (decelerate && (decelerate.x || decelerate.y))\n {\n decelerate.percentChangeX = decelerate.percentChangeY = this.options.friction;\n }\n }\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n if (this.options.interrupt && this.parent.input.count() !== 0)\n {\n return;\n }\n if (!this.snapping)\n {\n const current = this.options.topLeft ? this.parent.corner : this.parent.center;\n\n if (current.x !== this.x || current.y !== this.y)\n {\n this.snapStart();\n }\n }\n else\n {\n const snapping = this.snapping;\n\n snapping.time += elapsed;\n let finished;\n let x;\n let y;\n\n const startX = this.startX as number;\n const startY = this.startY as number;\n const deltaX = this.deltaX as number;\n const deltaY = this.deltaY as number;\n\n if (snapping.time > this.options.time)\n {\n finished = true;\n x = startX + deltaX;\n y = startY + deltaY;\n }\n else\n {\n const percent = this.ease(snapping.time, 0, 1, this.options.time);\n\n x = startX + (deltaX * percent);\n y = startY + (deltaY * percent);\n }\n if (this.options.topLeft)\n {\n this.parent.moveCorner(x, y);\n }\n else\n {\n this.parent.moveCenter(x, y);\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'snap' });\n\n if (finished)\n {\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap');\n }\n this.parent.emit('snap-end', this.parent);\n this.snapping = null;\n }\n }\n }\n}\n","import { Plugin } from './Plugin';\nimport ease from '../ease';\n\nimport type { Point } from '@pixi/math';\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link SnapZoom}. */\nexport interface ISnapZoomOptions\n{\n /** the desired width to snap (to maintain aspect ratio, choose only width or height) */\n width?: number;\n\n /** the desired height to snap (to maintain aspect ratio, choose only width or height) */\n height?: number;\n\n /**\n * time for snapping in ms\n *\n * @default 1000\n */\n time?: number;\n\n /** ease function or name (see http://easings.net/ for supported names) */\n ease?: any;\n\n /** Place this point at center during zoom instead of center of the viewport */\n center?: Point | null;\n\n /**\n * Pause snapping with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Removes this plugin after snapping is complete\n *\n * @default false\n */\n removeOnComplete?: boolean;\n\n /**\n * Removes this plugin if interrupted by any user input\n *\n * @default false\n */\n removeOnInterrupt?: boolean;\n\n /**\n * Starts the snap immediately regardless of whether the viewport is at the desired zoom\n *\n * @default false\n */\n forceStart?: boolean;\n\n /**\n * Zoom but do not move\n *\n * @default false\n */\n noMove?: boolean;\n}\n\nconst DEFAULT_SNAP_ZOOM_OPTIONS: Required = {\n width: 0,\n height: 0,\n time: 1000,\n ease: 'easeInOutSine',\n center: null,\n interrupt: true,\n removeOnComplete: false,\n removeOnInterrupt: false,\n forceStart: false,\n noMove: false\n};\n\n/**\n * @event snap-zoom-start(Viewport) emitted each time a fit animation starts\n * @event snap-zoom-end(Viewport) emitted each time fit reaches its target\n * @event snap-zoom-end(Viewport) emitted each time fit reaches its target\n */\nexport class SnapZoom extends Plugin\n{\n public readonly options: Required;\n\n protected ease: any;\n protected xScale: number;\n protected yScale: number;\n protected xIndependent: boolean;\n protected yIndependent: boolean;\n protected snapping?: {\n time: number;\n startX: number;\n startY: number;\n deltaX: number;\n deltaY: number;\n } | null;\n\n /**\n * This is called by {@link Viewport.snapZoom}.\n */\n constructor(parent: Viewport, options: ISnapZoomOptions = {})\n {\n super(parent);\n\n this.options = Object.assign({}, DEFAULT_SNAP_ZOOM_OPTIONS, options);\n this.ease = ease(this.options.ease);\n\n // Assign defaults for typescript.\n this.xIndependent = false;\n this.yIndependent = false;\n this.xScale = 0;\n this.yScale = 0;\n\n if (this.options.width > 0)\n {\n this.xScale = parent.screenWidth / this.options.width;\n this.xIndependent = true;\n }\n if (this.options.height > 0)\n {\n this.yScale = parent.screenHeight / this.options.height;\n this.yIndependent = true;\n }\n\n this.xScale = this.xIndependent ? (this.xScale as number) : (this.yScale as number);\n this.yScale = this.yIndependent ? (this.yScale as number) : this.xScale;\n\n if (this.options.time === 0)\n {\n // TODO: Fix this\n // @ts-expect-error todo\n parent.container.scale.x = this.xScale;\n\n // TODO: Fix this\n // @ts-expect-error todo\n parent.container.scale.y = this.yScale;\n\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n }\n else if (options.forceStart)\n {\n this.createSnapping();\n }\n }\n\n private createSnapping(): void\n {\n const startWorldScreenWidth = this.parent.worldScreenWidth;\n const startWorldScreenHeight = this.parent.worldScreenHeight;\n const endWorldScreenWidth = this.parent.screenWidth / this.xScale;\n const endWorldScreenHeight = this.parent.screenHeight / this.yScale;\n\n this.snapping = {\n time: 0,\n startX: startWorldScreenWidth,\n startY: startWorldScreenHeight,\n deltaX: endWorldScreenWidth - startWorldScreenWidth,\n deltaY: endWorldScreenHeight - startWorldScreenHeight\n };\n\n this.parent.emit('snap-zoom-start', this.parent);\n }\n\n public resize(): void\n {\n this.snapping = null;\n\n if (this.options.width > 0)\n {\n this.xScale = this.parent.screenWidth / this.options.width;\n }\n if (this.options.height > 0)\n {\n this.yScale = this.parent.screenHeight / this.options.height;\n }\n this.xScale = this.xIndependent ? this.xScale : this.yScale;\n this.yScale = this.yIndependent ? this.yScale : this.xScale;\n }\n\n public wheel(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n\n return false;\n }\n\n public down(): boolean\n {\n if (this.options.removeOnInterrupt)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n else if (this.options.interrupt)\n {\n this.snapping = null;\n }\n\n return false;\n }\n\n public update(elapsed: number): void\n {\n if (this.paused)\n {\n return;\n }\n if (this.options.interrupt && this.parent.input.count() !== 0)\n {\n return;\n }\n\n let oldCenter: Point | undefined;\n\n if (!this.options.center && !this.options.noMove)\n {\n oldCenter = this.parent.center;\n }\n if (!this.snapping)\n {\n if (this.parent.scale.x !== this.xScale || this.parent.scale.y !== this.yScale)\n {\n this.createSnapping();\n }\n }\n else if (this.snapping)\n {\n const snapping = this.snapping;\n\n snapping.time += elapsed;\n\n if (snapping.time >= this.options.time)\n {\n this.parent.scale.set(this.xScale, this.yScale);\n if (this.options.removeOnComplete)\n {\n this.parent.plugins.remove('snap-zoom');\n }\n this.parent.emit('snap-zoom-end', this.parent);\n this.snapping = null;\n }\n else\n {\n const snapping = this.snapping;\n const worldScreenWidth = this.ease(snapping.time, snapping.startX, snapping.deltaX, this.options.time);\n const worldScreenHeight = this.ease(snapping.time, snapping.startY, snapping.deltaY, this.options.time);\n\n this.parent.scale.x = this.parent.screenWidth / worldScreenWidth;\n this.parent.scale.y = this.parent.screenHeight / worldScreenHeight;\n }\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (!this.options.noMove)\n {\n if (!this.options.center)\n {\n this.parent.moveCenter(oldCenter as Point);\n }\n else\n {\n this.parent.moveCenter(this.options.center);\n }\n }\n }\n }\n\n public resume(): void\n {\n this.snapping = null;\n super.resume();\n }\n}\n","import { Plugin } from './Plugin';\nimport { IPointData, Point } from '@pixi/math';\n\nimport type { Viewport } from '../Viewport';\n\n/** Options for {@link Wheel}. */\nexport interface IWheelOptions\n{\n /**\n * Percent to scroll with each spin\n *\n * @default 0.1\n */\n percent?: number;\n\n /**\n * smooth the zooming by providing the number of frames to zoom between wheel spins\n *\n * @default false\n */\n smooth?: false | number;\n\n /**\n * Stop smoothing with any user input on the viewport\n *\n * @default true\n */\n interrupt?: boolean;\n\n /**\n * Reverse the direction of the scroll\n *\n * @default false\n */\n reverse?: boolean;\n\n /**\n * Place this point at center during zoom instead of current mouse position\n *\n * @default null\n */\n center?: Point | null;\n\n /**\n * Scaling factor for non-DOM_DELTA_PIXEL scrolling events\n *\n * @default 20\n */\n lineHeight?: number;\n\n /**\n * Axis to zoom\n *\n * @default 'all'\n */\n axis?: 'all' | 'x' | 'y';\n}\n\nconst DEFAULT_WHEEL_OPTIONS: Required = {\n percent: 0.1,\n smooth: false,\n interrupt: true,\n reverse: false,\n center: null,\n lineHeight: 20,\n axis: 'all',\n};\n\n/**\n * Plugin for handling wheel scrolling for viewport zoom.\n *\n * @event wheel({wheel: {dx, dy, dz}, event, viewport})\n */\nexport class Wheel extends Plugin\n{\n public readonly options: Required;\n\n protected smoothing?: IPointData | null;\n protected smoothingCenter?: Point | null;\n protected smoothingCount?: number;\n\n /**\n * This is called by {@link Viewport.wheel}.\n */\n constructor(parent: Viewport, options: IWheelOptions = {})\n {\n super(parent);\n this.options = Object.assign({}, DEFAULT_WHEEL_OPTIONS, options);\n }\n\n public down(): boolean\n {\n if (this.options.interrupt)\n {\n this.smoothing = null;\n }\n\n return false;\n }\n\n protected isAxisX(): boolean\n {\n return ['all', 'x'].includes(this.options.axis);\n }\n\n protected isAxisY(): boolean\n {\n return ['all', 'y'].includes(this.options.axis);\n }\n\n public update(): void\n {\n if (this.smoothing)\n {\n const point = this.smoothingCenter;\n const change = this.smoothing;\n let oldPoint;\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point as IPointData);\n }\n if (this.isAxisX())\n {\n this.parent.scale.x += change.x;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y += change.y;\n }\n\n this.parent.emit('zoomed', { viewport: this.parent, type: 'wheel' });\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += (point as IPointData).x - newPoint.x;\n this.parent.y += (point as IPointData).y - newPoint.y;\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n (this.smoothingCount as number)++;\n\n if (this.smoothingCount as number >= this.options.smooth)\n {\n this.smoothing = null;\n }\n }\n }\n\n public wheel(e: WheelEvent): boolean | undefined\n {\n if (this.paused)\n {\n return;\n }\n\n const point = this.parent.input.getPointerPosition(e);\n const sign = this.options.reverse ? -1 : 1;\n const step = sign * -e.deltaY * (e.deltaMode ? this.options.lineHeight : 1) / 500;\n const change = Math.pow(2, (1 + this.options.percent) * step);\n\n if (this.options.smooth)\n {\n const original = {\n x: this.smoothing ? this.smoothing.x * (this.options.smooth - (this.smoothingCount as number)) : 0,\n y: this.smoothing ? this.smoothing.y * (this.options.smooth - (this.smoothingCount as number)) : 0\n };\n\n this.smoothing = {\n x: ((this.parent.scale.x + original.x) * change - this.parent.scale.x) / this.options.smooth,\n y: ((this.parent.scale.y + original.y) * change - this.parent.scale.y) / this.options.smooth,\n };\n this.smoothingCount = 0;\n this.smoothingCenter = point;\n }\n else\n {\n let oldPoint: IPointData | undefined;\n\n if (!this.options.center)\n {\n oldPoint = this.parent.toLocal(point);\n }\n if (this.isAxisX())\n {\n this.parent.scale.x *= change;\n }\n if (this.isAxisY())\n {\n this.parent.scale.y *= change;\n }\n this.parent.emit('zoomed', { viewport: this.parent, type: 'wheel' });\n const clamp = this.parent.plugins.get('clamp-zoom', true);\n\n if (clamp)\n {\n clamp.clamp();\n }\n if (this.options.center)\n {\n this.parent.moveCenter(this.options.center);\n }\n else\n {\n const newPoint = this.parent.toGlobal(oldPoint as IPointData);\n\n this.parent.x += point.x - newPoint.x;\n this.parent.y += point.y - newPoint.y;\n }\n }\n\n this.parent.emit('moved', { viewport: this.parent, type: 'wheel' });\n this.parent.emit('wheel', { wheel: { dx: e.deltaX, dy: e.deltaY, dz: e.deltaZ }, event: e, viewport: this.parent });\n\n if (!this.parent.options.passiveWheel)\n {\n return true;\n }\n }\n}\n","import type {\n Animate,\n Bounce,\n Clamp,\n ClampZoom,\n Decelerate,\n Drag,\n Follow,\n MouseEdges,\n Pinch,\n Plugin,\n Snap,\n SnapZoom,\n Wheel,\n} from './plugins';\nimport type { InteractionEvent } from '@pixi/interaction';\nimport type { Viewport } from './Viewport';\n\nconst PLUGIN_ORDER = [\n 'drag',\n 'pinch',\n 'wheel',\n 'follow',\n 'mouse-edges',\n 'decelerate',\n 'aniamte',\n 'bounce',\n 'snap-zoom',\n 'clamp-zoom',\n 'snap',\n 'clamp',\n];\n\n/**\n * Use this to access current plugins or add user-defined plugins\n *\n * @public\n */\nexport class PluginManager\n{\n /** Maps mounted plugins by their type */\n public plugins: Partial>;\n\n /**\n * List of plugins mounted\n *\n * This list is kept sorted by the internal priority of plugins (hard-coded).\n */\n public list: Array;\n\n /** The viewport using the plugins managed by `this`. */\n public readonly viewport: Viewport;\n\n /** This is called by {@link Viewport} to initialize the {@link Viewport.plugins plugins}. */\n constructor(viewport: Viewport)\n {\n this.viewport = viewport;\n this.list = [];\n this.plugins = {};\n }\n\n /**\n * Inserts a named plugin or a user plugin into the viewport\n * default plugin order: 'drag', 'pinch', 'wheel', 'follow', 'mouse-edges', 'decelerate', 'bounce',\n * 'snap-zoom', 'clamp-zoom', 'snap', 'clamp'\n *\n * @param {string} name of plugin\n * @param {Plugin} plugin - instantiated Plugin class\n * @param {number} index to insert userPlugin (otherwise inserts it at the end)\n */\n public add(name: string, plugin: Plugin, index: number = PLUGIN_ORDER.length)\n {\n this.plugins[name] = plugin;\n\n const current = PLUGIN_ORDER.indexOf(name);\n\n if (current !== -1)\n {\n PLUGIN_ORDER.splice(current, 1);\n }\n\n PLUGIN_ORDER.splice(index, 0, name);\n this.sort();\n }\n\n public get(name: 'animate', ignorePaused?: boolean): Animate | undefined | null;\n public get(name: 'bounce', ignorePaused?: boolean): Bounce | undefined | null;\n public get(name: 'clamp', ignorePaused?: boolean): Clamp | undefined | null;\n public get(name: 'clamp-zoom', ignorePaused?: boolean): ClampZoom | undefined | null;\n public get(name: 'decelerate', ignorePaused?: boolean): Decelerate | undefined | null;\n public get(name: 'drag', ignorePaused?: boolean): Drag | undefined | null;\n public get(name: 'follow', ignorePaused?: boolean): Follow | undefined | null;\n public get(name: 'mouse-edges', ignorePaused?: boolean): MouseEdges | undefined | null;\n public get(name: 'pinch', ignorePaused?: boolean): Pinch | undefined | null;\n public get(name: 'snap', ignorePaused?: boolean): Snap | undefined | null;\n public get(name: 'snap-zoom', ignorePaused?: boolean): SnapZoom | undefined | null;\n public get(name: 'wheel', ignorePaused?: boolean): Wheel | undefined | null;\n public get(name: string, ignorePaused?: boolean): T | undefined | null;\n\n /**\n * Get plugin\n *\n * @param {string} name of plugin\n * @param {boolean} [ignorePaused] return null if plugin is paused\n */\n public get(name: string, ignorePaused?: boolean): T | undefined | null\n {\n if (ignorePaused)\n {\n if (this.plugins[name]?.paused)\n {\n return null;\n }\n }\n\n return this.plugins[name] as T;\n }\n\n /**\n * Update all active plugins\n *\n * @internal\n * @ignore\n * @param {number} elapsed type in milliseconds since last update\n */\n public update(elapsed: number): void\n {\n for (const plugin of this.list)\n {\n plugin.update(elapsed);\n }\n }\n\n /**\n * Resize all active plugins\n *\n * @internal\n * @ignore\n */\n public resize(): void\n {\n for (const plugin of this.list)\n {\n plugin.resize();\n }\n }\n\n /** Clamps and resets bounce and decelerate (as needed) after manually moving viewport */\n public reset(): void\n {\n for (const plugin of this.list)\n {\n plugin.reset();\n }\n }\n\n /** removes all installed plugins */\n public removeAll(): void\n {\n this.plugins = {};\n this.sort();\n }\n\n /**\n * Removes installed plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public remove(name: string): void\n {\n if (this.plugins[name])\n {\n delete this.plugins[name];\n this.viewport.emit(`${name}-remove`);\n this.sort();\n }\n }\n\n /**\n * Pause plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public pause(name: string): void\n {\n this.plugins[name]?.pause();\n }\n\n /**\n * Resume plugin\n *\n * @param {string} name of plugin (e.g., 'drag', 'pinch')\n */\n public resume(name: string): void\n {\n this.plugins[name]?.resume();\n }\n\n /**\n * Sort plugins according to PLUGIN_ORDER\n *\n * @internal\n * @ignore\n */\n public sort()\n {\n this.list = [];\n\n for (const plugin of PLUGIN_ORDER)\n {\n if (this.plugins[plugin])\n {\n this.list.push(this.plugins[plugin] as Plugin);\n }\n }\n }\n\n /**\n * Handle down for all plugins\n *\n * @internal\n * @ignore\n */\n public down(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.list)\n {\n if (plugin.down(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle move for all plugins\n *\n * @internal\n * @ignore\n */\n public move(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.viewport.plugins.list)\n {\n if (plugin.move(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle up for all plugins\n *\n * @internal\n * @ignore\n */\n public up(event: InteractionEvent): boolean\n {\n let stop = false;\n\n for (const plugin of this.list)\n {\n if (plugin.up(event))\n {\n stop = true;\n }\n }\n\n return stop;\n }\n\n /**\n * Handle wheel event for all plugins\n *\n * @internal\n * @ignore\n */\n public wheel(e: WheelEvent): boolean\n {\n let result = false;\n\n for (const plugin of this.list)\n {\n if (plugin.wheel(e))\n {\n result = true;\n }\n }\n\n return result;\n }\n}\n","import { Container } from '@pixi/display';\nimport { Point, Rectangle } from '@pixi/math';\nimport { Ticker } from '@pixi/ticker';\n\nimport { InputManager } from './InputManager';\nimport { PluginManager } from './PluginManager';\nimport {\n Animate,\n Bounce,\n Clamp,\n ClampZoom,\n Decelerate,\n Drag,\n Follow,\n MouseEdges,\n Pinch,\n Snap,\n SnapZoom,\n Wheel\n} from './plugins';\n\nimport type { DisplayObject, IDestroyOptions } from '@pixi/display';\nimport type { IHitArea, InteractionManager } from '@pixi/interaction';\n\n/** Options for {@link Viewport}. */\nexport interface IViewportOptions {\n /** @default window.innerWidth */\n screenWidth?: number;\n\n /** @default window.innerHeight */\n screenHeight?: number;\n\n /** @default this.width */\n worldWidth?: number | null;\n\n /** @default this.height */\n worldHeight?: number | null;\n\n /**\n * Number of pixels to move to trigger an input event (e.g., drag, pinch) or disable a clicked event\n *\n * @default 5\n */\n threshold?: number;\n\n /**\n * Whether the 'wheel' event is set to passive (note: if false, e.preventDefault() will be called when wheel is used over the viewport)\n *\n * @default true\n */\n passiveWheel?: boolean;\n\n /**\n * Whether to stopPropagation of events that impact the viewport (except wheel events, see options.passiveWheel)\n */\n stopPropagation?: boolean;\n\n /**\n * Change the default hitArea from world size to a new value\n */\n forceHitArea?: Rectangle | null;\n\n /**\n * Set this if you want to manually call update() function on each frame\n *\n * @default false\n */\n noTicker?: boolean;\n\n /**\n * InteractionManager, available from instantiated `WebGLRenderer/CanvasRenderer.plugins.interaction`\n *\n * It's used to calculate pointer postion relative to canvas location on screen\n */\n interaction?: InteractionManager | null;\n\n /**\n * Remove oncontextmenu=() => {} from the divWheel element\n */\n disableOnContextMenu?: boolean;\n\n /**\n * div to attach the wheel event\n *\n * @default document.body\n */\n divWheel?: HTMLElement;\n\n /**\n * Use this PIXI.ticker for updates\n *\n * @default PIXI.Ticker.shared\n */\n ticker?: Ticker;\n}\n\nexport interface ICompleteViewportOptions extends IViewportOptions {\n screenWidth: number;\n screenHeight: number;\n threshold: number;\n passiveWheel: boolean;\n stopPropagation: boolean;\n noTicker: boolean;\n ticker: Ticker;\n}\n\nexport interface IViewportTransformState {\n x: number;\n y: number;\n scaleX: number;\n scaleY: number;\n}\n\nconst DEFAULT_VIEWPORT_OPTIONS: ICompleteViewportOptions = {\n screenWidth: window.innerWidth,\n screenHeight: window.innerHeight,\n worldWidth: null,\n worldHeight: null,\n threshold: 5,\n passiveWheel: true,\n stopPropagation: false,\n forceHitArea: null,\n noTicker: false,\n interaction: null,\n disableOnContextMenu: false,\n ticker: Ticker.shared,\n};\n\n/**\n * Main class to use when creating a Viewport\n *\n * @public\n * @fires clicked\n * @fires drag-start\n * @fires drag-end\n * @fires drag-remove\n * @fires pinch-start\n * @fires pinch-end\n * @fires pinch-remove\n * @fires snap-start\n * @fires snap-end\n * @fires snap-remove\n * @fires snap-zoom-start\n * @fires snap-zoom-end\n * @fires snap-zoom-remove\n * @fires bounce-x-start\n * @fires bounce-x-end\n * @fires bounce-y-start\n * @fires bounce-y-end\n * @fires bounce-remove\n * @fires wheel\n * @fires wheel-remove\n * @fires wheel-scroll\n * @fires wheel-scroll-remove\n * @fires mouse-edge-start\n * @fires mouse-edge-end\n * @fires mouse-edge-remove\n * @fires moved\n * @fires moved-end\n * @fires zoomed\n * @fires zoomed-end\n * @fires frame-end\n */\nexport class Viewport extends Container\n{\n /** Flags whether the viewport is being panned */\n public moving?: boolean;\n\n public screenWidth: number;\n public screenHeight: number;\n\n /** Number of pixels to move to trigger an input event (e.g., drag, pinch) or disable a clicked event */\n public threshold: number;\n\n public readonly input: InputManager;\n\n /** Use this to add user plugins or access existing plugins (e.g., to pause, resume, or remove them) */\n public readonly plugins: PluginManager;\n\n /** Flags whether the viewport zoom is being changed. */\n public zooming?: boolean;\n\n public lastViewport?: IViewportTransformState | null;\n\n /** The options passed when creating this viewport, merged with the default values */\n public readonly options: ICompleteViewportOptions & { divWheel: HTMLElement };\n\n private _dirty?: boolean;\n private _forceHitArea?: IHitArea | null;\n private _hitAreaDefault?: Rectangle;\n private _pause?: boolean;\n private readonly tickerFunction?: () => void;\n private _worldWidth?: number | null;\n private _worldHeight?: number | null;\n\n /**\n * @param options\n */\n constructor(options: IViewportOptions = {})\n {\n super();\n this.options = Object.assign(\n {},\n { divWheel: document.body },\n DEFAULT_VIEWPORT_OPTIONS,\n options\n );\n\n this.screenWidth = this.options.screenWidth;\n this.screenHeight = this.options.screenHeight;\n\n this._worldWidth = this.options.worldWidth;\n this._worldHeight = this.options.worldHeight;\n this.forceHitArea = this.options.forceHitArea;\n this.threshold = this.options.threshold;\n\n this.options.divWheel = this.options.divWheel || document.body;\n\n if (this.options.disableOnContextMenu)\n {\n this.options.divWheel.oncontextmenu = (e) => e.preventDefault();\n }\n if (!this.options.noTicker)\n {\n this.tickerFunction = () => this.update(this.options.ticker.elapsedMS);\n this.options.ticker.add(this.tickerFunction);\n }\n\n this.input = new InputManager(this);\n this.plugins = new PluginManager(this);\n }\n\n /** Overrides PIXI.Container's destroy to also remove the 'wheel' and PIXI.Ticker listeners */\n destroy(options: IDestroyOptions)\n {\n if (!this.options.noTicker && this.tickerFunction)\n {\n this.options.ticker.remove(this.tickerFunction);\n }\n\n this.input.destroy();\n super.destroy(options);\n }\n\n /**\n * Update viewport on each frame.\n *\n * By default, you do not need to call this unless you set `options.noTicker=true`.\n *\n * @param {number} elapsed time in milliseconds since last update\n */\n update(elapsed: number): void\n {\n if (!this.pause)\n {\n this.plugins.update(elapsed);\n\n if (this.lastViewport)\n {\n // Check for moved-end event\n if (this.lastViewport.x !== this.x || this.lastViewport.y !== this.y)\n {\n this.moving = true;\n }\n else if (this.moving)\n {\n this.emit('moved-end', this);\n this.moving = false;\n }\n\n // Check for zoomed-end event\n if (this.lastViewport.scaleX !== this.scale.x || this.lastViewport.scaleY !== this.scale.y)\n {\n this.zooming = true;\n }\n else if (this.zooming)\n {\n this.emit('zoomed-end', this);\n this.zooming = false;\n }\n }\n\n if (!this.forceHitArea)\n {\n this._hitAreaDefault = new Rectangle(this.left, this.top, this.worldScreenWidth, this.worldScreenHeight);\n this.hitArea = this._hitAreaDefault;\n }\n\n this._dirty = this._dirty || !this.lastViewport\n || this.lastViewport.x !== this.x || this.lastViewport.y !== this.y\n || this.lastViewport.scaleX !== this.scale.x || this.lastViewport.scaleY !== this.scale.y;\n\n this.lastViewport = {\n x: this.x,\n y: this.y,\n scaleX: this.scale.x,\n scaleY: this.scale.y\n };\n this.emit('frame-end', this);\n }\n }\n\n /** Use this to set screen and world sizes, needed for pinch/wheel/clamp/bounce. */\n resize(\n screenWidth = window.innerWidth,\n screenHeight = window.innerHeight,\n worldWidth: number,\n worldHeight: number\n ): void\n {\n this.screenWidth = screenWidth;\n this.screenHeight = screenHeight;\n\n if (typeof worldWidth !== 'undefined')\n {\n this._worldWidth = worldWidth;\n }\n if (typeof worldHeight !== 'undefined')\n {\n this._worldHeight = worldHeight;\n }\n\n this.plugins.resize();\n this.dirty = true;\n }\n\n /** World width, in pixels */\n get worldWidth(): number\n {\n if (this._worldWidth)\n {\n return this._worldWidth;\n }\n\n return this.width / this.scale.x;\n }\n set worldWidth(value)\n {\n this._worldWidth = value;\n this.plugins.resize();\n }\n\n /** World height, in pixels */\n get worldHeight(): number\n {\n if (this._worldHeight)\n {\n return this._worldHeight;\n }\n\n return this.height / this.scale.y;\n }\n set worldHeight(value)\n {\n this._worldHeight = value;\n this.plugins.resize();\n }\n\n /** Get visible world bounds of viewport */\n public getVisibleBounds(): Rectangle\n {\n return new Rectangle(this.left, this.top, this.worldScreenWidth, this.worldScreenHeight);\n }\n\n /** Change coordinates from screen to world */\n public toWorld(x: number, y: number): Point;\n /** Change coordinates from screen to world */\n public toWorld(screenPoint: Point): Point;\n\n public toWorld(x: number | Point, y?: number): Point\n {\n if (arguments.length === 2)\n {\n return this.toLocal(new Point(x as number, y));\n }\n\n return this.toLocal(x as Point);\n }\n\n /** Change coordinates from world to screen */\n public toScreen(x: number, y: number): Point;\n /** Change coordinates from world to screen */\n public toScreen(worldPoint: Point): Point;\n\n public toScreen(x: number | Point, y?: number): Point\n {\n if (arguments.length === 2)\n {\n return this.toGlobal(new Point(x as number, y));\n }\n\n return this.toGlobal(x as Point);\n }\n\n /** Screen width in world coordinates */\n get worldScreenWidth(): number\n {\n return this.screenWidth / this.scale.x;\n }\n\n /** Screen height in world coordinates */\n get worldScreenHeight(): number\n {\n return this.screenHeight / this.scale.y;\n }\n\n /** World width in screen coordinates */\n get screenWorldWidth(): number\n {\n return this.worldWidth * this.scale.x;\n }\n\n /** World height in screen coordinates */\n get screenWorldHeight()\n {\n return this.worldHeight * this.scale.y;\n }\n\n /** Center of screen in world coordinates */\n get center(): Point\n {\n return new Point(\n this.worldScreenWidth / 2 - this.x / this.scale.x,\n this.worldScreenHeight / 2 - this.y / this.scale.y,\n );\n }\n set center(value: Point)\n {\n this.moveCenter(value);\n }\n\n /** Move center of viewport to (x, y) */\n public moveCenter(x: number, y: number): this;\n\n /** Move center of viewport to {@code center}. */\n public moveCenter(center: Point): this;\n\n public moveCenter(...args: [number, number] | [Point]): this\n {\n let x: number;\n let y: number;\n\n if (typeof args[0] === 'number')\n {\n x = args[0];\n y = args[1] as number;\n }\n else\n {\n x = args[0].x;\n y = args[0].y;\n }\n\n const newX = (this.worldScreenWidth / 2 - x) * this.scale.x;\n const newY = (this.worldScreenHeight / 2 - y) * this.scale.y;\n\n if (this.x !== newX || this.y !== newY)\n {\n this.position.set(newX, newY);\n this.plugins.reset();\n this.dirty = true;\n }\n\n return this;\n }\n\n /** Top-left corner of Viewport */\n get corner(): Point\n {\n return new Point(-this.x / this.scale.x, -this.y / this.scale.y);\n }\n set corner(value: Point)\n {\n this.moveCorner(value);\n }\n\n /** Move viewport's top-left corner; also clamps and resets decelerate and bounce (as needed) */\n public moveCorner(x: number, y: number): this;\n\n /** move viewport's top-left corner; also clamps and resets decelerate and bounce (as needed) */\n public moveCorner(center: Point): this;\n\n public moveCorner(...args: [number, number] | [Point])\n {\n let x;\n let y;\n\n if (args.length === 1)\n {\n x = -args[0].x * this.scale.x;\n y = -args[0].y * this.scale.y;\n }\n else\n {\n x = -args[0] * this.scale.x;\n y = -args[1] * this.scale.y;\n }\n\n if (x !== this.x || y !== this.y)\n {\n this.position.set(x, y);\n this.plugins.reset();\n this.dirty = true;\n }\n\n return this;\n }\n\n /** Get how many world pixels fit in screen's width */\n get screenWidthInWorldPixels()\n {\n return this.screenWidth / this.scale.x;\n }\n\n /** Get how many world pixels fit on screen's height */\n get screenHeightInWorldPixels()\n {\n return this.screenHeight / this.scale.y;\n }\n\n /**\n * Find the scale value that fits a world width on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param width - Width in world pixels\n * @return - scale\n */\n findFitWidth(width: number): number\n {\n return this.screenWidth / width;\n }\n\n /**\n * Finds the scale value that fits a world height on the screens\n * does not change the viewport (use fit... to change)\n *\n * @param height - Height in world pixels\n * @return - scale\n */\n findFitHeight(height: number): number\n {\n return this.screenHeight / height;\n }\n\n /**\n * Finds the scale value that fits the smaller of a world width and world height on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param {number} width in world pixels\n * @param {number} height in world pixels\n * @returns {number} scale\n */\n findFit(width: number, height: number): number\n {\n const scaleX = this.screenWidth / width;\n const scaleY = this.screenHeight / height;\n\n return Math.min(scaleX, scaleY);\n }\n\n /**\n * Finds the scale value that fits the larger of a world width and world height on the screen\n * does not change the viewport (use fit... to change)\n *\n * @param {number} width in world pixels\n * @param {number} height in world pixels\n * @returns {number} scale\n */\n findCover(width: number, height: number): number\n {\n const scaleX = this.screenWidth / width;\n const scaleY = this.screenHeight / height;\n\n return Math.max(scaleX, scaleY);\n }\n\n /**\n * Change zoom so the width fits in the viewport\n *\n * @param width - width in world coordinates\n * @param center - maintain the same center\n * @param scaleY - whether to set scaleY=scaleX\n * @param noClamp - whether to disable clamp-zoom\n * @returns {Viewport} this\n */\n fitWidth(width = this.worldWidth, center?: boolean, scaleY = true, noClamp?: boolean): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.x = this.screenWidth / width;\n\n if (scaleY)\n {\n this.scale.y = this.scale.x;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (!noClamp && clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so the height fits in the viewport\n *\n * @param {number} [height=this.worldHeight] in world coordinates\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @param {boolean} [scaleX=true] whether to set scaleX = scaleY\n * @param {boolean} [noClamp] whether to disable clamp-zoom\n * @returns {Viewport} this\n */\n fitHeight(height = this.worldHeight, center?: boolean, scaleX = true, noClamp?: boolean)\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.y = this.screenHeight / height;\n\n if (scaleX)\n {\n this.scale.x = this.scale.y;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (!noClamp && clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so it fits the entire world in the viewport\n *\n * @param {boolean} center maintain the same center of the screen after zoom\n * @returns {Viewport} this\n */\n fitWorld(center?: boolean): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n\n this.scale.x = this.screenWidth / this.worldWidth;\n this.scale.y = this.screenHeight / this.worldHeight;\n\n if (this.scale.x < this.scale.y)\n {\n this.scale.y = this.scale.x;\n }\n else\n {\n this.scale.x = this.scale.y;\n }\n\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Change zoom so it fits the size or the entire world in the viewport\n *\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @param {number} [width=this.worldWidth] desired width\n * @param {number} [height=this.worldHeight] desired height\n * @returns {Viewport} this\n */\n fit(center?: boolean, width = this.worldWidth, height = this.worldHeight): this\n {\n let save: Point | undefined;\n\n if (center)\n {\n save = this.center;\n }\n\n this.scale.x = this.screenWidth / width;\n this.scale.y = this.screenHeight / height;\n\n if (this.scale.x < this.scale.y)\n {\n this.scale.y = this.scale.x;\n }\n else\n {\n this.scale.x = this.scale.y;\n }\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n // eslint-disable-next-line\n // @ts-ignore\n set visible(value: boolean)\n {\n if (!value)\n {\n this.input.clear();\n }\n\n super.visible = value;\n }\n\n /**\n * Zoom viewport to specific value.\n *\n * @param {number} scale value (e.g., 1 would be 100%, 0.25 would be 25%)\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n setZoom(scale: number, center?: boolean): this\n {\n let save;\n\n if (center)\n {\n save = this.center;\n }\n this.scale.set(scale);\n const clampZoom = this.plugins.get('clamp-zoom', true);\n\n if (clampZoom)\n {\n clampZoom.clamp();\n }\n if (center && save)\n {\n this.moveCenter(save);\n }\n\n return this;\n }\n\n /**\n * Zoom viewport by a certain percent (in both x and y direction).\n *\n * @param {number} percent change (e.g., 0.25 would increase a starting scale of 1.0 to 1.25)\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n zoomPercent(percent: number, center?: boolean): this\n {\n return this.setZoom(this.scale.x + this.scale.x * percent, center);\n }\n\n /**\n * Zoom viewport by increasing/decreasing width by a certain number of pixels.\n *\n * @param {number} change in pixels\n * @param {boolean} [center] maintain the same center of the screen after zoom\n * @return {Viewport} this\n */\n zoom(change: number, center?: boolean)\n {\n this.fitWidth(change + this.worldScreenWidth, center);\n\n return this;\n }\n\n /** Changes scale of viewport and maintains center of viewport */\n get scaled(): number\n {\n return this.scale.x;\n }\n set scaled(scale: number)\n {\n this.setZoom(scale, true);\n }\n\n /**\n * @param {SnapZoomOptions} options\n */\n snapZoom(options: any): this\n {\n this.plugins.add('snap-zoom', new SnapZoom(this, options));\n\n return this;\n }\n\n /** Is container out of world bounds */\n OOB(): {\n left: boolean;\n right: boolean;\n top: boolean;\n bottom: boolean;\n cornerPoint: Point;\n }\n {\n return {\n left: this.left < 0,\n right: this.right > this.worldWidth,\n top: this.top < 0,\n bottom: this.bottom > this.worldHeight,\n cornerPoint: new Point(\n this.worldWidth * this.scale.x - this.screenWidth,\n this.worldHeight * this.scale.y - this.screenHeight\n )\n };\n }\n\n /** World coordinates of the right edge of the screen */\n get right(): number\n {\n return -this.x / this.scale.x + this.worldScreenWidth;\n }\n set right(value: number)\n {\n this.x = -value * this.scale.x + this.screenWidth;\n this.plugins.reset();\n }\n\n /** World coordinates of the left edge of the screen */\n get left(): number\n {\n return -this.x / this.scale.x;\n }\n set left(value: number)\n {\n this.x = -value * this.scale.x;\n this.plugins.reset();\n }\n\n /** World coordinates of the top edge of the screen */\n get top(): number\n {\n return -this.y / this.scale.y;\n }\n set top(value: number)\n {\n this.y = -value * this.scale.y;\n this.plugins.reset();\n }\n\n /** World coordinates of the bottom edge of the screen */\n get bottom(): number\n {\n return -this.y / this.scale.y + this.worldScreenHeight;\n }\n set bottom(value: number)\n {\n this.y = -value * this.scale.y + this.screenHeight;\n this.plugins.reset();\n }\n\n /**\n * Determines whether the viewport is dirty (i.e., needs to be renderered to the screen because of a change)\n */\n get dirty(): boolean\n {\n return !!this._dirty;\n }\n set dirty(value: boolean)\n {\n this._dirty = value;\n }\n\n /**\n * Permanently changes the Viewport's hitArea\n *\n * NOTE: if not set then hitArea = PIXI.Rectangle(Viewport.left, Viewport.top, Viewport.worldScreenWidth, Viewport.worldScreenHeight)\n */\n get forceHitArea(): IHitArea | null | undefined\n {\n return this._forceHitArea;\n }\n set forceHitArea(value: IHitArea | null | undefined)\n {\n if (value)\n {\n this._forceHitArea = value;\n this.hitArea = value;\n }\n else\n {\n this._forceHitArea = null;\n this.hitArea = new Rectangle(0, 0, this.worldWidth, this.worldHeight);\n }\n }\n\n /**\n * Enable one-finger touch to drag\n *\n * NOTE: if you expect users to use right-click dragging, you should enable `viewport.options.disableOnContextMenu`\n * to avoid the context menu popping up on each right-click drag.\n *\n * @param {DragOptions} [options]\n * @returns {Viewport} this\n */\n public drag(options: any): this\n {\n this.plugins.add('drag', new Drag(this, options));\n\n return this;\n }\n\n /**\n * Clamp to world boundaries or other provided boundaries\n *\n * NOTES:\n * clamp is disabled if called with no options; use { direction: 'all' } for all edge clamping\n * screenWidth, screenHeight, worldWidth, and worldHeight needs to be set for this to work properly\n *\n * @param {ClampOptions} [options]\n * @returns {Viewport} this\n */\n public clamp(options: any): this\n {\n this.plugins.add('clamp', new Clamp(this, options));\n\n return this;\n }\n\n /**\n * Decelerate after a move\n *\n * NOTE: this fires 'moved' event during deceleration\n *\n * @param {DecelerateOptions} [options]\n * @return {Viewport} this\n */\n public decelerate(options: any): this\n {\n this.plugins.add('decelerate', new Decelerate(this, options));\n\n return this;\n }\n\n /**\n * Bounce on borders\n * NOTES:\n * screenWidth, screenHeight, worldWidth, and worldHeight needs to be set for this to work properly\n * fires 'moved', 'bounce-x-start', 'bounce-y-start', 'bounce-x-end', and 'bounce-y-end' events\n * @param {object} [options]\n * @param {string} [options.sides=all] all, horizontal, vertical, or combination of top, bottom, right, left (e.g., 'top-bottom-right')\n * @param {number} [options.friction=0.5] friction to apply to decelerate if active\n * @param {number} [options.time=150] time in ms to finish bounce\n * @param {object} [options.bounceBox] use this bounceBox instead of (0, 0, viewport.worldWidth, viewport.worldHeight)\n * @param {number} [options.bounceBox.x=0]\n * @param {number} [options.bounceBox.y=0]\n * @param {number} [options.bounceBox.width=viewport.worldWidth]\n * @param {number} [options.bounceBox.height=viewport.worldHeight]\n * @param {string|function} [options.ease=easeInOutSine] ease function or name (see http://easings.net/ for supported names)\n * @param {string} [options.underflow=center] (top/bottom/center and left/right/center, or center) where to place world if too small for screen\n * @return {Viewport} this\n */\n public bounce(options: any): this\n {\n this.plugins.add('bounce', new Bounce(this, options));\n\n return this;\n }\n\n /**\n * Enable pinch to zoom and two-finger touch to drag\n *\n * @param {PinchOptions} [options]\n * @return {Viewport} this\n */\n public pinch(options: any): this\n {\n this.plugins.add('pinch', new Pinch(this, options));\n\n return this;\n }\n\n /**\n * Snap to a point\n *\n * @param {number} x\n * @param {number} y\n * @param {SnapOptions} [options]\n * @return {Viewport} this\n */\n public snap(x: number, y: number, options: any): this\n {\n this.plugins.add('snap', new Snap(this, x, y, options));\n\n return this;\n }\n\n /**\n * Follow a target\n *\n * NOTES:\n * uses the (x, y) as the center to follow; for PIXI.Sprite to work properly, use sprite.anchor.set(0.5)\n * options.acceleration is not perfect as it doesn't know the velocity of the target\n * it adds acceleration to the start of movement and deceleration to the end of movement when the target is stopped\n * fires 'moved' event\n * @param {PIXI.DisplayObject} target to follow\n * @param {FollowOptions} [options]\n * @returns {Viewport} this\n */\n public follow(target: DisplayObject, options: any): this\n {\n this.plugins.add('follow', new Follow(this, target, options));\n\n return this;\n }\n\n /**\n * Zoom using mouse wheel\n *\n * @param {WheelOptions} [options]\n * @return {Viewport} this\n */\n public wheel(options: any): this\n {\n this.plugins.add('wheel', new Wheel(this, options));\n\n return this;\n }\n\n /**\n * Animate the position and/or scale of the viewport\n *\n * @param {AnimateOptions} options\n * @returns {Viewport} this\n */\n public animate(options: any): this\n {\n this.plugins.add('animate', new Animate(this, options));\n\n return this;\n }\n\n /**\n * Enable clamping of zoom to constraints\n *\n * The minWidth/Height settings are how small the world can get (as it would appear on the screen)\n * before clamping. The maxWidth/maxHeight is how larger the world can scale (as it would appear on\n * the screen) before clamping.\n *\n * For example, if you have a world size of 1000 x 1000 and a screen size of 100 x 100, if you set\n * minWidth/Height = 100 then the world will not be able to zoom smaller than the screen size (ie,\n * zooming out so it appears smaller than the screen). Similarly, if you set maxWidth/Height = 100\n * the world will not be able to zoom larger than the screen size (ie, zooming in so it appears\n * larger than the screen).\n *\n * @param {ClampZoomOptions} [options]\n * @return {Viewport} this\n */\n public clampZoom(options: any): this\n {\n this.plugins.add('clamp-zoom', new ClampZoom(this, options));\n\n return this;\n }\n\n /**\n * Scroll viewport when mouse hovers near one of the edges or radius-distance from center of screen.\n *\n * NOTE: fires 'moved' event\n *\n * @param {MouseEdgesOptions} [options]\n */\n public mouseEdges(options: any): this\n {\n this.plugins.add('mouse-edges', new MouseEdges(this, options));\n\n return this;\n }\n\n /** Pause viewport (including animation updates such as decelerate) */\n get pause(): boolean\n {\n return !!this._pause;\n }\n set pause(value: boolean)\n {\n this._pause = value;\n\n this.lastViewport = null;\n this.moving = false;\n this.zooming = false;\n\n if (value)\n {\n this.input.pause();\n }\n }\n\n /**\n * Move the viewport so the bounding box is visible\n *\n * @param x - left\n * @param y - top\n * @param width\n * @param height\n * @param resizeToFit - Resize the viewport so the box fits within the viewport\n */\n public ensureVisible(x: number, y: number, width: number, height: number, resizeToFit?: boolean)\n {\n if (resizeToFit && (width > this.worldScreenWidth || height > this.worldScreenHeight))\n {\n this.fit(true, width, height);\n this.emit('zoomed', { viewport: this, type: 'ensureVisible' });\n }\n let moved = false;\n\n if (x < this.left)\n {\n this.left = x;\n moved = true;\n }\n else if (x + width > this.right)\n {\n this.right = x + width;\n moved = true;\n }\n if (y < this.top)\n {\n this.top = y;\n moved = true;\n }\n else if (y + height > this.bottom)\n {\n this.bottom = y + height;\n moved = true;\n }\n if (moved)\n {\n this.emit('moved', { viewport: this, type: 'ensureVisible' });\n }\n }\n}\n\n/**\n * Fires after a mouse or touch click\n * @event Viewport#clicked\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a drag starts\n * @event Viewport#drag-start\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a drag ends\n * @event Viewport#drag-end\n * @type {object}\n * @property {PIXI.Point} screen\n * @property {PIXI.Point} world\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a pinch starts\n * @event Viewport#pinch-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a pinch end\n * @event Viewport#pinch-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap starts\n * @event Viewport#snap-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap ends\n * @event Viewport#snap-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap-zoom starts\n * @event Viewport#snap-zoom-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a snap-zoom ends\n * @event Viewport#snap-zoom-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce starts in the x direction\n * @event Viewport#bounce-x-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce ends in the x direction\n * @event Viewport#bounce-x-end\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce starts in the y direction\n * @event Viewport#bounce-y-start\n * @type {Viewport}\n */\n\n/**\n * Fires when a bounce ends in the y direction\n * @event Viewport#bounce-y-end\n * @type {Viewport}\n */\n\n/**\n * Fires when for a mouse wheel event\n * @event Viewport#wheel\n * @type {object}\n * @property {object} wheel\n * @property {number} wheel.dx\n * @property {number} wheel.dy\n * @property {number} wheel.dz\n * @property {Viewport} viewport\n */\n\n/**\n * Fires when a wheel-scroll occurs\n * @event Viewport#wheel-scroll\n * @type {Viewport}\n */\n\n/**\n * Fires when a mouse-edge starts to scroll\n * @event Viewport#mouse-edge-start\n * @type {Viewport}\n */\n\n/**\n * Fires when the mouse-edge scrolling ends\n * @event Viewport#mouse-edge-end\n * @type {Viewport}\n */\n\n/**\n * Fires when viewport moves through UI interaction, deceleration, ensureVisible, or follow\n * @event Viewport#moved\n * @type {object}\n * @property {Viewport} viewport\n * @property {string} type - (drag, snap, pinch, follow, bounce-x, bounce-y,\n * clamp-x, clamp-y, decelerate, mouse-edges, wheel, ensureVisible)\n */\n\n/**\n * Fires when viewport moves through UI interaction, deceleration, ensureVisible, or follow\n * @event Viewport#zoomed\n * @type {object}\n * @property {Viewport} viewport\n * @property {string} type (drag-zoom, pinch, wheel, clamp-zoom, ensureVisible)\n */\n\n/**\n * Fires when viewport stops moving\n * @event Viewport#moved-end\n * @type {Viewport}\n */\n\n/**\n * Fires when viewport stops zooming\n * @event Viewport#zoomed-end\n * @type {Viewport}\n */\n\n/**\n* Fires at the end of an update frame\n* @event Viewport#frame-end\n* @type {Viewport}\n*/\n"],"names":[],"mappings":";u0BA+CA,wwGCEA,g3ICcA,06GCjCA,i5ECUA,s0DC2DA,gjJCpEA,+qDCiBA,03FCbA,8nECmBA,ymDCUA,8iFCNA,siLCxCA,0vCC+FA"} \ No newline at end of file diff --git a/index.d.ts b/index.d.ts index f4bce51c..9359d4d6 100644 --- a/index.d.ts +++ b/index.d.ts @@ -186,8 +186,8 @@ export declare interface IClampZoomOptions { minHeight?: number | null; maxWidth?: number | null; maxHeight?: number | null; - minScale?: number | null; - maxScale?: number | null; + minScale?: number | null | IScale; + maxScale?: number | null | IScale; } export declare interface ICompleteViewportOptions extends IViewportOptions { @@ -281,6 +281,11 @@ export declare interface IPinchOptions { axis?: 'all' | 'x' | 'y'; } +export declare interface IScale { + x: null | number; + y: null | number; +} + export declare interface ISnapOptions { topLeft?: boolean; friction?: number; diff --git a/src/Viewport.ts b/src/Viewport.ts index 17859943..21218862 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -5,18 +5,18 @@ import { Ticker } from '@pixi/ticker'; import { InputManager } from './InputManager'; import { PluginManager } from './PluginManager'; import { - Animate, - Bounce, - Clamp, - ClampZoom, - Decelerate, - Drag, - Follow, - MouseEdges, - Pinch, - Snap, - SnapZoom, - Wheel + Animate, IAnimateOptions, + Bounce, IBounceOptions, + Clamp, IClampOptions, + ClampZoom, IClampZoomOptions, + Decelerate, IDecelerateOptions, + Drag, IDragOptions, + Follow, IFollowOptions, + MouseEdges, IMouseEdgesOptions, + Pinch, IPinchOptions, + Snap, ISnapOptions, + SnapZoom, ISnapZoomOptions, + Wheel, IWheelOptions, } from './plugins'; import type { DisplayObject, IDestroyOptions } from '@pixi/display'; @@ -44,7 +44,8 @@ export interface IViewportOptions { threshold?: number; /** - * Whether the 'wheel' event is set to passive (note: if false, e.preventDefault() will be called when wheel is used over the viewport) + * Whether the 'wheel' event is set to passive (note: if false, e.preventDefault() will be called when wheel + * is used over the viewport) * * @default true */ @@ -231,7 +232,7 @@ export class Viewport extends Container } /** Overrides PIXI.Container's destroy to also remove the 'wheel' and PIXI.Ticker listeners */ - destroy(options: IDestroyOptions) + destroy(options: IDestroyOptions): void { if (!this.options.noTicker && this.tickerFunction) { @@ -273,7 +274,7 @@ export class Viewport extends Container { this.zooming = true; } - else if (this.zooming) + else if (this.zooming) { this.emit('zoomed-end', this); this.zooming = false; @@ -334,7 +335,7 @@ export class Viewport extends Container return this.width / this.scale.x; } - set worldWidth(value) + set worldWidth(value: number) { this._worldWidth = value; this.plugins.resize(); @@ -350,7 +351,7 @@ export class Viewport extends Container return this.height / this.scale.y; } - set worldHeight(value) + set worldHeight(value: number) { this._worldHeight = value; this.plugins.resize(); @@ -411,7 +412,7 @@ export class Viewport extends Container } /** World height in screen coordinates */ - get screenWorldHeight() + get screenWorldHeight(): number { return this.worldHeight * this.scale.y; } @@ -420,8 +421,8 @@ export class Viewport extends Container get center(): Point { return new Point( - this.worldScreenWidth / 2 - this.x / this.scale.x, - this.worldScreenHeight / 2 - this.y / this.scale.y, + (this.worldScreenWidth / 2) - (this.x / this.scale.x), + (this.worldScreenHeight / 2) - (this.y / this.scale.y), ); } set center(value: Point) @@ -430,12 +431,12 @@ export class Viewport extends Container } /** Move center of viewport to (x, y) */ - public moveCenter(x: number, y: number): this; + public moveCenter(x: number, y: number): Viewport; /** Move center of viewport to {@code center}. */ - public moveCenter(center: Point): this; + public moveCenter(center: Point): Viewport; - public moveCenter(...args: [number, number] | [Point]): this + public moveCenter(...args: [number, number] | [Point]): Viewport { let x: number; let y: number; @@ -451,8 +452,8 @@ export class Viewport extends Container y = args[0].y; } - const newX = (this.worldScreenWidth / 2 - x) * this.scale.x; - const newY = (this.worldScreenHeight / 2 - y) * this.scale.y; + const newX = ((this.worldScreenWidth / 2) - x) * this.scale.x; + const newY = ((this.worldScreenHeight / 2) - y) * this.scale.y; if (this.x !== newX || this.y !== newY) { @@ -474,13 +475,13 @@ export class Viewport extends Container this.moveCorner(value); } - /** Move viewport's top-left corner; also clamps and resets decelerate and bounce (as needed) */ - public moveCorner(x: number, y: number): this; + /** Move Viewport's top-left corner; also clamps and resets decelerate and bounce (as needed) */ + public moveCorner(x: number, y: number): Viewport; - /** move viewport's top-left corner; also clamps and resets decelerate and bounce (as needed) */ - public moveCorner(center: Point): this; + /** move Viewport's top-left corner; also clamps and resets decelerate and bounce (as needed) */ + public moveCorner(center: Point): Viewport; - public moveCorner(...args: [number, number] | [Point]) + public moveCorner(...args: [number, number] | [Point]): Viewport { let x; let y; @@ -507,13 +508,13 @@ export class Viewport extends Container } /** Get how many world pixels fit in screen's width */ - get screenWidthInWorldPixels() + get screenWidthInWorldPixels(): number { return this.screenWidth / this.scale.x; } /** Get how many world pixels fit on screen's height */ - get screenHeightInWorldPixels() + get screenHeightInWorldPixels(): number { return this.screenHeight / this.scale.y; } @@ -583,7 +584,7 @@ export class Viewport extends Container * @param noClamp - whether to disable clamp-zoom * @returns {Viewport} this */ - fitWidth(width = this.worldWidth, center?: boolean, scaleY = true, noClamp?: boolean): this + fitWidth(width = this.worldWidth, center?: boolean, scaleY = true, noClamp?: boolean): Viewport { let save: Point | undefined; @@ -622,7 +623,7 @@ export class Viewport extends Container * @param {boolean} [noClamp] whether to disable clamp-zoom * @returns {Viewport} this */ - fitHeight(height = this.worldHeight, center?: boolean, scaleX = true, noClamp?: boolean) + fitHeight(height = this.worldHeight, center?: boolean, scaleX = true, noClamp?: boolean): Viewport { let save: Point | undefined; @@ -658,7 +659,7 @@ export class Viewport extends Container * @param {boolean} center maintain the same center of the screen after zoom * @returns {Viewport} this */ - fitWorld(center?: boolean): this + fitWorld(center?: boolean): Viewport { let save: Point | undefined; @@ -702,7 +703,7 @@ export class Viewport extends Container * @param {number} [height=this.worldHeight] desired height * @returns {Viewport} this */ - fit(center?: boolean, width = this.worldWidth, height = this.worldHeight): this + fit(center?: boolean, width = this.worldWidth, height = this.worldHeight): Viewport { let save: Point | undefined; @@ -755,7 +756,7 @@ export class Viewport extends Container * @param {boolean} [center] maintain the same center of the screen after zoom * @return {Viewport} this */ - setZoom(scale: number, center?: boolean): this + setZoom(scale: number, center?: boolean): Viewport { let save; @@ -785,9 +786,9 @@ export class Viewport extends Container * @param {boolean} [center] maintain the same center of the screen after zoom * @return {Viewport} this */ - zoomPercent(percent: number, center?: boolean): this + zoomPercent(percent: number, center?: boolean): Viewport { - return this.setZoom(this.scale.x + this.scale.x * percent, center); + return this.setZoom(this.scale.x + (this.scale.x * percent), center); } /** @@ -797,7 +798,7 @@ export class Viewport extends Container * @param {boolean} [center] maintain the same center of the screen after zoom * @return {Viewport} this */ - zoom(change: number, center?: boolean) + zoom(change: number, center?: boolean): Viewport { this.fitWidth(change + this.worldScreenWidth, center); @@ -815,9 +816,23 @@ export class Viewport extends Container } /** - * @param {SnapZoomOptions} options + * Returns zoom to the desired scale + * + * @param {ISnapZoomOptions} options + * @param {number} [options.width=0] - the desired width to snap (to maintain aspect ratio, choose width or height) + * @param {number} [options.height=0] - the desired height to snap (to maintain aspect ratio, choose width or height) + * @param {number} [options.time=1000] - time for snapping in ms + * @param {(string|function)} [options.ease=easeInOutSine] ease function or name (see http://easings.net/ + * for supported names) + * @param {PIXI.Point} [options.center] - place this point at center during zoom instead of center of the viewport + * @param {boolean} [options.interrupt=true] - pause snapping with any user input on the viewport + * @param {boolean} [options.removeOnComplete] - removes this plugin after snapping is complete + * @param {boolean} [options.removeOnInterrupt] - removes this plugin if interrupted by any user input + * @param {boolean} [options.forceStart] - starts the snap immediately regardless of whether the viewport is at the + * desired zoom + * @param {boolean} [options.noMove] - zoom but do not move */ - snapZoom(options: any): this + snapZoom(options: ISnapZoomOptions): Viewport { this.plugins.add('snap-zoom', new SnapZoom(this, options)); @@ -831,7 +846,7 @@ export class Viewport extends Container top: boolean; bottom: boolean; cornerPoint: Point; - } + } { return { left: this.left < 0, @@ -839,8 +854,8 @@ export class Viewport extends Container top: this.top < 0, bottom: this.bottom > this.worldHeight, cornerPoint: new Point( - this.worldWidth * this.scale.x - this.screenWidth, - this.worldHeight * this.scale.y - this.screenHeight + (this.worldWidth * this.scale.x) - this.screenWidth, + (this.worldHeight * this.scale.y) - this.screenHeight ) }; } @@ -848,11 +863,11 @@ export class Viewport extends Container /** World coordinates of the right edge of the screen */ get right(): number { - return -this.x / this.scale.x + this.worldScreenWidth; + return (-this.x / this.scale.x) + this.worldScreenWidth; } set right(value: number) { - this.x = -value * this.scale.x + this.screenWidth; + this.x = (-value * this.scale.x) + this.screenWidth; this.plugins.reset(); } @@ -881,16 +896,16 @@ export class Viewport extends Container /** World coordinates of the bottom edge of the screen */ get bottom(): number { - return -this.y / this.scale.y + this.worldScreenHeight; + return (-this.y / this.scale.y) + this.worldScreenHeight; } set bottom(value: number) { - this.y = -value * this.scale.y + this.screenHeight; + this.y = (-value * this.scale.y) + this.screenHeight; this.plugins.reset(); } /** - * Determines whether the viewport is dirty (i.e., needs to be renderered to the screen because of a change) + * Determines whether the viewport is dirty (i.e., needs to be rendered to the screen because of a change) */ get dirty(): boolean { @@ -904,7 +919,8 @@ export class Viewport extends Container /** * Permanently changes the Viewport's hitArea * - * NOTE: if not set then hitArea = PIXI.Rectangle(Viewport.left, Viewport.top, Viewport.worldScreenWidth, Viewport.worldScreenHeight) + * NOTE: if not set then hitArea = PIXI.Rectangle(Viewport.left, Viewport.top, Viewport.worldScreenWidth, + * Viewport.worldScreenHeight) */ get forceHitArea(): IHitArea | null | undefined { @@ -930,10 +946,26 @@ export class Viewport extends Container * NOTE: if you expect users to use right-click dragging, you should enable `viewport.options.disableOnContextMenu` * to avoid the context menu popping up on each right-click drag. * - * @param {DragOptions} [options] + * @param {IDragOptions} [options] + * @param {string} [options.direction=all] direction to drag + * @param {boolean} [options.pressDrag=true] whether click to drag is active + * @param {boolean} [options.wheel=true] use wheel to scroll in direction (unless wheel plugin is active) + * @param {number} [options.wheelScroll=1] number of pixels to scroll with each wheel spin + * @param {boolean} [options.reverse] reverse the direction of the wheel scroll + * @param {(boolean|string)} [options.clampWheel=false] clamp wheel(to avoid weird bounce with mouse wheel) + * @param {string} [options.underflow=center] where to place world if too small for screen + * @param {number} [options.factor=1] factor to multiply drag to increase the speed of movement + * @param {string} [options.mouseButtons=all] changes which mouse buttons trigger drag, use: 'all', 'left', + * 'right' 'middle', or some combination, like, 'middle-right'; you may want to set + * viewport.options.disableOnContextMenu if you want to use right-click dragging + * @param {string[]} [options.keyToPress=null] - array containing + * {@link key|https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code} codes of keys that can be + * pressed for the drag to be triggered, e.g.: ['ShiftLeft', 'ShiftRight'}. + * @param {boolean} [options.ignoreKeyToPressOnTouch=false] - ignore keyToPress for touch events + * @param {number} [options.lineHeight=20] - scaling factor for non-DOM_DELTA_PIXEL scrolling events * @returns {Viewport} this */ - public drag(options: any): this + public drag(options: IDragOptions): Viewport { this.plugins.add('drag', new Drag(this, options)); @@ -942,15 +974,35 @@ export class Viewport extends Container /** * Clamp to world boundaries or other provided boundaries + * There are three ways to clamp: + * 1. direction: 'all' = the world is clamped to its world boundaries, ie, you cannot drag any part of offscreen + * direction: 'x' | 'y' = only the x or y direction is clamped to its world boundary + * 2. left, right, top, bottom = true | number = the world is clamped to the world's pixel location for each side; + * if any of these are set to true, then the location is set to the boundary + * [0, viewport.worldWidth/viewport.worldHeight], eg: to allow the world to be completely dragged offscreen, + * set [-viewport.worldWidth, -viewport.worldHeight, viewport.worldWidth * 2, viewport.worldHeight * 2] + * + * Underflow determines what happens when the world is smaller than the viewport + * 1. none = the world is clamped but there is no special behavior + * 2. center = the world is centered on the viewport + * 3. combination of top/bottom/center and left/right/center (case insensitive) = the world is stuck to the + * appropriate boundaries * * NOTES: * clamp is disabled if called with no options; use { direction: 'all' } for all edge clamping * screenWidth, screenHeight, worldWidth, and worldHeight needs to be set for this to work properly * - * @param {ClampOptions} [options] - * @returns {Viewport} this + * @param {object} [options] + * @param {(number|boolean)} [options.left=false] - clamp left; true = 0 + * @param {(number|boolean)} [options.right=false] - clamp right; true = viewport.worldWidth + * @param {(number|boolean)} [options.top=false] - clamp top; true = 0 + * @param {(number|boolean)} [options.bottom=false] - clamp bottom; true = viewport.worldHeight + * @param {string} [direction] - (all, x, or y) using clamps of [0, viewport.worldWidth/viewport.worldHeight]; + * replaces left/right/top/bottom if set + * @param {string} [underflow=center] - where to place world if too small for screen (e.g., top-right, center, + * none, bottomLeft) * @returns {Viewport} this */ - public clamp(options: any): this + public clamp(options: IClampOptions): Viewport { this.plugins.add('clamp', new Clamp(this, options)); @@ -962,10 +1014,14 @@ export class Viewport extends Container * * NOTE: this fires 'moved' event during deceleration * - * @param {DecelerateOptions} [options] + * @param {IDecelerateOptions} [options] + * @param {number} [options.friction=0.95] - percent to decelerate after movement + * @param {number} [options.bounce=0.8] - percent to decelerate when past boundaries (only applicable when + * viewport.bounce() is active) + * @param {number} [options.minSpeed=0.01] - minimum velocity before stopping/reversing acceleration * @return {Viewport} this */ - public decelerate(options: any): this + public decelerate(options: IDecelerateOptions): Viewport { this.plugins.add('decelerate', new Decelerate(this, options)); @@ -978,19 +1034,22 @@ export class Viewport extends Container * screenWidth, screenHeight, worldWidth, and worldHeight needs to be set for this to work properly * fires 'moved', 'bounce-x-start', 'bounce-y-start', 'bounce-x-end', and 'bounce-y-end' events * @param {object} [options] - * @param {string} [options.sides=all] all, horizontal, vertical, or combination of top, bottom, right, left (e.g., 'top-bottom-right') - * @param {number} [options.friction=0.5] friction to apply to decelerate if active - * @param {number} [options.time=150] time in ms to finish bounce - * @param {object} [options.bounceBox] use this bounceBox instead of (0, 0, viewport.worldWidth, viewport.worldHeight) + * @param {string} [options.sides=all] - all, horizontal, vertical, or combination of top, bottom, right, left + * (e.g., 'top-bottom-right') + * @param {number} [options.friction=0.5] - friction to apply to decelerate if active + * @param {number} [options.time=150] - time in ms to finish bounce + * @param {object} [options.bounceBox] - use this bounceBox instead of (0, 0, viewport.worldWidth, viewport.worldHeight) * @param {number} [options.bounceBox.x=0] * @param {number} [options.bounceBox.y=0] * @param {number} [options.bounceBox.width=viewport.worldWidth] * @param {number} [options.bounceBox.height=viewport.worldHeight] - * @param {string|function} [options.ease=easeInOutSine] ease function or name (see http://easings.net/ for supported names) - * @param {string} [options.underflow=center] (top/bottom/center and left/right/center, or center) where to place world if too small for screen + * @param {string|function} [options.ease=easeInOutSine] - ease function or name + * (see http://easings.net/ for supported names) + * @param {string} [options.underflow=center] - (top/bottom/center and left/right/center, or center) + * where to place world if too small for screen * @return {Viewport} this */ - public bounce(options: any): this + public bounce(options: IBounceOptions): Viewport { this.plugins.add('bounce', new Bounce(this, options)); @@ -1001,9 +1060,14 @@ export class Viewport extends Container * Enable pinch to zoom and two-finger touch to drag * * @param {PinchOptions} [options] + * @param {boolean} [options.noDrag] - disable two-finger dragging + * @param {number} [options.percent=1] - percent to modify pinch speed + * @param {number} [options.factor=1] - factor to multiply two-finger drag to increase the speed of movement + * @param {PIXI.Point} [options.center] - place this point at center during zoom instead of center of two fingers + * @param {('all'|'x'|'y')} [options.axis=all] - axis to zoom * @return {Viewport} this */ - public pinch(options: any): this + public pinch(options: IPinchOptions): Viewport { this.plugins.add('pinch', new Pinch(this, options)); @@ -1015,10 +1079,20 @@ export class Viewport extends Container * * @param {number} x * @param {number} y - * @param {SnapOptions} [options] + * @param {ISnapOptions} [options] + * @param {boolean} [options.topLeft] - snap to the top-left of viewport instead of center + * @param {number} [options.friction=0.8] - friction/frame to apply if decelerate is active + * @param {number} [options.time=1000] - time in ms to snap + * @param {string|function} [options.ease=easeInOutSine] - ease function or name (see http://easings.net/ + * for supported names) + * @param {boolean} [options.interrupt=true] - pause snapping with any user input on the viewport + * @param {boolean} [options.removeOnComplete] - removes this plugin after snapping is complete + * @param {boolean} [options.removeOnInterrupt] - removes this plugin if interrupted by any user input + * @param {boolean} [options.forceStart] - starts the snap immediately regardless of whether the viewport is at + * the desired location * @return {Viewport} this */ - public snap(x: number, y: number, options: any): this + public snap(x: number, y: number, options: ISnapOptions): Viewport { this.plugins.add('snap', new Snap(this, x, y, options)); @@ -1033,11 +1107,17 @@ export class Viewport extends Container * options.acceleration is not perfect as it doesn't know the velocity of the target * it adds acceleration to the start of movement and deceleration to the end of movement when the target is stopped * fires 'moved' event + * * @param {PIXI.DisplayObject} target to follow - * @param {FollowOptions} [options] + * @param {IFollowOptions} [options] + * @param {number} [options.speed=0] - to follow in pixels/frame (0=teleport to location) + * @param {number} [options.acceleration] - set acceleration to accelerate and decelerate at this rate; speed + * cannot be 0 to use acceleration + * @param {number} [options.radius] - radius (in world coordinates) of center circle where movement is allowed + * without moving the viewport * @returns {Viewport} this * @returns {Viewport} this */ - public follow(target: DisplayObject, options: any): this + public follow(target: DisplayObject, options: IFollowOptions): Viewport { this.plugins.add('follow', new Follow(this, target, options)); @@ -1047,10 +1127,20 @@ export class Viewport extends Container /** * Zoom using mouse wheel * - * @param {WheelOptions} [options] + * NOTE: the default event listener for 'wheel' event is document.body. Use `Viewport.options.divWheel` to + * change this default + * + * @param {IWheelOptions} [options] + * @param {number} [options.percent=0.1] - percent to scroll with each spin + * @param {number} [options.smooth] - smooth the zooming by providing the number of frames to zoom between wheel spins + * @param {boolean} [options.interrupt=true] - stop smoothing with any user input on the viewport + * @param {boolean} [options.reverse] - reverse the direction of the scroll + * @param {PIXI.Point} [options.center] - place this point at center during zoom instead of current mouse position + * @param {number} [options.lineHeight=20] - scaling factor for non-DOM_DELTA_PIXEL scrolling events + * @param {('all'|'x'|'y')} [options.axis=all] - axis to zoom * @return {Viewport} this */ - public wheel(options: any): this + public wheel(options: IWheelOptions): Viewport { this.plugins.add('wheel', new Wheel(this, options)); @@ -1059,11 +1149,23 @@ export class Viewport extends Container /** * Animate the position and/or scale of the viewport - * - * @param {AnimateOptions} options + * To set the zoom level, use: (1) scale, (2) scaleX and scaleY, or (3) width and/or height + * @params {object} options + * @params {number} [options.time=1000] - time to animate + * @params {PIXI.Point} [options.position=viewport.center] - position to move viewport + * @params {number} [options.width] - desired viewport width in world pixels (use instead of scale; + * aspect ratio is maintained if height is not provided) + * @params {number} [options.height] - desired viewport height in world pixels (use instead of scale; + * aspect ratio is maintained if width is not provided) + * @params {number} [options.scale] - scale to change zoom (scale.x = scale.y) + * @params {number} [options.scaleX] - independently change zoom in x-direction + * @params {number} [options.scaleY] - independently change zoom in y-direction + * @params {(function|string)} [options.ease=linear] - easing function to use + * @params {function} [options.callbackOnComplete] + * @params {boolean} [options.removeOnInterrupt] removes this plugin if interrupted by any user input * @returns {Viewport} this */ - public animate(options: any): this + public animate(options: IAnimateOptions): Viewport { this.plugins.add('animate', new Animate(this, options)); @@ -1083,10 +1185,16 @@ export class Viewport extends Container * the world will not be able to zoom larger than the screen size (ie, zooming in so it appears * larger than the screen). * - * @param {ClampZoomOptions} [options] + * @param {object} [options] + * @param {number} [options.minWidth] - minimum width + * @param {number} [options.minHeight] - minimum height + * @param {number} [options.maxWidth] - maximum width + * @param {number} [options.maxHeight] - maximum height + * @param {number} [options.minScale] - minimum scale + * @param {number} [options.maxScale] - minimum scale * @return {Viewport} this */ - public clampZoom(options: any): this + public clampZoom(options: IClampZoomOptions): Viewport { this.plugins.add('clamp-zoom', new ClampZoom(this, options)); @@ -1096,11 +1204,23 @@ export class Viewport extends Container /** * Scroll viewport when mouse hovers near one of the edges or radius-distance from center of screen. * - * NOTE: fires 'moved' event + * NOTES: fires 'moved' event; there's a known bug where the mouseEdges does not work properly with "windowed" viewports * - * @param {MouseEdgesOptions} [options] + * @param {IMouseEdgesOptions} [options] + * @param {number} [options.radius] - distance from center of screen in screen pixels + * @param {number} [options.distance] - distance from all sides in screen pixels + * @param {number} [options.top] - alternatively, set top distance (leave unset for no top scroll) + * @param {number} [options.bottom] - alternatively, set bottom distance (leave unset for no top scroll) + * @param {number} [options.left] - alternatively, set left distance (leave unset for no top scroll) + * @param {number} [options.right] - alternatively, set right distance (leave unset for no top scroll) + * @param {number} [options.speed=8] - speed in pixels/frame to scroll viewport + * @param {boolean} [options.reverse] - reverse direction of scroll + * @param {boolean} [options.noDecelerate] - don't use decelerate plugin even if it's installed + * @param {boolean} [options.linear] - if using radius, use linear movement (+/- 1, +/- 1) instead of angled + * movement (Math.cos(angle from center), Math.sin(angle from center)) + * @param {boolean} [options.allowButtons] allows plugin to continue working even when there's a mousedown event */ - public mouseEdges(options: any): this + public mouseEdges(options: IMouseEdgesOptions): Viewport { this.plugins.add('mouse-edges', new MouseEdges(this, options)); @@ -1135,7 +1255,7 @@ export class Viewport extends Container * @param height * @param resizeToFit - Resize the viewport so the box fits within the viewport */ - public ensureVisible(x: number, y: number, width: number, height: number, resizeToFit?: boolean) + public ensureVisible(x: number, y: number, width: number, height: number, resizeToFit?: boolean): void { if (resizeToFit && (width > this.worldScreenWidth || height > this.worldScreenHeight)) { diff --git a/src/plugins/ClampZoom.ts b/src/plugins/ClampZoom.ts index 081a9af5..3f31c84c 100644 --- a/src/plugins/ClampZoom.ts +++ b/src/plugins/ClampZoom.ts @@ -22,10 +22,10 @@ export interface IClampZoomOptions maxHeight?: number | null; /** Minimum scale */ - minScale?: number | null; + minScale?: number | null | IScale; /** Maximum scale */ - maxScale?: number | null; + maxScale?: number | null | IScale; } const DEFAULT_CLAMP_ZOOM_OPTIONS: Required = { @@ -62,7 +62,7 @@ export class ClampZoom extends Plugin this.clamp(); } - /** Clamp the viewport's zoom immediately. */ + /** Clamp the viewport scale zoom) */ public clamp(): void { if (this.paused) @@ -115,20 +115,59 @@ export class ClampZoom extends Plugin } } else + if (this.options.minScale || this.options.maxScale) { - let scale = this.parent.scale.x; + const minScale: IScale = { x: null, y: null }; + const maxScale: IScale = { x: null, y: null }; - if (this.options.minScale !== null && scale < this.options.minScale) + if (typeof this.options.minScale === 'number') { - scale = this.options.minScale; + minScale.x = this.options.minScale; + minScale.y = this.options.minScale; } - if (this.options.maxScale !== null && scale > this.options.maxScale) + else if (this.options.minScale !== null) { - scale = this.options.maxScale; + const optsMinScale = this.options.minScale as IScale; + + minScale.x = typeof optsMinScale.x === 'undefined' ? null : optsMinScale.x; + minScale.y = typeof optsMinScale.y === 'undefined' ? null : optsMinScale.y; + } + + if (typeof this.options.maxScale === 'number') + { + maxScale.x = this.options.maxScale; + maxScale.y = this.options.maxScale; + } + else if (this.options.maxScale !== null) + { + const optsMaxScale = this.options.maxScale as IScale; + + maxScale.x = typeof optsMaxScale.x === 'undefined' ? null : optsMaxScale.x; + maxScale.y = typeof optsMaxScale.y === 'undefined' ? null : optsMaxScale.y; } - if (scale !== this.parent.scale.x) + + let scaleX = this.parent.scale.x; + let scaleY = this.parent.scale.y; + + if (minScale.x !== null && scaleX < minScale.x) { - this.parent.scale.set(scale); + scaleX = minScale.x; + } + if (maxScale.x !== null && scaleX > maxScale.x) + { + scaleX = maxScale.x; + } + if (minScale.y !== null && scaleY < minScale.y) + { + scaleY = minScale.y; + } + if (maxScale.y !== null && scaleY > maxScale.y) + { + scaleY = maxScale.y; + } + if (scaleX !== this.parent.scale.x || scaleY !== this.parent.scale.y) + { + this.parent.scale.set(scaleX, scaleY); this.parent.emit('zoomed', { viewport: this.parent, type: 'clamp-zoom' }); } } @@ -139,3 +178,9 @@ export class ClampZoom extends Plugin this.clamp(); } } + +/** This allows independent x and y values for min/maxScale */ +export interface IScale { + x: null | number + y: null | number +} diff --git a/test/clamp-zoom.js b/test/clamp-zoom.js index 34a695d2..9a37ab13 100644 --- a/test/clamp-zoom.js +++ b/test/clamp-zoom.js @@ -1,87 +1,136 @@ -require('./node-shim') -const assert = require('chai').assert -const Viewport = require('../').Viewport +require('./node-shim'); +const assert = require('chai').assert; +const Viewport = require('../').Viewport; describe('pixi-viewport', () => { - it('clampZoom with default options', () => { - const viewport = new Viewport({ screenWidth: 100, screenHeight: 100, worldWidth: 200, worldHeight: 200 }) - viewport.clampZoom() - const clampZoom = viewport.plugins.get('clamp-zoom') - assert.isNull(clampZoom.options.minWidth) - assert.isNull(clampZoom.options.minHeight) - assert.isNull(clampZoom.options.maxWidth) - assert.isNull(clampZoom.options.maxHeight) - assert.isNull(clampZoom.options.minScale) - assert.isNull(clampZoom.options.maxScale) - viewport.destroy() - }) + it('clampZoom with default options', () => + { + const viewport = new Viewport({ screenWidth: 100, screenHeight: 100, worldWidth: 200, worldHeight: 200 }); - it('clampZoom with minScale/maxScale', () => { - const viewport = new Viewport({ screenWidth: 100, screenHeight: 100, worldWidth: 200, worldHeight: 200 }) - viewport.clampZoom({ minScale: 0.25 }) - viewport.setZoom(0.15) - assert.equal(viewport.scale.x, 0.25) - assert.equal(viewport.scale.y, 0.25) - viewport.setZoom(0.3) - assert.equal(viewport.scale.x, 0.3) - assert.equal(viewport.scale.y, 0.3) - viewport.clampZoom({ maxScale: 2 }) - viewport.setZoom(3) - assert.equal(viewport.scale.x, 2) - assert.equal(viewport.scale.y, 2) - viewport.setZoom(1.8) - assert.equal(viewport.scale.x, 1.8) - assert.equal(viewport.scale.y, 1.8) - viewport.clampZoom({ minScale: 0.5, maxScale: 2 }) - viewport.setZoom(0.3) - assert.equal(viewport.scale.x, 0.5) - assert.equal(viewport.scale.y, 0.5) - viewport.setZoom(3) - assert.equal(viewport.scale.x, 2) - assert.equal(viewport.scale.y, 2) - viewport.setZoom(1.2) - assert.equal(viewport.scale.x, 1.2) - assert.equal(viewport.scale.y, 1.2) - viewport.destroy() - }) + viewport.clampZoom(); + const clampZoom = viewport.plugins.get('clamp-zoom'); - it('clampZoom with min/max Width/Height', () => { - const viewport = new Viewport({ screenWidth: 100, screenHeight: 100, worldWidth: 200, worldHeight: 200 }) - viewport.clampZoom({ minWidth: 50 }) - viewport.fitWidth(25) - assert.equal(viewport.worldScreenWidth, 50) - viewport.fitWidth(60) - assert.equal(viewport.worldScreenWidth, 60) + assert.isNull(clampZoom.options.minWidth); + assert.isNull(clampZoom.options.minHeight); + assert.isNull(clampZoom.options.maxWidth); + assert.isNull(clampZoom.options.maxHeight); + assert.isNull(clampZoom.options.minScale); + assert.isNull(clampZoom.options.maxScale); + viewport.destroy(); + }); - viewport.clampZoom({ maxWidth: 100 }) - viewport.fitWidth(200) - assert.equal(viewport.worldScreenWidth, 100) - viewport.fitWidth(75) - assert.equal(viewport.worldScreenWidth, 75) + it('clampZoom with minScale/maxScale', () => + { + const viewport = new Viewport({ screenWidth: 100, screenHeight: 100, worldWidth: 200, worldHeight: 200 }); - viewport.clampZoom({ minHeight: 50 }) - viewport.fitHeight(25) - assert.equal(viewport.worldScreenHeight, 50) - viewport.fitHeight(60) - assert.equal(viewport.worldScreenHeight, 60) + viewport.clampZoom({ minScale: 0.25 }); + viewport.setZoom(0.15); + assert.equal(viewport.scale.x, 0.25); + assert.equal(viewport.scale.y, 0.25); + viewport.setZoom(0.3); + assert.equal(viewport.scale.x, 0.3); + assert.equal(viewport.scale.y, 0.3); + viewport.clampZoom({ maxScale: 2 }); + viewport.setZoom(3); + assert.equal(viewport.scale.x, 2); + assert.equal(viewport.scale.y, 2); + viewport.setZoom(1.8); + assert.equal(viewport.scale.x, 1.8); + assert.equal(viewport.scale.y, 1.8); - viewport.clampZoom({ maxHeight: 100 }) - viewport.fitHeight(200) - assert.equal(viewport.worldScreenHeight, 100) - viewport.fitHeight(75) - assert.equal(viewport.worldScreenHeight, 75) + viewport.clampZoom({ minScale: 0.5, maxScale: 2 }); + viewport.setZoom(0.3); + assert.equal(viewport.scale.x, 0.5); + assert.equal(viewport.scale.y, 0.5); + viewport.setZoom(3); + assert.equal(viewport.scale.x, 2); + assert.equal(viewport.scale.y, 2); + viewport.setZoom(1.2); + assert.equal(viewport.scale.x, 1.2); + assert.equal(viewport.scale.y, 1.2); - viewport.clampZoom({ minWidth: 10, minHeight: 20, maxWidth: 100, maxHeight: 150 }) - viewport.fitWidth(5) - assert.equal(viewport.worldScreenWidth, 20) - viewport.fitWidth(110) - assert.equal(viewport.worldScreenWidth, 100) - viewport.fitHeight(10) - assert.equal(viewport.worldScreenHeight, 20) - viewport.fitHeight(160) - assert.equal(viewport.worldScreenHeight, 100) + viewport.clampZoom({ minScale: { x: 1 }, maxScale: { x: 1 } }); + viewport.setZoom(0.3); + assert.equal(viewport.scale.x, 1); + assert.equal(viewport.scale.y, 0.3); + viewport.setZoom(3); + assert.equal(viewport.scale.x, 1); + assert.equal(viewport.scale.y, 3); + viewport.setZoom(1.2); + assert.equal(viewport.scale.x, 1); + assert.equal(viewport.scale.y, 1.2); + viewport.clampZoom({ minScale: { y: 1 }, maxScale: { y: 1 } }); + viewport.setZoom(0.3); + assert.equal(viewport.scale.x, 0.3); + assert.equal(viewport.scale.y, 1); + viewport.setZoom(3); + assert.equal(viewport.scale.x, 3); + assert.equal(viewport.scale.y, 1); + viewport.setZoom(1.2); + assert.equal(viewport.scale.x, 1.2); + assert.equal(viewport.scale.y, 1); + viewport.clampZoom({ minScale: { x: 1, y: 0.5 }, maxScale: { x: 1, y: 2 } }); + viewport.setZoom(0.3); + assert.equal(viewport.scale.x, 1); + assert.equal(viewport.scale.y, 0.5); + viewport.setZoom(3); + assert.equal(viewport.scale.x, 1); + assert.equal(viewport.scale.y, 2); + viewport.setZoom(1.2); + assert.equal(viewport.scale.x, 1); + assert.equal(viewport.scale.y, 1.2); + viewport.clampZoom({ minScale: { x: 0.5, y: 1 }, maxScale: { x: 2, y: 1 } }); + viewport.setZoom(0.3); + assert.equal(viewport.scale.x, 0.5); + assert.equal(viewport.scale.y, 1); + viewport.setZoom(3); + assert.equal(viewport.scale.x, 2); + assert.equal(viewport.scale.y, 1); + viewport.setZoom(1.2); + assert.equal(viewport.scale.x, 1.2); + assert.equal(viewport.scale.y, 1); + viewport.destroy(); + }); - viewport.destroy() - }) -}) \ No newline at end of file + it('clampZoom with min/max Width/Height', () => + { + const viewport = new Viewport({ screenWidth: 100, screenHeight: 100, worldWidth: 200, worldHeight: 200 }); + + viewport.clampZoom({ minWidth: 50 }); + viewport.fitWidth(25); + assert.equal(viewport.worldScreenWidth, 50); + viewport.fitWidth(60); + assert.equal(viewport.worldScreenWidth, 60); + + viewport.clampZoom({ maxWidth: 100 }); + viewport.fitWidth(200); + assert.equal(viewport.worldScreenWidth, 100); + viewport.fitWidth(75); + assert.equal(viewport.worldScreenWidth, 75); + + viewport.clampZoom({ minHeight: 50 }); + viewport.fitHeight(25); + assert.equal(viewport.worldScreenHeight, 50); + viewport.fitHeight(60); + assert.equal(viewport.worldScreenHeight, 60); + + viewport.clampZoom({ maxHeight: 100 }); + viewport.fitHeight(200); + assert.equal(viewport.worldScreenHeight, 100); + viewport.fitHeight(75); + assert.equal(viewport.worldScreenHeight, 75); + + viewport.clampZoom({ minWidth: 10, minHeight: 20, maxWidth: 100, maxHeight: 150 }); + viewport.fitWidth(5); + assert.equal(viewport.worldScreenWidth, 20); + viewport.fitWidth(110); + assert.equal(viewport.worldScreenWidth, 100); + viewport.fitHeight(10); + assert.equal(viewport.worldScreenHeight, 20); + viewport.fitHeight(160); + assert.equal(viewport.worldScreenHeight, 100); + + viewport.destroy(); + }); +}); diff --git a/test/follow.js b/test/follow.js index 801ef721..01924c74 100644 --- a/test/follow.js +++ b/test/follow.js @@ -1,111 +1,122 @@ -require('./node-shim') -const assert = require('chai').assert -const Viewport = require('../').Viewport +require('./node-shim'); +const assert = require('chai').assert; +const Viewport = require('../').Viewport; describe('follow', () => { it('default options', () => { - const viewport = new Viewport({ screenWidth: 100, screenHeight: 200 }) - const target = { x: 10, y: 11 } - viewport.follow(target) - const follow = viewport.plugins.get('follow') - assert.equal(follow.options.speed, 0) - assert.isNull(follow.options.acceleration) - assert.isNull(follow.options.radius) - target.x = 20 - target.y = 21 - assert.equal(viewport.center.x, 50) - assert.equal(viewport.center.y, 100) + const viewport = new Viewport({ screenWidth: 100, screenHeight: 200 }); + const target = { x: 10, y: 11 }; + + viewport.follow(target); + const follow = viewport.plugins.get('follow'); + + assert.equal(follow.options.speed, 0); + assert.isNull(follow.options.acceleration); + assert.isNull(follow.options.radius); + target.x = 20; + target.y = 21; + assert.equal(viewport.center.x, 50); + assert.equal(viewport.center.y, 100); requestAnimationFrame(() => { - assert.equal(viewport.center.x, 20) - assert.equal(viewport.center.y, 21) - viewport.destroy() - }) - }) + assert.equal(viewport.center.x, 20); + assert.equal(viewport.center.y, 21); + viewport.destroy(); + }); + }); it('paused', () => { - const viewport = new Viewport({ screenWidth: 100, screenHeight: 200 }) - const target = { x: 10, y: 11 } - viewport.follow(target) - viewport.plugins.pause('follow') + const viewport = new Viewport({ screenWidth: 100, screenHeight: 200 }); + const target = { x: 10, y: 11 }; + + viewport.follow(target); + viewport.plugins.pause('follow'); requestAnimationFrame(() => { - assert.equal(viewport.center.x, 50) - assert.equal(viewport.center.y, 100) - viewport.destroy() - }) - }) + assert.equal(viewport.center.x, 50); + assert.equal(viewport.center.y, 100); + viewport.destroy(); + }); + }); it('speed', () => { - const viewport = new Viewport({ screenWidth: 100, screenHeight: 200 }) - const target = { x: 10, y: 11 } - viewport.follow(target, { speed: 2 }) + const viewport = new Viewport({ screenWidth: 100, screenHeight: 200 }); + const target = { x: 10, y: 11 }; + + viewport.follow(target, { speed: 2 }); requestAnimationFrame(() => { - assert.equal(Math.floor(viewport.center.x), 49) - assert.equal(Math.floor(viewport.center.y), 98) - }) + assert.isTrue(Math.abs(Math.floor(viewport.center.x) - 49) < 1); + assert.isTrue(Math.abs(Math.floor(viewport.center.y) - 98) < 1); + }); setTimeout(() => { - assert.equal(viewport.center.x, 10) - assert.equal(viewport.center.y, 11) - viewport.destroy() - }, 1000) - }) + assert.equal(viewport.center.x, 10); + assert.equal(viewport.center.y, 11); + viewport.destroy(); + }, 1000); + }); it('radius', () => { - const viewport = new Viewport({ screenWidth: 100, screenHeight: 200 }) - const target = { x: 10, y: 11 } - viewport.follow(target, { radius: 10, speed: 5 }) - let count = 0 - viewport.on('frame-end', () => { - count++ + const viewport = new Viewport({ screenWidth: 100, screenHeight: 200 }); + const target = { x: 10, y: 11 }; + + viewport.follow(target, { radius: 10, speed: 5 }); + let count = 0; + + viewport.on('frame-end', () => + { + count++; if (count === 10) { - assert.equal(Math.floor(viewport.center.x), 29) - assert.equal(Math.floor(viewport.center.y), 54) - viewport.moveCenter(15, 15) + assert.equal(Math.floor(viewport.center.x), 29); + assert.equal(Math.floor(viewport.center.y), 54); + viewport.moveCenter(15, 15); setTimeout(() => { - assert.equal(viewport.center.x, 15) - assert.equal(viewport.center.y, 15) - viewport.destroy() - }, 1000) + assert.equal(viewport.center.x, 15); + assert.equal(viewport.center.y, 15); + viewport.destroy(); + }, 1000); } - }) - }) + }); + }); it('speed and acceleration', () => { - const viewport = new Viewport({ screenWidth: 100, screenHeight: 200 }) - const target = { x: 10, y: 11 } - viewport.follow(target, { acceleration: 0.1, speed: 5 }) + const viewport = new Viewport({ screenWidth: 100, screenHeight: 200 }); + const target = { x: 10, y: 11 }; + + viewport.follow(target, { acceleration: 0.1, speed: 5 }); viewport.once('frame-end', () => { - assert.equal(Math.floor(viewport.center.x), 48) - assert.closeTo(Math.floor(viewport.center.y), 99, 2) - }) + assert.closeTo(Math.floor(viewport.center.x), 48, 2); + assert.closeTo(Math.floor(viewport.center.y), 99, 5); + }); setTimeout(() => { - assert.equal(Math.floor(viewport.center.x), 10) - assert.equal(Math.floor(viewport.center.y), 11) - viewport.destroy() - }, 1000) - }) + assert.equal(Math.floor(viewport.center.x), 10); + assert.equal(Math.floor(viewport.center.y), 11); + viewport.destroy(); + }, 1000); + }); + + it('acceleration to a stop', () => + { + const viewport = new Viewport({ screenWidth: 100, screenHeight: 200 }); + const target = { x: 40, y: 90 }; - it('acceleration to a stop', () => { - const viewport = new Viewport({ screenWidth: 100, screenHeight: 200 }) - const target = { x: 40, y: 90 } - viewport.follow(target, { acceleration: 0.01, speed: 5 }) - setTimeout(() => { - assert.equal(viewport.center.x, 40) - assert.equal(viewport.center.y, 90) - viewport.destroy() - }, 2000) - }) -}) \ No newline at end of file + viewport.follow(target, { acceleration: 0.01, speed: 5 }); + setTimeout(() => + { + assert.equal(viewport.center.x, 40); + assert.equal(viewport.center.y, 90); + viewport.destroy(); + }, 2000); + }); +}); diff --git a/test/mouse-edges.js b/test/mouse-edges.js index eb010b5e..394b25a6 100644 --- a/test/mouse-edges.js +++ b/test/mouse-edges.js @@ -1,44 +1,48 @@ -require('./node-shim') -const assert = require('chai').assert -const Viewport = require('../').Viewport +require('./node-shim'); +const assert = require('chai').assert; +const Viewport = require('../').Viewport; describe('mouseEdges', () => { it('default options', () => { - const viewport = new Viewport({ screenWidth: 100, screenHeight: 200 }) - viewport.mouseEdges() - const mouseEdges = viewport.plugins.get('mouse-edges') - assert.isNull(mouseEdges.options.radius) - assert.isNull(mouseEdges.options.distance) - assert.isNull(mouseEdges.options.top) - assert.isNull(mouseEdges.options.bottom) - assert.isNull(mouseEdges.options.left) - assert.isNull(mouseEdges.options.right) - assert.equal(mouseEdges.options.speed, 8) - assert.equal(mouseEdges.reverse, -1) - assert.isFalse(mouseEdges.options.noDecelerate) - assert.isFalse(mouseEdges.options.linear) - assert.isFalse(mouseEdges.options.allowButtons) - viewport.destroy() - }) + const viewport = new Viewport({ screenWidth: 100, screenHeight: 200 }); + + viewport.mouseEdges(); + const mouseEdges = viewport.plugins.get('mouse-edges'); + + assert.isNull(mouseEdges.options.radius); + assert.isNull(mouseEdges.options.distance); + assert.isNull(mouseEdges.options.top); + assert.isNull(mouseEdges.options.bottom); + assert.isNull(mouseEdges.options.left); + assert.isNull(mouseEdges.options.right); + assert.equal(mouseEdges.options.speed, 8); + assert.equal(mouseEdges.reverse, -1); + assert.isFalse(mouseEdges.options.noDecelerate); + assert.isFalse(mouseEdges.options.linear); + assert.isFalse(mouseEdges.options.allowButtons); + viewport.destroy(); + }); it('options', () => { - const viewport = new Viewport({ screenWidth: 100, screenHeight: 200 }) - viewport.mouseEdges() - const mouseEdges = viewport.plugins.get('mouse-edges') - assert.isNull(mouseEdges.options.radius) - assert.isNull(mouseEdges.options.distance) - assert.isNull(mouseEdges.options.top) - assert.isNull(mouseEdges.options.bottom) - assert.isNull(mouseEdges.options.left) - assert.isNull(mouseEdges.options.right) - assert.equal(mouseEdges.options.speed, 8) - assert.isFalse(mouseEdges.options.reverse) - assert.isFalse(mouseEdges.options.noDecelerate) - assert.isFalse(mouseEdges.options.linear) - assert.isFalse(mouseEdges.options.allowButtons) - viewport.destroy() - }) -}) \ No newline at end of file + const viewport = new Viewport({ screenWidth: 100, screenHeight: 200 }); + + viewport.mouseEdges(); + const mouseEdges = viewport.plugins.get('mouse-edges'); + + assert.isNull(mouseEdges.options.radius); + assert.isNull(mouseEdges.options.distance); + assert.isNull(mouseEdges.options.top); + assert.isNull(mouseEdges.options.bottom); + assert.isNull(mouseEdges.options.left); + assert.isNull(mouseEdges.options.right); + assert.equal(mouseEdges.options.speed, 8); + assert.isFalse(mouseEdges.options.reverse); + assert.isFalse(mouseEdges.options.noDecelerate); + assert.isFalse(mouseEdges.options.linear); + assert.isFalse(mouseEdges.options.allowButtons); + viewport.destroy(); + }); +});