UNPKG

@photo-sphere-viewer/core

Version:

A JavaScript library to display 360° panoramas

1 lines 413 kB
{"version":3,"sources":["src/index.ts","src/data/constants.ts","src/icons/arrow.svg","src/icons/close.svg","src/icons/download.svg","src/icons/fullscreen-in.svg","src/icons/fullscreen-out.svg","src/icons/info.svg","src/icons/menu.svg","src/icons/zoom-in.svg","src/icons/zoom-out.svg","src/utils/index.ts","src/utils/math.ts","src/utils/browser.ts","src/utils/misc.ts","src/utils/psv.ts","src/PSVError.ts","src/utils/Animation.ts","src/utils/Dynamic.ts","src/utils/MultiDynamic.ts","src/utils/PressHandler.ts","src/utils/Slider.ts","src/events.ts","src/lib/TypedEventTarget.ts","src/adapters/AbstractAdapter.ts","src/adapters/DualFisheyeAdapter.ts","src/adapters/EquirectangularAdapter.ts","src/data/system.ts","src/components/AbstractComponent.ts","src/buttons/AbstractButton.ts","src/buttons/CustomButton.ts","src/buttons/DescriptionButton.ts","src/buttons/DownloadButton.ts","src/buttons/FullscreenButton.ts","src/buttons/MenuButton.ts","src/buttons/AbstractMoveButton.ts","src/buttons/MoveDownButton.ts","src/buttons/MoveLeftButton.ts","src/buttons/MoveRightButton.ts","src/buttons/MoveUpButton.ts","src/buttons/AbstractZoomButton.ts","src/buttons/ZoomInButton.ts","src/buttons/ZoomOutButton.ts","src/buttons/ZoomRangeButton.ts","src/data/config.ts","src/plugins/AbstractPlugin.ts","src/components/NavbarCaption.ts","src/components/Navbar.ts","src/data/cache.ts","src/components/Loader.ts","src/components/Notification.ts","src/components/Overlay.ts","src/components/Panel.ts","src/components/Tooltip.ts","src/icons/error.svg","src/services/DataHelper.ts","src/services/AbstractService.ts","src/services/EventsHandler.ts","src/icons/gesture.svg","src/icons/mousewheel.svg","src/services/Renderer.ts","src/lib/BlobLoader.ts","src/lib/ImageLoader.ts","src/services/TextureLoader.ts","src/services/ViewerDynamics.ts","src/services/ViewerState.ts","src/Viewer.ts"],"sourcesContent":["import * as CONSTANTS from './data/constants';\nimport * as utils from './utils';\nimport * as events from './events';\n\nexport type { AdapterConstructor } from './adapters/AbstractAdapter';\nexport type { DualFisheyeAdapterConfig } from './adapters/DualFisheyeAdapter';\nexport type { EquirectangularAdapterConfig } from './adapters/EquirectangularAdapter';\nexport type { ButtonConfig, ButtonConstructor } from './buttons/AbstractButton';\nexport type { Tooltip, TooltipConfig, TooltipPosition } from './components/Tooltip';\nexport type { Loader } from './components/Loader';\nexport type { Navbar } from './components/Navbar';\nexport type { Notification, NotificationConfig } from './components/Notification';\nexport type { Overlay, OverlayConfig } from './components/Overlay';\nexport type { Panel, PanelConfig } from './components/Panel';\nexport type { TypedEventTarget } from './lib/TypedEventTarget';\nexport type { PluginConstructor } from './plugins/AbstractPlugin';\nexport type { DataHelper } from './services/DataHelper';\nexport type { Renderer, CustomRenderer } from './services/Renderer';\nexport type { TextureLoader } from './services/TextureLoader';\nexport type { ViewerState } from './services/ViewerState';\n\nexport { AbstractAdapter } from './adapters/AbstractAdapter';\nexport { DualFisheyeAdapter } from './adapters/DualFisheyeAdapter';\nexport { EquirectangularAdapter } from './adapters/EquirectangularAdapter';\nexport { AbstractButton } from './buttons/AbstractButton';\nexport { AbstractComponent } from './components/AbstractComponent';\nexport { registerButton } from './components/Navbar';\nexport { Cache } from './data/cache';\nexport { DEFAULTS } from './data/config';\nexport { SYSTEM } from './data/system';\nexport { TypedEvent } from './lib/TypedEventTarget';\nexport { AbstractPlugin, AbstractConfigurablePlugin } from './plugins/AbstractPlugin';\nexport { PSVError } from './PSVError';\nexport { Viewer } from './Viewer';\nexport * from './model';\nexport { CONSTANTS, events, utils };\nexport const VERSION = PKG_VERSION;\n\n/** @internal */\nimport './styles/index.scss';\n","import arrow from '../icons/arrow.svg';\nimport close from '../icons/close.svg';\nimport download from '../icons/download.svg';\nimport fullscreenIn from '../icons/fullscreen-in.svg';\nimport fullscreenOut from '../icons/fullscreen-out.svg';\nimport info from '../icons/info.svg';\nimport menu from '../icons/menu.svg';\nimport zoomIn from '../icons/zoom-in.svg';\nimport zoomOut from '../icons/zoom-out.svg';\n\n/**\n * Minimum duration of the animations created with {@link Viewer#animate}\n */\nexport const ANIMATION_MIN_DURATION = 500;\n\n/**\n * Number of pixels below which a mouse move will be considered as a click\n */\nexport const MOVE_THRESHOLD = 4;\n\n/**\n * Delay in milliseconds between two clicks to consider a double click\n */\nexport const DBLCLICK_DELAY = 300;\n\n/**\n * Delay in milliseconds to emulate a long touch\n */\nexport const LONGTOUCH_DELAY = 500;\n\n/**\n * Delay in milliseconds to for the two fingers overlay to appear\n */\nexport const TWOFINGERSOVERLAY_DELAY = 100;\n\n/**\n * Duration in milliseconds of the \"ctrl zoom\" overlay\n */\nexport const CTRLZOOM_TIMEOUT = 2000;\n\n/**\n * Radius of the SphereGeometry, Half-length of the BoxGeometry\n */\nexport const SPHERE_RADIUS = 10;\n\n/**\n * Property name added to viewer element\n */\nexport const VIEWER_DATA = 'photoSphereViewer';\n\n/**\n * CSS class that must be applied on elements whose mouse events must not bubble to the viewer itself\n */\nexport const CAPTURE_EVENTS_CLASS = 'psv--capture-event';\n\n/**\n * Actions available for {@link ViewerConfig['keyboardActions']} configuration\n */\nexport enum ACTIONS {\n ROTATE_UP = 'ROTATE_UP',\n ROTATE_DOWN = 'ROTATE_DOWN',\n ROTATE_RIGHT = 'ROTATE_RIGHT',\n ROTATE_LEFT = 'ROTATE_LEFT',\n ZOOM_IN = 'ZOOM_IN',\n ZOOM_OUT = 'ZOOM_OUT',\n}\n\n/**\n * Internal identifiers for various stuff\n * @internal\n */\nexport const IDS = {\n MENU: 'menu',\n TWO_FINGERS: 'twoFingers',\n CTRL_ZOOM: 'ctrlZoom',\n ERROR: 'error',\n DESCRIPTION: 'description',\n};\n\n/**\n * Subset of keyboard codes\n */\nexport const KEY_CODES = {\n Enter: 'Enter',\n Control: 'Control',\n Escape: 'Escape',\n Space: ' ',\n PageUp: 'PageUp',\n PageDown: 'PageDown',\n ArrowLeft: 'ArrowLeft',\n ArrowUp: 'ArrowUp',\n ArrowRight: 'ArrowRight',\n ArrowDown: 'ArrowDown',\n Delete: 'Delete',\n Plus: '+',\n Minus: '-',\n};\n\n/**\n * Collection of SVG icons\n */\nexport const ICONS = {\n arrow,\n close,\n download,\n fullscreenIn,\n fullscreenOut,\n info,\n menu,\n zoomIn,\n zoomOut,\n};\n\n/**\n * Collection of easing functions\n * @see https://gist.github.com/frederickk/6165768\n */\nexport const EASINGS: Record<string, (t: number) => number> = {\n linear: (t: number) => t,\n\n inQuad: (t: number) => t * t,\n outQuad: (t: number) => t * (2 - t),\n inOutQuad: (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),\n\n inCubic: (t: number) => t * t * t,\n outCubic: (t: number) => --t * t * t + 1,\n inOutCubic: (t: number) => (t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1),\n\n inQuart: (t: number) => t * t * t * t,\n outQuart: (t: number) => 1 - --t * t * t * t,\n inOutQuart: (t: number) => (t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t),\n\n inQuint: (t: number) => t * t * t * t * t,\n outQuint: (t: number) => 1 + --t * t * t * t * t,\n inOutQuint: (t: number) => (t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t),\n\n inSine: (t: number) => 1 - Math.cos(t * (Math.PI / 2)),\n outSine: (t: number) => Math.sin(t * (Math.PI / 2)),\n inOutSine: (t: number) => 0.5 - 0.5 * Math.cos(Math.PI * t),\n\n inExpo: (t: number) => Math.pow(2, 10 * (t - 1)),\n outExpo: (t: number) => 1 - Math.pow(2, -10 * t),\n inOutExpo: (t: number) => ((t = t * 2 - 1) < 0 ? 0.5 * Math.pow(2, 10 * t) : 1 - 0.5 * Math.pow(2, -10 * t)),\n\n inCirc: (t: number) => 1 - Math.sqrt(1 - t * t),\n outCirc: (t: number) => Math.sqrt(1 - (t - 1) * (t - 1)),\n inOutCirc: (t: number) => (t *= 2) < 1 ? 0.5 - 0.5 * Math.sqrt(1 - t * t) : 0.5 + 0.5 * Math.sqrt(1 - (t -= 2) * t),\n};\n","<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"40 40 432 432\"><g transform=\"rotate(0, 256, 256)\"><path fill=\"currentColor\" d=\"M425.23 210.55H227.39a5 5 0 01-3.53-8.53l56.56-56.57a45.5 45.5 0 000-64.28 45.15 45.15 0 00-32.13-13.3 45.15 45.15 0 00-32.14 13.3L41.32 256l174.83 174.83a45.15 45.15 0 0032.14 13.3 45.15 45.15 0 0032.13-13.3 45.5 45.5 0 000-64.28l-56.57-56.57a5 5 0 013.54-8.53h197.84c25.06 0 45.45-20.39 45.45-45.45s-20.4-45.45-45.45-45.45z\"/></g><!-- Created by Flatart from the Noun Project --></svg>\n","<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><g fill=\"currentColor\" transform=\" translate(50, 50) rotate(45)\"><rect x=\"-5\" y=\"-65\" width=\"10\" height=\"130\"/><rect x=\"-65\" y=\"-5\" width=\"130\" height=\"10\"/></g></svg>","<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><path fill=\"currentColor\" d=\"M83.3 35.6h-17V3H32.2v32.6H16.6l33.6 32.7 33-32.7z\"/><path fill=\"currentColor\" d=\"M83.3 64.2v16.3H16.6V64.2H-.1v32.6H100V64.2H83.3z\"/><!--Created by Michael Zenaty from the Noun Project--></svg>\n","<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><path fill=\"currentColor\" d=\"M100 40H87.1V18.8h-21V6H100zM100 93.2H66V80.3h21.1v-21H100zM34 93.2H0v-34h12.9v21.1h21zM12.9 40H0V6h34v12.9H12.8z\"/><!--Created by Garrett Knoll from the Noun Project--></svg>\n","<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><path fill=\"currentColor\" d=\"M66 7h13v21h21v13H66zM66 60.3h34v12.9H79v21H66zM0 60.3h34v34H21V73.1H0zM21 7h13v34H0V28h21z\"/><!--Created by Garrett Knoll from the Noun Project--></svg>\n","<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\"><path fill=\"currentColor\" d=\"M28.3 26.1c-1 2.6-1.9 4.8-2.6 7-2.5 7.4-5 14.7-7.2 22-1.3 4.4.5 7.2 4.3 7.8 1.3.2 2.8.2 4.2-.1 8.2-2 11.9-8.6 15.7-15.2l-2.2 2a18.8 18.8 0 0 1-7.4 5.2 2 2 0 0 1-1.6-.2c-.2-.1 0-1 0-1.4l.8-1.8L41.9 28c.5-1.4.9-3 .7-4.4-.2-2.6-3-4.4-6.3-4.4-8.8.2-15 4.5-19.5 11.8-.2.3-.2.6-.3 1.3 3.7-2.8 6.8-6.1 11.8-6.2z\"/><circle fill=\"currentColor\" cx=\"39.3\" cy=\"9.2\" r=\"8.2\"/><!--Created by Arafat Uddin from the Noun Project--></svg>\n","<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"10 10 80 80\"><g fill=\"currentColor\"><circle r=\"10\" cx=\"20\" cy=\"20\"/><circle r=\"10\" cx=\"50\" cy=\"20\"/><circle r=\"10\" cx=\"80\" cy=\"20\"/><circle r=\"10\" cx=\"20\" cy=\"50\"/><circle r=\"10\" cx=\"50\" cy=\"50\"/><circle r=\"10\" cx=\"80\" cy=\"50\"/><circle r=\"10\" cx=\"20\" cy=\"80\"/><circle r=\"10\" cx=\"50\" cy=\"80\"/><circle r=\"10\" cx=\"80\" cy=\"80\"/></g><!-- Created by Richard Kunák from the Noun Project--></svg>\n","<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\"><path fill=\"currentColor\" d=\"M14.043 12.22a7.738 7.738 0 1 0-1.823 1.822l4.985 4.985c.503.504 1.32.504 1.822 0a1.285 1.285 0 0 0 0-1.822l-4.984-4.985zm-6.305 1.043a5.527 5.527 0 1 1 0-11.053 5.527 5.527 0 0 1 0 11.053z\"/><path fill=\"currentColor\" d=\"M8.728 4.009H6.744v2.737H4.006V8.73h2.738v2.736h1.984V8.73h2.737V6.746H8.728z\"/><!--Created by Ryan Canning from the Noun Project--></svg>\n","<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\"><path fill=\"currentColor\" d=\"M14.043 12.22a7.738 7.738 0 1 0-1.823 1.822l4.985 4.985c.503.504 1.32.504 1.822 0a1.285 1.285 0 0 0 0-1.822l-4.984-4.985zm-6.305 1.043a5.527 5.527 0 1 1 0-11.053 5.527 5.527 0 0 1 0 11.053z\"/><path fill=\"currentColor\" d=\"M4.006 6.746h7.459V8.73H4.006z\"/><!--Created by Ryan Canning from the Noun Project--></svg>\n","export * from './browser';\nexport * from './math';\nexport * from './misc';\nexport * from './psv';\n\nexport * from './Animation';\nexport * from './Dynamic';\nexport * from './MultiDynamic';\nexport * from './PressHandler';\nexport * from './Slider';\n","import { Point, Position } from '../model';\n\n/**\n * Ensures a value is within 0 and `max` by wrapping max to 0\n */\nexport function wrap(value: number, max: number): number {\n let result = value % max;\n\n if (result < 0) {\n result += max;\n }\n\n return result;\n}\n\n/**\n * Computes the sum of an array\n */\nexport function sum(array: number[]): number {\n return array.reduce((a, b) => a + b, 0);\n}\n\n/**\n * Computes the distance between two points\n */\nexport function distance(p1: Point, p2: Point): number {\n return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));\n}\n\n/**\n * Computes the angle between two points\n */\nexport function angle(p1: Point, p2: Point): number {\n return Math.atan2(p2.y - p1.y, p2.x - p1.x);\n}\n\n/**\n * Compute the shortest offset between two angles on a sphere\n */\nexport function getShortestArc(from: number, to: number): number {\n const candidates = [\n 0, // direct\n Math.PI * 2, // clock-wise cross zero\n -Math.PI * 2, // counter-clock-wise cross zero\n ];\n\n return candidates.reduce((value, candidate) => {\n const newCandidate = to - from + candidate;\n return Math.abs(newCandidate) < Math.abs(value) ? newCandidate : value;\n }, Infinity);\n}\n\n/**\n * Computes the angle between the current position and a target position\n */\nexport function getAngle(position1: Position, position2: Position): number {\n return Math.acos(\n Math.cos(position1.pitch)\n * Math.cos(position2.pitch)\n * Math.cos(position1.yaw - position2.yaw)\n + Math.sin(position1.pitch)\n * Math.sin(position2.pitch),\n );\n}\n\n/**\n * Returns the distance between two points on a sphere of radius one\n * @see http://www.movable-type.co.uk/scripts/latlong.html\n */\nexport function greatArcDistance([yaw1, pitch1]: [number, number], [yaw2, pitch2]: [number, number]): number {\n // if yaw delta is > PI, apply an offset to only consider the shortest arc\n if (yaw1 - yaw2 > Math.PI) {\n yaw1 -= 2 * Math.PI;\n } else if (yaw1 - yaw2 < -Math.PI) {\n yaw1 += 2 * Math.PI;\n }\n const x = (yaw2 - yaw1) * Math.cos((pitch1 + pitch2) / 2);\n const y = pitch2 - pitch1;\n return Math.sqrt(x * x + y * y);\n}\n","import { Point } from '../model';\nimport { angle, distance } from './math';\n\n/**\n * Get an element in the page by an unknown selector\n */\nexport function getElement(selector: string | HTMLElement): HTMLElement {\n if (typeof selector === 'string') {\n return selector.match(/^[a-z]/i) ? document.getElementById(selector) : document.querySelector(selector);\n } else {\n return selector;\n }\n}\n\n/**\n * Toggles a CSS class\n */\nexport function toggleClass(element: Element, className: string, active?: boolean) {\n if (active === undefined) {\n element.classList.toggle(className);\n } else if (active) {\n element.classList.add(className);\n } else if (!active) {\n element.classList.remove(className);\n }\n}\n\n/**\n * Adds one or several CSS classes to an element\n */\nexport function addClasses(element: Element, className: string) {\n element.classList.add(...className.split(' ').filter(c => !!c));\n}\n\n/**\n * Removes one or several CSS classes to an element\n */\nexport function removeClasses(element: Element, className: string) {\n element.classList.remove(...className.split(' ').filter(c => !!c));\n}\n\n/**\n * Searches if an element has a particular parent at any level including itself\n */\nexport function hasParent(el: HTMLElement, parent: Element): boolean {\n let test: HTMLElement | null = el;\n\n do {\n if (test === parent) {\n return true;\n }\n test = test.parentElement;\n } while (test);\n\n return false;\n}\n\n/**\n * Gets the closest parent matching the selector (can by itself)\n */\nexport function getClosest(el: HTMLElement, selector: string): HTMLElement | null {\n // When el is document or window, the matches does not exist\n if (!el?.matches) {\n return null;\n }\n\n let test: HTMLElement | null = el;\n\n do {\n if (test.matches(selector)) {\n return test;\n }\n test = test.parentElement;\n } while (test);\n\n return null;\n}\n\n/**\n * Returns the first element of the event' composedPath\n */\nexport function getEventTarget(e: Event): HTMLElement | null {\n return e?.composedPath()[0] as HTMLElement || null;\n}\n\n/**\n * Returns the first element of the event's composedPath matching the selector\n */\nexport function getMatchingTarget(e: Event, selector: string): HTMLElement | null {\n if (!e) {\n return null;\n }\n return e.composedPath().find((el) => {\n if (!(el instanceof HTMLElement) && !(el instanceof SVGElement)) {\n return false;\n }\n\n return el.matches(selector);\n }) as HTMLElement;\n}\n\n/**\n * Gets the position of an element in the viewport without reflow\n * Will gives the same result as getBoundingClientRect() as soon as there are no CSS transforms\n */\nexport function getPosition(el: HTMLElement): Point {\n let x = 0;\n let y = 0;\n let test: HTMLElement | null = el;\n\n while (test) {\n x += test.offsetLeft - test.scrollLeft + test.clientLeft;\n y += test.offsetTop - test.scrollTop + test.clientTop;\n test = test.offsetParent as HTMLElement;\n }\n\n x -= window.scrollX;\n y -= window.scrollY;\n\n return { x, y };\n}\n\n/**\n * Gets an element style value\n */\nexport function getStyleProperty(elt: Element, varname: string): string {\n return window.getComputedStyle(elt).getPropertyValue(varname);\n}\n\nexport type TouchData = {\n distance: number;\n angle: number;\n center: Point;\n};\n\n/**\n * Returns data about a touch event (first 2 fingers) : distance, angle, center\n */\nexport function getTouchData(e: TouchEvent): TouchData {\n if (e.touches.length < 2) {\n return null;\n }\n\n const p1 = { x: e.touches[0].clientX, y: e.touches[0].clientY };\n const p2 = { x: e.touches[1].clientX, y: e.touches[1].clientY };\n\n return {\n distance: distance(p1, p2),\n angle: angle(p1, p2),\n center: { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 },\n };\n}\n\nlet fullscreenElement: HTMLElement;\n\n/**\n * Detects if fullscreen is enabled\n */\nexport function isFullscreenEnabled(elt: HTMLElement, isIphone = false): boolean {\n if (isIphone) {\n return elt === fullscreenElement;\n } else {\n return document.fullscreenElement === elt;\n }\n}\n\n/**\n * Enters fullscreen mode\n */\nexport function requestFullscreen(elt: HTMLElement, isIphone = false) {\n if (isIphone) {\n fullscreenElement = elt;\n elt.classList.add('psv-fullscreen-emulation');\n document.dispatchEvent(new Event('fullscreenchange'));\n } else {\n elt.requestFullscreen();\n }\n}\n\n/**\n * Exits fullscreen mode\n */\nexport function exitFullscreen(isIphone = false) {\n if (isIphone) {\n fullscreenElement.classList.remove('psv-fullscreen-emulation');\n fullscreenElement = null;\n document.dispatchEvent(new Event('fullscreenchange'));\n } else {\n document.exitFullscreen();\n }\n}\n","/**\n * Transforms a string to dash-case\n * @see https://github.com/shahata/dasherize\n */\nexport function dasherize(str: string): string {\n return str.replace(/[A-Z](?:(?=[^A-Z])|[A-Z]*(?=[A-Z][^A-Z]|$))/g, (s, i) => {\n return (i > 0 ? '-' : '') + s.toLowerCase();\n });\n}\n\n/**\n * Returns a function, that, when invoked, will only be triggered at most once during a given window of time.\n */\nexport function throttle<T extends (...args: any) => any>(callback: T, wait: number): (...args: Parameters<T>) => void {\n let paused = false;\n return function (this: any, ...args: Parameters<T>) {\n if (!paused) {\n paused = true;\n setTimeout(() => {\n callback.apply(this, args);\n paused = false;\n }, wait);\n }\n };\n}\n\n/**\n * Test if an object is a plain object\n * Test if an object is a plain object, i.e. is constructed by the built-in\n * Object constructor and inherits directly from Object.prototype or null.\n * @see https://github.com/lodash/lodash/blob/master/isPlainObject.js\n */\nexport function isPlainObject<T extends Record<string, any>>(value: any): value is T {\n if (typeof value !== 'object' || value === null || Object.prototype.toString.call(value) !== '[object Object]') {\n return false;\n }\n if (Object.getPrototypeOf(value) === null) {\n return true;\n }\n let proto = value;\n while (Object.getPrototypeOf(proto) !== null) {\n proto = Object.getPrototypeOf(proto);\n }\n return Object.getPrototypeOf(value) === proto;\n}\n\n/**\n * Merges the enumerable attributes of two objects\n * Replaces arrays and alters the target object.\n * @copyright Nicholas Fisher <nfisher110@gmail.com>\n */\nexport function deepmerge<T>(target: T, src: T): T {\n const first = src;\n\n return (function merge(target: any, src: any) {\n if (Array.isArray(src)) {\n if (!target || !Array.isArray(target)) {\n target = [];\n } else {\n target.length = 0;\n }\n src.forEach((e, i) => {\n target[i] = merge(null, e);\n });\n } else if (typeof src === 'object') {\n if (!target || Array.isArray(target)) {\n target = {};\n }\n Object.keys(src).forEach((key) => {\n if (key === '__proto__') {\n return;\n }\n if (typeof src[key] !== 'object' || !src[key] || !isPlainObject(src[key])) {\n target[key] = src[key];\n } else if (src[key] !== first) {\n if (!target[key]) {\n target[key] = merge(null, src[key]);\n } else {\n merge(target[key], src[key]);\n }\n }\n });\n } else {\n target = src;\n }\n\n return target;\n })(target, src);\n}\n\n/**\n * Deeply clones an object\n */\nexport function clone<T>(src: T): T {\n return deepmerge(null as T, src);\n}\n\n/**\n * Tests of an object is empty\n */\nexport function isEmpty(obj: any): boolean {\n return !obj || (Object.keys(obj).length === 0 && obj.constructor === Object);\n}\n\n/**\n * Returns if a valu is null or undefined\n */\nexport function isNil(val: any): val is null | undefined {\n return val === null || val === undefined;\n}\n\n/**\n * Returns the first non null non undefined parameter\n */\nexport function firstNonNull<T>(...values: T[]): T | null {\n for (const val of values) {\n if (!isNil(val)) {\n return val;\n }\n }\n\n return null;\n}\n\n/**\n * Returns deep equality between objects\n * @see https://gist.github.com/egardner/efd34f270cc33db67c0246e837689cb9\n */\nexport function deepEqual(obj1: any, obj2: any): boolean {\n if (obj1 === obj2) {\n return true;\n } else if (isObject(obj1) && isObject(obj2)) {\n if (Object.keys(obj1).length !== Object.keys(obj2).length) {\n return false;\n }\n for (const prop of Object.keys(obj1)) {\n if (!deepEqual(obj1[prop], obj2[prop])) {\n return false;\n }\n }\n return true;\n } else {\n return false;\n }\n}\n\nfunction isObject(obj: any): boolean {\n return typeof obj === 'object' && obj !== null;\n}\n","import { Euler, LinearFilter, LinearMipmapLinearFilter, MathUtils, Quaternion, Texture, Vector3 } from 'three';\nimport { PSVError } from '../PSVError';\nimport { ExtendedPosition, PanoData, Point, ResolvableBoolean } from '../model';\nimport { getStyleProperty } from './browser';\nimport { wrap } from './math';\nimport { clone, firstNonNull, isPlainObject } from './misc';\n\n/**\n * Executes a callback with the value of a ResolvableBoolean\n */\nexport function resolveBoolean(value: boolean | ResolvableBoolean, cb: (val: boolean, init: boolean) => void) {\n if (isPlainObject(value)) {\n cb((value as ResolvableBoolean).initial, true);\n (value as ResolvableBoolean).promise.then(res => cb(res, false));\n } else {\n cb(value as boolean, true);\n }\n}\n\n/**\n * Inverts the result of a ResolvableBoolean\n */\nexport function invertResolvableBoolean(value: ResolvableBoolean): ResolvableBoolean {\n return {\n initial: !value.initial,\n promise: value.promise.then(res => !res),\n };\n}\n\n/**\n * Builds an Error with name 'AbortError'\n */\nexport function getAbortError(): Error {\n const error = new Error('Loading was aborted.');\n error.name = 'AbortError';\n return error;\n}\n\n/**\n * Tests if an Error has name 'AbortError'\n */\nexport function isAbortError(err: Error): boolean {\n return err?.name === 'AbortError';\n}\n\n/**\n * Displays a warning in the console with \"PhotoSphereViewer\" prefix\n */\nexport function logWarn(message: string) {\n console.warn(`PhotoSphereViewer: ${message}`);\n}\n\n/**\n * Checks if an object is a ExtendedPosition, ie has textureX/textureY or yaw/pitch\n */\nexport function isExtendedPosition(object: any): object is ExtendedPosition {\n if (!object || Array.isArray(object)) {\n return false;\n }\n return [\n ['textureX', 'textureY'],\n ['yaw', 'pitch'],\n ].some(([key1, key2]) => {\n return object[key1] !== undefined && object[key2] !== undefined;\n });\n}\n\n/**\n * Returns the value of a given attribute in the panorama metadata\n */\nexport function getXMPValue(data: string, attr: string, intVal = true): number | null {\n // XMP data are stored in children\n let result = data.match('<GPano:' + attr + '>(.*)</GPano:' + attr + '>');\n if (result !== null) {\n const val = intVal ? parseInt(result[1], 10) : parseFloat(result[1]);\n return isNaN(val) ? null : val;\n }\n\n // XMP data are stored in attributes\n result = data.match('GPano:' + attr + '=\"(.*?)\"');\n if (result !== null) {\n const val = intVal ? parseInt(result[1], 10) : parseFloat(result[1]);\n return isNaN(val) ? null : val;\n }\n\n return null;\n}\n\nconst CSS_POSITIONS: Record<string, string> = {\n top: '0%',\n bottom: '100%',\n left: '0%',\n right: '100%',\n center: '50%',\n};\nconst X_VALUES = ['left', 'center', 'right'];\nconst Y_VALUES = ['top', 'center', 'bottom'];\nconst POS_VALUES = [...X_VALUES, ...Y_VALUES];\nconst CENTER = 'center';\n\n/**\n * Translate CSS values like \"top center\" or \"10% 50%\" as top and left positions (0-1 range)\n * The implementation is as close as possible to the \"background-position\" specification\n * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/background-position}\n */\nexport function parsePoint(value: string | Point): Point {\n if (!value) {\n return { x: 0.5, y: 0.5 };\n }\n\n if (typeof value === 'object') {\n return value;\n }\n\n let tokens = value.toLocaleLowerCase().split(' ').slice(0, 2);\n\n if (tokens.length === 1) {\n if (CSS_POSITIONS[tokens[0]]) {\n tokens = [tokens[0], CENTER];\n } else {\n tokens = [tokens[0], tokens[0]];\n }\n }\n\n const xFirst = tokens[1] !== 'left' && tokens[1] !== 'right' && tokens[0] !== 'top' && tokens[0] !== 'bottom';\n\n tokens = tokens.map(token => CSS_POSITIONS[token] || token);\n\n if (!xFirst) {\n tokens.reverse();\n }\n\n const parsed = tokens.join(' ').match(/^([0-9.]+)% ([0-9.]+)%$/);\n\n if (parsed) {\n return {\n x: parseFloat(parsed[1]) / 100,\n y: parseFloat(parsed[2]) / 100,\n };\n } else {\n return { x: 0.5, y: 0.5 };\n }\n}\n\n/**\n * Parse a CSS-like position into an array of position keywords among top, bottom, left, right and center\n * @param value\n * @param [options]\n * @param [options.allowCenter=true] allow \"center center\"\n * @param [options.cssOrder=true] force CSS order (y axis then x axis)\n */\nexport function cleanCssPosition(\n value: string | string[],\n { allowCenter, cssOrder } = {\n allowCenter: true,\n cssOrder: true,\n },\n): [string, string] | null {\n if (!value) {\n return null;\n }\n\n if (typeof value === 'string') {\n value = value.split(' ');\n }\n\n if (value.length === 1) {\n if (value[0] === CENTER) {\n value = [CENTER, CENTER];\n } else if (X_VALUES.indexOf(value[0]) !== -1) {\n value = [CENTER, value[0]];\n } else if (Y_VALUES.indexOf(value[0]) !== -1) {\n value = [value[0], CENTER];\n }\n }\n\n if (value.length !== 2 || POS_VALUES.indexOf(value[0]) === -1 || POS_VALUES.indexOf(value[1]) === -1) {\n logWarn(`Unparsable position ${value}`);\n return null;\n }\n\n if (!allowCenter && value[0] === CENTER && value[1] === CENTER) {\n logWarn(`Invalid position center center`);\n return null;\n }\n\n if (cssOrder && !cssPositionIsOrdered(value)) {\n value = [value[1], value[0]];\n }\n if (value[1] === CENTER && X_VALUES.indexOf(value[0]) !== -1) {\n value = [CENTER, value[0]];\n }\n if (value[0] === CENTER && Y_VALUES.indexOf(value[1]) !== -1) {\n value = [value[1], CENTER];\n }\n\n return value as [string, string];\n}\n\n/**\n * Checks if an array of two positions is ordered (y axis then x axis)\n */\nexport function cssPositionIsOrdered(value: string[]): boolean {\n return Y_VALUES.indexOf(value[0]) !== -1 && X_VALUES.indexOf(value[1]) !== -1;\n}\n\n/**\n * Parses an speed\n * @param speed in radians/degrees/revolutions per second/minute\n * @throws {@link PSVError} when the speed cannot be parsed\n */\nexport function parseSpeed(speed: string | number): number {\n let parsed;\n\n if (typeof speed === 'string') {\n const speedStr = speed.toString().trim();\n\n // Speed extraction\n let speedValue = parseFloat(speedStr.replace(/^(-?[0-9]+(?:\\.[0-9]*)?).*$/, '$1'));\n const speedUnit = speedStr.replace(/^-?[0-9]+(?:\\.[0-9]*)?(.*)$/, '$1').trim();\n\n // \"per minute\" -> \"per second\"\n if (speedUnit.match(/(pm|per minute)$/)) {\n speedValue /= 60;\n }\n\n // Which unit?\n switch (speedUnit) {\n // Degrees per minute / second\n case 'dpm':\n case 'degrees per minute':\n case 'dps':\n case 'degrees per second':\n parsed = MathUtils.degToRad(speedValue);\n break;\n\n // Radians per minute / second\n case 'rdpm':\n case 'radians per minute':\n case 'rdps':\n case 'radians per second':\n parsed = speedValue;\n break;\n\n // Revolutions per minute / second\n case 'rpm':\n case 'revolutions per minute':\n case 'rps':\n case 'revolutions per second':\n parsed = speedValue * Math.PI * 2;\n break;\n\n // Unknown unit\n default:\n throw new PSVError(`Unknown speed unit \"${speedUnit}\"`);\n }\n } else {\n parsed = speed;\n }\n\n return parsed;\n}\n\n/**\n * Converts a speed into a duration for a specific angle to travel\n */\nexport function speedToDuration(value: string | number, angle: number): number {\n if (typeof value !== 'number') {\n // desired radial speed\n const speed = parseSpeed(value);\n // compute duration\n return (angle / Math.abs(speed)) * 1000;\n } else {\n return Math.abs(value);\n }\n}\n\n/**\n * Parses an angle value in radians or degrees and returns a normalized value in radians\n * @param angle - eg: 3.14, 3.14rad, 180deg\n * @param [zeroCenter=false] - normalize between -Pi - Pi instead of 0 - 2*Pi\n * @param [halfCircle=zeroCenter] - normalize between -Pi/2 - Pi/2 instead of -Pi - Pi\n * @throws {@link PSVError} when the angle cannot be parsed\n */\nexport function parseAngle(angle: string | number, zeroCenter = false, halfCircle = zeroCenter): number {\n let parsed;\n\n if (typeof angle === 'string') {\n const match = angle\n .toLowerCase()\n .trim()\n .match(/^(-?[0-9]+(?:\\.[0-9]*)?)(.*)$/);\n\n if (!match) {\n throw new PSVError(`Unknown angle \"${angle}\"`);\n }\n\n const value = parseFloat(match[1]);\n const unit = match[2];\n\n if (unit) {\n switch (unit) {\n case 'deg':\n case 'degs':\n parsed = MathUtils.degToRad(value);\n break;\n case 'rad':\n case 'rads':\n parsed = value;\n break;\n default:\n throw new PSVError(`Unknown angle unit \"${unit}\"`);\n }\n } else {\n parsed = value;\n }\n } else if (typeof angle === 'number' && !isNaN(angle)) {\n parsed = angle;\n } else {\n throw new PSVError(`Unknown angle \"${angle}\"`);\n }\n\n parsed = wrap(zeroCenter ? parsed + Math.PI : parsed, Math.PI * 2);\n\n return zeroCenter\n ? MathUtils.clamp(parsed - Math.PI, -Math.PI / (halfCircle ? 2 : 1), Math.PI / (halfCircle ? 2 : 1))\n : parsed;\n}\n\n/**\n * Creates a THREE texture from an image\n */\nexport function createTexture(img: TexImageSource, mimaps = false): Texture {\n const texture = new Texture(img);\n texture.needsUpdate = true;\n texture.minFilter = mimaps ? LinearMipmapLinearFilter : LinearFilter;\n texture.generateMipmaps = mimaps;\n texture.anisotropy = mimaps ? 2 : 1;\n return texture;\n}\n\nconst quaternion = new Quaternion();\n\n/**\n * Applies the inverse of Euler angles to a vector\n */\nexport function applyEulerInverse(vector: Vector3, euler: Euler) {\n quaternion.setFromEuler(euler).invert();\n vector.applyQuaternion(quaternion);\n}\n\n/**\n * Declaration of configuration parsers, used by {@link getConfigParser}\n */\nexport type ConfigParsers<T, U extends T = T> = {\n [key in keyof T]: (val: T[key], opts: { defValue: U[key]; rawConfig: T }) => U[key];\n};\n\n/**\n * Result of {@link getConfigParser}\n */\nexport type ConfigParser<T, U extends T> = {\n (config: T): U;\n defaults: Required<U>;\n parsers: ConfigParsers<T, U>;\n};\n\n/**\n * Creates a function to validate an user configuration object\n *\n * @template T type of input config\n * @template U type of config after parsing\n *\n * @param defaults the default configuration\n * @param parsers function used to parse/validate the configuration\n *\n * @example\n * ```ts\n * type MyConfig = {\n * value: number;\n * label?: string;\n * };\n *\n * const getConfig<MyConfig>({\n * value: 1,\n * label: 'Title',\n * }, {\n * value(value, { defValue }) {\n * return value < 10 ? value : defValue;\n * }\n * });\n *\n * const config = getConfig({ value: 3 });\n * ```\n */\nexport function getConfigParser<T extends Record<string, any>, U extends T = T>(\n defaults: Required<U>,\n parsers?: ConfigParsers<T, U>,\n): ConfigParser<T, U> {\n const parser = function (userConfig: T): U {\n const rawConfig: U = clone({\n ...defaults,\n ...userConfig,\n });\n\n const config: U = {} as U;\n\n for (let [key, value] of Object.entries(rawConfig) as Array<[keyof T, any]>) {\n if (parsers && key in parsers) {\n value = parsers[key](value, {\n rawConfig: rawConfig,\n defValue: defaults[key],\n });\n } else if (!(key in defaults)) {\n logWarn(`Unknown option ${key as string}`);\n continue;\n }\n\n // @ts-ignore\n config[key] = value;\n }\n\n return config;\n } as ConfigParser<T, U>;\n\n parser.defaults = defaults;\n parser.parsers = parsers || ({} as any);\n\n return parser;\n}\n\n/**\n * Checks if a stylesheet is loaded by the presence of a CSS variable\n */\nexport function checkStylesheet(element: HTMLElement, name: string) {\n if (getStyleProperty(element, `--psv-${name}-loaded`) !== 'true') {\n console.error(`PhotoSphereViewer: stylesheet \"@photo-sphere-viewer/${name}/index.css\" is not loaded`);\n }\n}\n\n/**\n * Checks that a dependency version is the same as the core\n */\nexport function checkVersion(name: string, version: string, coreVersion: string) {\n if (version && version !== coreVersion) {\n console.error(`PhotoSphereViewer: @photo-sphere-viewer/${name} is in version ${version} but @photo-sphere-viewer/core is in version ${coreVersion}`);\n }\n}\n\n/**\n * Checks if the viewer is not used insude a closed shadow DOM\n */\nexport function checkClosedShadowDom(el: Node) {\n do {\n if (el instanceof ShadowRoot && el.mode === 'closed') {\n console.error(`PhotoSphereViewer: closed shadow DOM detected, the viewer might not work as expected`);\n return;\n }\n el = el.parentNode;\n } while (el);\n}\n\n/**\n * Merge XMP data with custom panoData, also apply default behaviour when data is missing\n */\nexport function mergePanoData(width: number, height: number, newPanoData?: PanoData, xmpPanoData?: PanoData): PanoData {\n const panoData: PanoData = {\n isEquirectangular: true,\n fullWidth: firstNonNull(newPanoData?.fullWidth, xmpPanoData?.fullWidth),\n fullHeight: firstNonNull(newPanoData?.fullHeight, xmpPanoData?.fullHeight),\n croppedWidth: width,\n croppedHeight: height,\n croppedX: firstNonNull(newPanoData?.croppedX, xmpPanoData?.croppedX),\n croppedY: firstNonNull(newPanoData?.croppedY, xmpPanoData?.croppedY),\n poseHeading: firstNonNull(newPanoData?.poseHeading, xmpPanoData?.poseHeading, 0),\n posePitch: firstNonNull(newPanoData?.posePitch, xmpPanoData?.posePitch, 0),\n poseRoll: firstNonNull(newPanoData?.poseRoll, xmpPanoData?.poseRoll, 0),\n initialHeading: xmpPanoData?.initialHeading,\n initialPitch: xmpPanoData?.initialPitch,\n initialFov: xmpPanoData?.initialFov,\n };\n\n // construct missing data\n if (!panoData.fullWidth && !panoData.fullHeight) {\n panoData.fullWidth = Math.max(width, height * 2);\n panoData.fullHeight = Math.round(panoData.fullWidth / 2);\n }\n if (!panoData.fullWidth) {\n panoData.fullWidth = panoData.fullHeight * 2;\n }\n if (!panoData.fullHeight) {\n panoData.fullHeight = Math.round(panoData.fullWidth / 2);\n }\n if (panoData.croppedX === null) {\n panoData.croppedX = Math.round((panoData.fullWidth - width) / 2);\n }\n if (panoData.croppedY === null) {\n panoData.croppedY = Math.round((panoData.fullHeight - height) / 2);\n }\n\n // sanity checks\n if (Math.abs(panoData.fullWidth - panoData.fullHeight * 2) > 1) {\n logWarn('Invalid panoData, fullWidth should be twice fullHeight');\n panoData.fullHeight = Math.round(panoData.fullWidth / 2);\n }\n if (panoData.croppedX + panoData.croppedWidth > panoData.fullWidth) {\n logWarn('Invalid panoData, croppedX + croppedWidth > fullWidth');\n panoData.croppedX = panoData.fullWidth - panoData.croppedWidth;\n }\n if (panoData.croppedY + panoData.croppedHeight > panoData.fullHeight) {\n logWarn('Invalid panoData, croppedY + croppedHeight > fullHeight');\n panoData.croppedY = panoData.fullHeight - panoData.croppedHeight;\n }\n if (panoData.croppedX < 0) {\n logWarn('Invalid panoData, croppedX < 0');\n panoData.croppedX = 0;\n }\n if (panoData.croppedY < 0) {\n logWarn('Invalid panoData, croppedY < 0');\n panoData.croppedY = 0;\n }\n\n return panoData;\n}\n","export class PSVError extends Error {\n constructor(message: string, reason?: any) {\n super(reason && reason instanceof Error ? `${message}: ${reason.message}` : message);\n this.name = 'PSVError';\n (Error as any).captureStackTrace?.(this, PSVError);\n }\n}\n","import { EASINGS } from '../data/constants';\n\n/**\n * Options for {@link Animation}\n */\nexport type AnimationOptions<T> = {\n /**\n * interpolated properties\n */\n properties: Partial<Record<keyof T, { start: number; end: number }>>;\n /**\n * duration of the animation\n */\n duration: number;\n /**\n * delay before start\n * @default 0\n */\n delay?: number;\n /**\n * interpoaltion function, see {@link CONSTANTS.EASINGS}\n * @default 'linear'\n */\n easing?: string | ((t: number) => number);\n /**\n * function called for each frame\n */\n onTick: (properties: Record<keyof T, number>, progress: number) => void;\n};\n\ntype PropertyValues = AnimationOptions<any>['properties']['k'];\n\n/**\n * Interpolation helper for animations\n *\n * Implements the Promise API with an additional \"cancel\" method.\n * The promise is resolved with `true` when the animation is completed and `false` if the animation is cancelled.\n * @template T the type of interpoalted properties\n *\n * @example\n * ```ts\n * const anim = new Animation({\n * properties: {\n * width: {start: 100, end: 200}\n * },\n * duration: 5000,\n * onTick: (properties) => element.style.width = `${properties.width}px`;\n * });\n *\n * anim.then((completed) => ...);\n *\n * anim.cancel();\n * ```\n */\nexport class Animation<T = any> implements PromiseLike<boolean> {\n private options: AnimationOptions<T>;\n private easing: (t: number) => number = EASINGS['linear'];\n private callbacks: Array<(complete: boolean) => void> = [];\n private start?: number;\n private delayTimeout: ReturnType<typeof setTimeout>;\n private animationFrame: ReturnType<typeof requestAnimationFrame>;\n\n resolved = false;\n cancelled = false;\n\n constructor(options: AnimationOptions<T>) {\n this.options = options;\n\n if (options) {\n if (options.easing) {\n this.easing = typeof options.easing === 'function'\n ? options.easing\n : EASINGS[options.easing] || EASINGS['linear'];\n }\n\n this.delayTimeout = setTimeout(() => {\n this.delayTimeout = undefined;\n this.animationFrame = window.requestAnimationFrame(t => this.__run(t));\n }, options.delay || 0);\n } else {\n this.resolved = true;\n }\n }\n\n private __run(timestamp: number) {\n if (this.cancelled) {\n return;\n }\n\n // first iteration\n if (!this.start) {\n this.start = timestamp;\n }\n\n // compute progress\n const progress = (timestamp - this.start) / this.options.duration;\n const current = {} as Record<keyof T, number>;\n\n if (progress < 1.0) {\n // interpolate properties\n for (const [name, prop] of Object.entries(this.options.properties) as Array<[string, PropertyValues]>) {\n if (prop) {\n const value = prop.start + (prop.end - prop.start) * this.easing(progress);\n // @ts-ignore\n current[name] = value;\n }\n }\n this.options.onTick(current, progress);\n\n this.animationFrame = window.requestAnimationFrame(t => this.__run(t));\n } else {\n // call onTick one last time with final values\n for (const [name, prop] of Object.entries(this.options.properties) as Array<[string, PropertyValues]>) {\n if (prop) {\n // @ts-ignore\n current[name] = prop.end;\n }\n }\n this.options.onTick(current, 1.0);\n\n this.__resolve(true);\n this.animationFrame = undefined;\n }\n }\n\n private __resolve(value: boolean) {\n if (value) {\n this.resolved = true;\n } else {\n this.cancelled = true;\n }\n this.callbacks.forEach(cb => cb(value));\n this.callbacks.length = 0;\n }\n\n /**\n * Promise chaining\n * @param [onFulfilled] - Called when the animation is complete (true) or cancelled (false)\n */\n then<U>(onFulfilled: (complete: boolean) => PromiseLike<U> | U): Promise<U> {\n if (this.resolved || this.cancelled) {\n return Promise.resolve(this.resolved).then(onFulfilled);\n }\n\n return new Promise((resolve: (complete: boolean) => void) => {\n this.callbacks.push(resolve);\n }).then(onFulfilled);\n }\n\n /**\n * Cancels the animation\n */\n cancel() {\n if (!this.cancelled && !this.resolved) {\n this.__resolve(false);\n\n if (this.delayTimeout) {\n window.clearTimeout(this.delayTimeout);\n this.delayTimeout = undefined;\n }\n if (this.animationFrame) {\n window.cancelAnimationFrame(this.animationFrame);\n this.animationFrame = undefined;\n }\n }\n }\n}\n","import { MathUtils } from 'three';\nimport { PSVError } from '../PSVError';\nimport { wrap } from './math';\n\nconst enum DynamicMode {\n STOP,\n INFINITE,\n POSITION,\n}\n\n/**\n * Represents a variable that can dynamically change with time (using requestAnimationFrame)\n */\nexport class Dynamic {\n private readonly min: number;\n private readonly max: number;\n private readonly wrap: boolean;\n\n private mode = DynamicMode.STOP;\n private speed = 0;\n private speedMult = 0;\n private currentSpeed = 0;\n private target = 0;\n private __current = 0;\n\n get current(): number {\n return this.__current;\n }\n\n private set current(current: number) {\n this.__current = current;\n }\n\n constructor(\n private readonly fn: (val: number) => void,\n config: {\n min: number;\n max: number;\n defaultValue: number;\n wrap: boolean;\n },\n ) {\n this.min = config.min;\n this.max = config.max;\n this.wrap = config.wrap;\n this.current = config.defaultValue;\n\n if (this.wrap && this.min !== 0) {\n throw new PSVError('invalid config');\n }\n\n if (this.fn) {\n this.fn(this.current);\n }\n }\n\n /**\n * Changes base speed\n */\n setSpeed(speed: number) {\n this.speed = speed;\n }\n\n /**\n * Defines the target position\n */\n goto(position: number, speedMult = 1) {\n this.mode = DynamicMode.POSITION;\n this.target = this.wrap ? wrap(position, this.max) : MathUtils.clamp(position, this.min, this.max);\n this.speedMult = speedMult;\n }\n\n /**\n * Increases/decreases the target position\n */\n step(step: number, speedMult = 1) {\n if (speedMult === 0) {\n this.setValue(this.current + step);\n } else {\n if (this.mode !== DynamicMode.POSITION) {\n this.target = this.current;\n }\n this.goto(this.target + step, speedMult);\n }\n }\n\n /**\n * Starts infinite movement\n */\n roll(invert = false, speedMult = 1) {\n this.mode = DynamicMode.INFINITE;\n this.target = invert ? -Infinity : Infinity;\n this.speedMult = speedMult;\n }\n\n /**\n * Stops movement\n */\n stop() {\n this.mode = DynamicMode.STOP;\n }\n\n /**\n * Defines the current position and immediately stops movement\n * @param {number} value\n */\n setValue(value: number): boolean {\n this.target = this.wrap ? wrap(value, this.max) : MathUtils.clamp(value, this.min, this.max);\n this.mode = DynamicMode.STOP;\n this.currentSpeed = 0;\n if (this.target !== this.current) {\n this.current = this.target;\n if (this.fn) {\n this.fn(this.current);\n }\n return true;\n }\n return false;\n }\n\n /**\n * @internal\n */\n update(elapsed: number): boolean {\n // in position mode switch to stop mode when in the decceleration window\n if (this.mode === DynamicMode.POSITION) {\n // in loop mode, alter \"current\" to avoid crossing the origin\n if (this.wrap && Math.abs(this.target - this.current) > this.max / 2) {\n this.current = this.current < this.target ? this.current + this.max : this.current - this.max;\n }\n\n const dstStop = (this.currentSpeed * this.currentSpeed) / (this.speed * this.speedMult * 4);\n if (Math.abs(this.target - this.current) <= dstStop) {\n this.mode = DynamicMode.STOP;\n }\n }\n\n // compute speed\n let targetSpeed = this.mode === DynamicMode.STOP ? 0 : this.speed * this.speedMult;\n if (this.target < this.current) {\n